1
0

19 Commits bc1b83cdbb ... c6b8a8cdae

Autor SHA1 Mensagem Data
  axkuhta c6b8a8cdae Show that there are no comments if there are indeed no comments há 1 ano atrás
  axkuhta ea925fa062 Add a sample comment há 1 ano atrás
  axkuhta e62ab17006 Implement article comments há 1 ano atrás
  axkuhta 14a06270f6 Match article status selector to actual supported states há 1 ano atrás
  axkuhta 07c7435408 Make /articles take the user to the list of published articles and /articles/edit to the control panel há 1 ano atrás
  axkuhta 812e15e9dd Article control panel há 1 ano atrás
  axkuhta a4fa785f99 Show published articles only há 1 ano atrás
  axkuhta 73542bd6cb Article editing + deletion há 1 ano atrás
  axkuhta cee15acfe8 Add article edit/delete links há 1 ano atrás
  axkuhta cbf3923589 Article creation logic há 1 ano atrás
  axkuhta 83d4028ad8 Improve look and feel há 1 ano atrás
  axkuhta 8aa683632a Article creation form há 1 ano atrás
  axkuhta 25c212562b Make articles viewable há 1 ano atrás
  axkuhta 625fc329d2 Improved look há 1 ano atrás
  axkuhta d77771cd04 Add two real sample articles há 1 ano atrás
  axkuhta 362ddfccf9 Article index há 1 ano atrás
  axkuhta fd4a99e943 Rename published_at/unpublished_at -> publish_at/unpublish_at to suggest future action há 1 ano atrás
  axkuhta 72eaddc91b Articles database table há 1 ano atrás
  axkuhta 7a36cdfaba Add Article model + migration há 1 ano atrás

+ 30 - 0
app/Console/Commands/LoadSampleData.php

@@ -3,6 +3,7 @@
 namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
+use App\Models\Article;
 use App\Models\Author;
 use App\Models\Book;
 
@@ -27,6 +28,35 @@ class LoadSampleData extends Command
      */
     public function handle()
     {
+		$article = new Article;
+		$article->title = "Использование netcat-openbsd в качестве HTTP сервера";
+		$article->description = "Если нет нормального веб-сервера...";
+		$article->content = "<p>При необходимости передать файл по HTTP, но отсутствии какого-либо веб-сервера, можно использовать netcat:</p><pre><code>cat <(echo -en \"HTTP/1.0 200 OK\\r\\n\\r\\n\") arch/x86/boot/bzImage | nc -N -l -p 8080</code></pre>";
+		$article->publish_at = now();
+		$article->status = Article::STATUS_PUBLISHED;
+		$article->save();
+
+        $article->comments()->create([
+			"name" => "somebody",
+			"email" => "a@mail.ru",
+			"content" => "Ну и дичь"
+        ]);
+
+		$article = new Article;
+		$article->title = "Построение спектрограмм при помощи ffmpeg";
+		$article->description = "Спектрограммы с микрофона в реальном времени";
+		$article->content = "<p>Под Linux, если есть pipewire (Или вообще везде так можно?), можно сделать так:<p><pre><code>ffplay -f lavfi -i 'amovie=mpv:f=pulse,aderivative,volume=10,showspectrum=color=viridis:s=1024x384' -an</code></pre><p>Просмотреть доступные устройства можно так:</p><pre><code>$ pw-link -o\nMidi-Bridge:Midi Through:(capture_0) Midi Through Port-0\nalsa_output.pci-0000_00_1f.3.analog-stereo:monitor_FL\nalsa_output.pci-0000_00_1f.3.analog-stereo:monitor_FR\nalsa_input.pci-0000_00_1f.3.analog-stereo:capture_FL\nalsa_input.pci-0000_00_1f.3.analog-stereo:capture_FR\nmpv:output_FL\nmpv:output_FR\n</code></pre>";
+		$article->publish_at = now();
+		$article->status = Article::STATUS_PUBLISHED;
+		$article->save();
+
+		$article = new Article;
+		$article->title = "Test article";
+		$article->description = "Short description";
+		$article->content = "Test article contents";
+		$article->publish_at = now();
+		$article->save();
+
         $author = new Author;
         $author->name = "Howard Johnson";
         $author->save();

+ 98 - 0
app/Http/Controllers/ArticleController.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Models\Article;
+
+class ArticleController extends Controller
+{
+	function published() {
+		return view("published", ["rows" => Article::published()->get()]);
+	}
+
+	function index() {
+		$all = Article::all()->groupBy("status");
+
+		$args = [
+			"drafts" => $all[Article::STATUS_DRAFT] ?? null,
+			"pending" => $all[Article::STATUS_PENDING] ?? null,
+			"published" => $all[Article::STATUS_PUBLISHED] ?? null,
+			"archive" => $all[Article::STATUS_ARCHIVE] ?? null
+		];
+
+		return view("articles", $args);
+	}
+
+	function add() {
+		return view("add_article_form");
+	}
+
+	function view(Article $article) {
+		return view("article", [
+			"article" => $article->load([
+				"comments" => function($query) { $query->recent(); }
+			])
+		]);
+	}
+
+	function edit(Article $article) {
+		return view("edit_article_form", ["article" => $article]);
+	}
+
+	function store(Request $request) {
+		$request->validate([
+			"id" => "nullable|exists:articles",
+			"title" => "required",
+			"description" => "nullable",
+			"content" => "required",
+			"status" => "required",
+			"publish_at" => "nullable",
+			"unpublish_at" => "nullable"
+		], [
+			"name" => "Публикация должна иметь название.",
+			"content" => "Публикация должна иметь текст."
+		]);
+
+		$arr = $request;
+
+		$article = Article::find($arr->id) ?? new Article;
+		$article->title = $arr->title;
+		$article->description = $arr->description ?? explode(".", $arr->content)[0];
+		$article->content = $arr->content;
+		$article->status = $arr->status;
+		$article->publish_at = $arr->publish_at;
+		$article->unpublish_at = $arr->unpublish_at;
+		$article->save();
+
+		return view("success");
+	}
+
+	function comment(Article $article, Request $request) {
+		$request->validate([
+			"name" => "required",
+			"email" => "required|email",
+			"content" => "required"
+		], [
+			"name" => "Укажите ваше имя.",
+			"email" => "Укажите ваш email.",
+			"content" => "Введите комментарий."
+		]);
+
+		$arr = $request;
+
+		$article->comments()->create([
+			"name" => $arr->name,
+			"email" => $arr->email,
+			"content" => $arr->content
+		]);
+
+		return view("success");
+	}
+
+	function drop(Article $article) {
+		$article->delete();
+
+		return view("success");
+	}
+}

+ 28 - 0
app/Models/Article.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class Article extends Model
+{
+	use SoftDeletes;
+
+	const STATUS_DRAFT		= 0;
+	const STATUS_PENDING	= 1;
+	const STATUS_PUBLISHED	= 2;
+	const STATUS_ARCHIVE	= 3;
+
+	function scopePending($query) {
+		$query->where("status", static::STATUS_PENDING);
+	}
+
+	function scopePublished($query) {
+		$query->where("status", static::STATUS_PUBLISHED);
+	}
+
+	function comments() {
+		return $this->morphMany(Comment::class, "commentable");
+	}
+}

+ 34 - 0
database/migrations/2023_12_02_171900_create_articles_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('articles', function (Blueprint $table) {
+            $table->id();
+            $table->timestamps();
+            $table->softDeletes();
+            $table->string("title");
+            $table->string("description");
+            $table->text("content");
+            $table->timestamp("publish_at")->nullable();
+            $table->timestamp("unpublish_at")->nullable();
+            $table->tinyInteger("status")->unsigned()->default(0);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('articles');
+    }
+};

+ 6 - 0
resources/views/add_article_form.blade.php

@@ -0,0 +1,6 @@
+@extends("layouts.app")
+
+@section("content")
+<h1>Добавление публикации</h1>
+@include("forms.article_form")
+@endsection

+ 14 - 0
resources/views/article.blade.php

@@ -0,0 +1,14 @@
+@extends("layouts.app")
+
+@section("content")
+<h1>{{$article->title}}</h1>
+<div>{{$article->publish_at}}</div>
+<div class="article-content">{!! $article->content !!}</div>
+<p>
+	<a href="/article/{{ $article->id }}/edit">Редактировать</a> | <a href="/article/{{ $article->id }}/delete">Удалить</a>
+</p>
+@include("include.comments", [
+	"comment_form_target" => "/article/$article->id/comment",
+	"comments" => $article->comments
+])
+@endsection

+ 98 - 0
resources/views/articles.blade.php

@@ -0,0 +1,98 @@
+@extends("layouts.app")
+
+@section("content")
+<h1>Панель управления публикациями</h1>
+<p>Здесь представлены все публикации в базе данных.</p>
+<p><a href="/article/add">Добавить публикацию</a></p>
+
+<h3>Черновики</h3>
+<table>
+	<tr>
+		<th>Название</th>
+		<th>Изменено</th>
+	</tr>
+	@if (!$drafts)
+		<tr>
+			<td colspan=2 class="td-empty">Пусто</td>
+		</tr>
+	@else
+	@foreach ($drafts as $row)
+		<tr>
+			<td><a href="/article/{{$row->id}}">{{$row->title}}</a></td>
+			<td>{{$row->updated_at}}</td>
+		</tr>
+	@endforeach
+	@endif
+</table>
+<br>
+
+<h3>Ждут публикации</h3>
+<table>
+	<tr>
+		<th>Название</th>
+		<th>Изменено</th>
+		<th>Опубликовать в</th>
+	</tr>
+	@if (!$pending)
+		<tr>
+			<td colspan=3 class="td-empty">Пусто</td>
+		</tr>
+	@else
+	@foreach ($pending as $row)
+		<tr>
+			<td><a href="/article/{{$row->id}}">{{$row->title}}</a></td>
+			<td>{{$row->updated_at}}</td>
+			<td>{{$row->publish_at}}</td>
+		</tr>
+	@endforeach
+	@endif
+</table>
+<br>
+
+<h3>Опубликованы</h3>
+<table>
+	<tr>
+		<th>Название</th>
+		<th>Изменено</th>
+		<th>Опубликовано</th>
+	</tr>
+	@if (!$published)
+		<tr>
+			<td colspan=3 class="td-empty">Пусто</td>
+		</tr>
+	@else
+	@foreach ($published as $row)
+		<tr>
+			<td><a href="/article/{{$row->id}}">{{$row->title}}</a></td>
+			<td>{{$row->updated_at}}</td>
+			<td>{{$row->publish_at}}</td>
+		</tr>
+	@endforeach
+	@endif
+</table>
+<br>
+
+<h3>Архив</h3>
+<table>
+	<tr>
+		<th>Название</th>
+		<th>Изменено</th>
+		<th>Снято с публикации</th>
+	</tr>
+	@if (!$archive)
+		<tr>
+			<td colspan=3 class="td-empty">Пусто</td>
+		</tr>
+	@else
+	@foreach ($archive as $row)
+		<tr>
+			<td><a href="/article/{{$row->id}}">{{$row->title}}</a></td>
+			<td>{{$row->updated_at}}</td>
+			<td>{{$row->unpublish_at}}</td>
+		</tr>
+	@endforeach
+	@endif
+</table>
+<br>
+
+@endsection

+ 6 - 0
resources/views/edit_article_form.blade.php

@@ -0,0 +1,6 @@
+@extends("layouts.app")
+
+@section("content")
+<h1>Редактирование публикации</h1>
+@include("forms.article_form", ["article" => $article])
+@endsection

+ 72 - 0
resources/views/forms/article_form.blade.php

@@ -0,0 +1,72 @@
+<form method="POST" action="">
+	@csrf
+
+	<input type="hidden" name="id" value="{{ $article->id ?? null }}">
+
+	<div>
+		<label>
+			<div>Название:</div>
+			<input type="text" name="title" placeholder="Название..." value="{{ old("title") ?? $article->title ?? null }}">
+			@error("title")
+				<span class="alert">{{ $message }}</span>
+			@enderror
+		</label>
+	</div>
+
+	<div>
+		<label>
+			<div>Короткое описание:</div>
+			<textarea name="description" class="article-description-textarea" placeholder="Если оставить это поле пустым, то короткое описание будет сгенерировано автоматически из первого предложения публикации...">{{ old("description") ?? $article->description ?? null }}</textarea>
+			@error("description")
+				<span class="alert">{{ $message }}</span>
+			@enderror
+		</label>
+	</div>
+
+	<div>
+		<label>
+			<div>Текст публикации:</div>
+			<textarea name="content" class="article-content-textarea" placeholder="Текст публикации...">{{ old("content") ?? $article->content ?? null }}</textarea>
+			@error("content")
+				<span class="alert">{{ $message }}</span>
+			@enderror
+		</label>
+	</div>
+
+	<div>
+		<label>
+			<div>Состояние:</div>
+			<select name="status" value="{{ old("status") ?? $article->status ?? null }}">
+				<option value="0">Черновик</option>
+				<option value="1">Ждёт публикации</option>
+				<option value="2">Опубликовано</option>
+				<option value="3">Архив</option>
+			</select>
+			@error("status")
+				<span class="alert">{{ $message }}</span>
+			@enderror
+		</label>
+	</div>
+
+	<div>
+		<label>
+			<div>Дата и время публикации:</div>
+			<input type="datetime-local" name="publish_at" value="{{ old("publish_at") ?? $article->publish_at ?? null }}">
+			@error("publish_at")
+				<span class="alert">{{ $message }}</span>
+			@enderror
+		</label>
+	</div>
+
+	<div>
+		<label>
+			<div>Дата и время снятия с публикации:</div>
+			<input type="datetime-local" name="unpublish_at" value="{{ old("unpublish_at") ?? $article->unpublish_at ?? null }}">
+			@error("unpublish_at")
+				<span class="alert">{{ $message }}</span>
+			@enderror
+		</label>
+	</div>
+
+	<input type="submit">
+</form>

+ 4 - 0
resources/views/include/comments.blade.php

@@ -34,9 +34,13 @@
 
 	<input type="submit">
 </form>
+@if (count($comments) == 0)
+	<p>Комментариев нет.</p>
+@else
 @foreach ($comments as $row)
 	<p>
 		<div>{{ $row->name }} ({{ $row->created_at }}):</div>
 		<div>{{ $row->content }}</div>
 	</p>
 @endforeach
+@endif

+ 1 - 0
resources/views/include/header.blade.php

@@ -2,6 +2,7 @@
 <a href="/" class="logo">LaravelDemo</a>
 <div class="container">
 <a href="/">Главная</a>
+<a href="/articles">Публикации</a>
 <a href="/authors">Авторы</a>
 <a href="/books">Книги</a>
 </div>

+ 37 - 1
resources/views/layouts/app.blade.php

@@ -81,11 +81,13 @@
 				padding: .2rem 0rem;
 			}
 
-			input, textarea {
+			input, textarea, select {
+				font-family: inherit;
 				font-size: inherit;
 				border: 1px solid #D0D0D0;
 				background-color: inherit;
 				color: inherit;
+				box-sizing: border-box;
 			}
 
 			.alert {
@@ -110,6 +112,40 @@
 			td {
 				border-top: 1px solid #707070;
 			}
+
+			.article-list-entry {
+				margin: 2rem 0;
+			}
+
+			.article-list-title {
+				font-size: 36px;
+				margin: .2rem 0;
+			}
+
+			.article-list-description {
+				margin: 1rem 0;
+			}
+
+			.article-description-textarea {
+				width: 100%;
+			}
+
+			.article-content-textarea {
+				width: 100%;
+				height: 25ex;
+			}
+
+			.article-content {
+				white-space: pre-wrap;
+			}
+
+			.td-empty {
+				text-align: center;
+			}
+
+			pre {
+				overflow: scroll;
+			}
 		</style>
 	</head>
 	<body>

+ 19 - 0
resources/views/published.blade.php

@@ -0,0 +1,19 @@
+@extends("layouts.app")
+
+@section("content")
+<h1>Все публикации</h1>
+
+<p><a href="/articles/edit">Панель управления публикациями</a></p>
+
+@foreach ($rows as $row)
+	<div class="article-list-entry">
+		<div class="article-list-title">{{$row->title}}</div>
+		<div class="article-list-time">{{$row->publish_at}}</div>
+		<div class="article-list-description">{{$row->description}}</div>
+		<div class="article-list-link"><a href="/article/{{$row->id}}">ОТКРЫТЬ</a></div>
+	</div>
+@endforeach
+
+<br>
+
+@endsection

+ 11 - 0
routes/web.php

@@ -43,3 +43,14 @@ Route::post('/author/add', [Controllers\AuthorController::class, 'store']);
 // API
 Route::get('/api/authors', function() { return Resources\AuthorResource::collection(Models\Author::all()); });
 Route::get('/api/books', function() { return Resources\BookResource::collection(Models\Book::all()->load("author")); });
+
+// Публикации
+Route::get('/articles', [Controllers\ArticleController::class, 'published']);
+Route::get('/articles/edit', [Controllers\ArticleController::class, 'index']);
+Route::get('/article/add', [Controllers\ArticleController::class, 'add']);
+Route::get('/article/{article}', [Controllers\ArticleController::class, 'view']);
+Route::get('/article/{article}/delete', [Controllers\ArticleController::class, 'drop']);
+Route::get('/article/{article}/edit', [Controllers\ArticleController::class, 'edit']);
+Route::post('/article/{article}/edit', [Controllers\ArticleController::class, 'store']);
+Route::post('/article/{article}/comment', [Controllers\ArticleController::class, 'comment']);
+Route::post('/article/add', [Controllers\ArticleController::class, 'store']);