1
0

19 Коммиты bc1b83cdbb ... c6b8a8cdae

Автор SHA1 Сообщение Дата
  axkuhta c6b8a8cdae Show that there are no comments if there are indeed no comments год назад
  axkuhta ea925fa062 Add a sample comment год назад
  axkuhta e62ab17006 Implement article comments год назад
  axkuhta 14a06270f6 Match article status selector to actual supported states год назад
  axkuhta 07c7435408 Make /articles take the user to the list of published articles and /articles/edit to the control panel год назад
  axkuhta 812e15e9dd Article control panel год назад
  axkuhta a4fa785f99 Show published articles only год назад
  axkuhta 73542bd6cb Article editing + deletion год назад
  axkuhta cee15acfe8 Add article edit/delete links год назад
  axkuhta cbf3923589 Article creation logic год назад
  axkuhta 83d4028ad8 Improve look and feel год назад
  axkuhta 8aa683632a Article creation form год назад
  axkuhta 25c212562b Make articles viewable год назад
  axkuhta 625fc329d2 Improved look год назад
  axkuhta d77771cd04 Add two real sample articles год назад
  axkuhta 362ddfccf9 Article index год назад
  axkuhta fd4a99e943 Rename published_at/unpublished_at -> publish_at/unpublish_at to suggest future action год назад
  axkuhta 72eaddc91b Articles database table год назад
  axkuhta 7a36cdfaba Add Article model + migration год назад

+ 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']);