AItEKS 6 days ago
parent
commit
c5d662ed39

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

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class PublishScheduledPosts extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'blog:publish-scheduled';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Публикация отложенных статей';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        //
+    }
+}

+ 39 - 0
app/Events/CommentPosted.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Events;
+
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+use App\Models\Comment;
+
+class CommentPosted
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    /**
+     * Create a new event instance.
+     */
+    public function __construct(Comment $comment)
+    {
+        //
+        $this->comment = $comment;
+    }
+
+    /**
+     * Get the channels the event should broadcast on.
+     *
+     * @return array<int, \Illuminate\Broadcasting\Channel>
+     */
+    public function broadcastOn(): array
+    {
+        return [
+            new PrivateChannel('channel-name'),
+        ];
+    }
+}

+ 38 - 0
app/Http/Controllers/Admin/CommentController.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+
+use App\Models\Comment;
+
+class CommentController extends Controller
+{
+    //
+    public function index()
+    {
+        //
+        $comments = Comment::where('is_approved', false)->latest()->get();
+
+        return view('admin.comments.index', compact('comments'));
+    }
+
+    public function approve(Comment $comment)
+    {
+        //
+        $comment->update([
+            'is_approved' => true,
+        ]);
+
+        return back()->with('success', 'Комментарий одобрен!');
+    }
+
+    public function destroy(Comment $comment)
+    {
+        //
+        $comment->delete();
+
+        return back()->with('success', 'Комментарий удален!');
+    }
+}

+ 106 - 0
app/Http/Controllers/Admin/PostController.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+
+use App\Models\Post;
+
+class PostController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index()
+    {
+        //
+        $posts = Post::latest()->paginate(10);
+
+        return view('admin.posts.index', compact('posts'));
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     */
+    public function create()
+    {
+        //
+        return view('admin.posts.form');
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     */
+    public function store(Request $request)
+    {
+        //
+        $validated = $request->validate([
+            'title' => 'required|string',
+            'content' => 'required|string',
+        ]);
+
+        $post = Post::create([
+            'title' => $validated['title'],
+            'slug' => Str::slug($validated['title']),
+            'content' => $validated['content'],
+            'is_published' => $request->boolean('is_published'),
+            'published_at' => $request->boolean('is_published') ? now() : null,
+            'user_id' => auth()->id(),
+            'scheduled_at' => $request->filled('scheduled_at') ? $request->input('scheduled_at') : null,
+        ]);
+
+        return redirect()->route('admin.posts.index')->with('success', 'Пост успешно создан!');
+    }
+
+    /**
+     * Display the specified resource.
+     */
+    public function show(string $id)
+    {
+        //
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     */
+    public function edit(Post $post)
+    {
+        //
+        return view('admin.posts.form', compact('post'));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     */
+    public function update(Request $request, Post $post)
+    {
+        //
+        $validated = $request->validate([
+            'title' => 'required|string',
+            'content' => 'required|string',
+        ]);
+
+        $post->update([
+            'title' => $validated['title'],
+            'content' => $validated['content'],
+            'is_published' => $request->boolean('is_published'),
+            'published_at' => $request->boolean('is_published') ? now() : null, 
+            'scheduled_at' => $request->filled('scheduled_at') ? $request->input('scheduled_at') : null,
+        ]);
+
+        return redirect()->route('admin.posts.index')->with('success', 'Пост успешно изменен!');
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     */
+    public function destroy(Post $post)
+    {
+        //
+        $post->delete();
+
+        return redirect()->route('admin.posts.index')->with('success', 'Пост успешно удален!');
+    }
+}

+ 24 - 0
app/Http/Controllers/CommentController.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+use App\Models\Post;
+use App\Events\CommentPosted;
+
+class CommentController extends Controller
+{
+    //
+    public function store(Request $request, Post $post)
+    {
+        $validated = $request->validate([
+            'author_name' => 'required|string',
+            'body' => 'required|string',
+        ]);
+        $comment = $post->comments()->create($validated);
+        CommentPosted::dispatch($comment);
+
+        return back()->with('success', 'Успешно!');
+    }
+}

+ 23 - 0
app/Http/Controllers/PostController.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+use App\Models\Post;
+
+class PostController extends Controller
+{
+    //
+    public function index()
+    {
+        $posts = Post::published()->paginate(10);
+
+        return view('posts.index', compact('posts'));
+    }
+
+    public function show(Post $post)
+    {
+        return view('posts.show', compact('post'));
+    }
+}

+ 31 - 0
app/Listeners/NotifyAdminOfNewComment.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Listeners;
+
+use App\Events\CommentPosted;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Support\Facades\Log;
+
+class NotifyAdminOfNewComment
+{
+    /**
+     * Create the event listener.
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Handle the event.
+     */
+    public function handle(CommentPosted $event): void
+    {
+        //
+        Log::info('Новый комментарий', [
+            'author' => $event->comment->author_name,
+            'post_title' => $event->comment->post->title,
+        ]);
+    }
+}

+ 21 - 0
app/Models/Comment.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Comment extends Model
+{
+    //
+    protected $fillable = [
+        'post_id',
+        'author_name',
+        'body',
+        'is_approved',
+    ];
+
+    public function post()
+    {
+        return $this->belongsTo(Post::class);
+    }
+}

+ 38 - 0
app/Models/Post.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+
+class Post extends Model
+{
+    //
+    use HasFactory;
+
+    protected $fillable = [
+        'user_id',
+        'title',
+        'slug',
+        'content',
+        'is_published',
+        'published_at',
+        'scheduled_at',
+    ];
+
+    protected $casts = [
+        'published_at' => 'datetime',
+        'scheduled_at' => 'datetime',
+        'is_published' => 'boolean',
+    ];
+
+    public function comments() 
+    {
+        return $this->hasMany(Comment::class);    
+    }
+
+    public function scopePublished($query) 
+    {
+        return $query->where('is_published', true);
+    }    
+}

+ 34 - 0
database/migrations/2025_12_15_070612_create_posts_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('posts', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('user_id')->constrained();
+            $table->string('title');
+            $table->string('slug');
+            $table->string('content');
+            $table->boolean('is_published')->default(false);
+            $table->timestamp('published_at')->nullable();
+            $table->timestamp('scheduled_at')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('posts');
+    }
+};

+ 31 - 0
database/migrations/2025_12_15_071359_create_comments_table.php

@@ -0,0 +1,31 @@
+<?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('comments', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('post_id')->constrained();
+            $table->string('author_name');
+            $table->string('body');
+            $table->boolean('is_approved')->default(false);
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('comments');
+    }
+};

+ 8 - 9
database/seeders/DatabaseSeeder.php

@@ -2,24 +2,23 @@
 
 
 namespace Database\Seeders;
 namespace Database\Seeders;
 
 
-use App\Models\User;
-use Illuminate\Database\Console\Seeds\WithoutModelEvents;
 use Illuminate\Database\Seeder;
 use Illuminate\Database\Seeder;
+use App\Models\User;
+use Illuminate\Support\Facades\Hash;
 
 
 class DatabaseSeeder extends Seeder
 class DatabaseSeeder extends Seeder
 {
 {
-    use WithoutModelEvents;
-
     /**
     /**
      * Seed the application's database.
      * Seed the application's database.
      */
      */
     public function run(): void
     public function run(): void
     {
     {
-        // User::factory(10)->create();
-
         User::factory()->create([
         User::factory()->create([
-            'name' => 'Test User',
-            'email' => 'test@example.com',
+            'name' => 'Admin',
+            'email' => 'admin@admin.com',
+            'password' => Hash::make('password'),
         ]);
         ]);
+        
+        // User::factory(10)->create();
     }
     }
-}
+}

+ 58 - 0
resources/views/admin/comments/index.blade.php

@@ -0,0 +1,58 @@
+@extends('layout')
+
+@section('content')
+    <div class="space-y-6">
+        <h1 class="text-3xl font-bold text-gray-900">Комментарии на модерацию</h1>
+
+        <div class="bg-white shadow-sm border border-gray-100 rounded-lg overflow-hidden">
+            <table class="min-w-full divide-y divide-gray-200">
+                <thead class="bg-gray-50">
+                <tr>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Автор</th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Комментарий</th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Статья</th>
+                    <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>
+                </tr>
+                </thead>
+                <tbody class="bg-white divide-y divide-gray-200">
+                @forelse($comments as $comment)
+                    <tr>
+                        <td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-gray-900">
+                            {{ $comment->author_name }}
+                        </td>
+                        <td class="px-6 py-4 text-sm text-gray-600 max-w-xs truncate">
+                            {{ $comment->body }}
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap text-sm text-indigo-600">
+                            <a href="{{ route('posts.show', $comment->post) }}" target="_blank">
+                                {{ Str::limit($comment->post->title, 20) }}
+                            </a>
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
+                            <!-- Кнопка Одобрить -->
+                            <form action="{{ route('admin.comments.approve', $comment) }}" method="POST" class="inline-block">
+                                @csrf
+                                @method('PATCH')
+                                <button type="submit" class="text-green-600 hover:text-green-900 font-bold">Одобрить</button>
+                            </form>
+
+                            <!-- Кнопка Удалить -->
+                            <form action="{{ route('admin.comments.destroy', $comment) }}" method="POST" class="inline-block" onsubmit="return confirm('Удалить этот спам?');">
+                                @csrf
+                                @method('DELETE')
+                                <button type="submit" class="text-red-600 hover:text-red-900 ml-2">Удалить</button>
+                            </form>
+                        </td>
+                    </tr>
+                @empty
+                    <tr>
+                        <td colspan="4" class="px-6 py-10 text-center text-gray-500">
+                            Нет комментариев, ожидающих проверки. Все чисто! 🎉
+                        </td>
+                    </tr>
+                @endforelse
+                </tbody>
+            </table>
+        </div>
+    </div>
+@endsection

+ 49 - 0
resources/views/admin/posts/form.blade.php

@@ -0,0 +1,49 @@
+@extends('layout')
+
+@section('content')
+    <div class="bg-white p-8 rounded-lg shadow-sm border border-gray-100">
+        <h2 class="text-2xl font-bold mb-6">{{ isset($post) ? 'Редактировать статью' : 'Новая статья' }}</h2>
+
+        <form action="{{ isset($post) ? route('admin.posts.update', $post) : route('admin.posts.store') }}" method="POST">
+            @csrf
+            @if(isset($post)) @method('PUT') @endif
+
+            <div class="space-y-6">
+                <!-- Заголовок -->
+                <div>
+                    <label class="block text-sm font-medium text-gray-700">Заголовок</label>
+                    <input type="text" name="title" value="{{ old('title', $post->title ?? '') }}" class="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
+                </div>
+
+                <!-- Контент -->
+                <div>
+                    <label class="block text-sm font-medium text-gray-700">Текст статьи</label>
+                    <textarea name="content" rows="10" class="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('content', $post->content ?? '') }}</textarea>
+                </div>
+
+                <!-- Настройки публикации -->
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-6 bg-gray-50 p-4 rounded-md">
+                    <div>
+                        <label class="block text-sm font-medium text-gray-700">Запланировать публикацию (дата/время)</label>
+                        <input type="datetime-local" name="scheduled_at" value="{{ old('scheduled_at', isset($post) && $post->scheduled_at ? $post->scheduled_at->format('Y-m-d\TH:i') : '') }}" class="mt-1 block w-full rounded-md border border-gray-300 p-2">
+                        <p class="text-xs text-gray-500 mt-1">Оставьте пустым для немедленной публикации или черновика</p>
+                    </div>
+
+                    <div class="flex items-center mt-6">
+                        <input type="checkbox" name="is_published" id="is_published" value="1" {{ old('is_published', $post->is_published ?? false) ? 'checked' : '' }} class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
+                        <label for="is_published" class="ml-2 block text-sm text-gray-900">
+                            Опубликовать немедленно?
+                        </label>
+                    </div>
+                </div>
+
+                <div class="flex justify-end gap-3">
+                    <a href="{{ route('admin.posts.index') }}" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">Отмена</a>
+                    <button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 shadow-sm">
+                        {{ isset($post) ? 'Обновить' : 'Создать' }}
+                    </button>
+                </div>
+            </div>
+        </form>
+    </div>
+@endsection

+ 69 - 0
resources/views/admin/posts/index.blade.php

@@ -0,0 +1,69 @@
+@extends('layout')
+
+@section('content')
+    <div class="space-y-6">
+        <div class="flex justify-between items-center">
+            <h1 class="text-3xl font-bold text-gray-900">Управление статьями</h1>
+            <a href="{{ route('admin.posts.create') }}" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition">
+                + Создать статью
+            </a>
+        </div>
+
+        <div class="bg-white shadow-sm border border-gray-100 rounded-lg overflow-hidden">
+            <table class="min-w-full divide-y divide-gray-200">
+                <thead class="bg-gray-50">
+                <tr>
+                    <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Заголовок</th>
+                    <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Статус</th>
+                    <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Дата публикации</th>
+                    <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Действия</th>
+                </tr>
+                </thead>
+                <tbody class="bg-white divide-y divide-gray-200">
+                @forelse($posts as $post)
+                    <tr>
+                        <td class="px-6 py-4 whitespace-nowrap">
+                            <div class="text-sm font-medium text-gray-900">{{ $post->title }}</div>
+                            <div class="text-sm text-gray-500 text-xs">{{ Str::limit($post->slug, 30) }}</div>
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap">
+                            @if($post->is_published)
+                                <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
+                                    Опубликовано
+                                </span>
+                            @else
+                                <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
+                                    Черновик
+                                </span>
+                            @endif
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+                            {{ $post->published_at ? $post->published_at->format('d.m.Y H:i') : ($post->scheduled_at ? 'Запл.: ' . $post->scheduled_at->format('d.m H:i') : '-') }}
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
+                            <a href="{{ route('admin.posts.edit', $post) }}" class="text-indigo-600 hover:text-indigo-900">Ред.</a>
+                            
+                            <form action="{{ route('admin.posts.destroy', $post) }}" method="POST" class="inline-block" onsubmit="return confirm('Удалить статью?');">
+                                @csrf
+                                @method('DELETE')
+                                <button type="submit" class="text-red-600 hover:text-red-900 ml-2">Удалить</button>
+                            </form>
+                        </td>
+                    </tr>
+                @empty
+                    <tr>
+                        <td colspan="4" class="px-6 py-4 text-center text-gray-500">
+                            Статей пока нет. Создайте первую!
+                        </td>
+                    </tr>
+                @endforelse
+                </tbody>
+            </table>
+        </div>
+
+        <!-- Пагинация -->
+        <div class="mt-4">
+            {{ $posts->links() }}
+        </div>
+    </div>
+@endsection

+ 67 - 0
resources/views/layout.blade.php

@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>My Simple Blog</title>
+    <!-- Подключаем Tailwind CSS -->
+    <script src="https://cdn.tailwindcss.com"></script>
+    <!-- Шрифт Inter для красоты -->
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
+    <style>
+        body { font-family: 'Inter', sans-serif; }
+    </style>
+</head>
+<body class="bg-gray-50 text-gray-800 flex flex-col min-h-screen">
+
+    <!-- Навигация -->
+    <nav class="bg-white shadow-sm border-b border-gray-100">
+        <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
+            <div class="flex justify-between h-16">
+                <div class="flex">
+                    <a href="{{ route('home') }}" class="flex-shrink-0 flex items-center font-bold text-xl text-indigo-600">
+                        BlogApp
+                    </a>
+                </div>
+                <div class="flex items-center space-x-4">
+                    <!-- Просто ссылка на админку. Если нажать - браузер попросит пароль -->
+                    <a href="{{ route('admin.posts.index') }}" class="text-sm text-indigo-600 hover:text-indigo-800 font-bold">
+                        Панель администратора
+                    </a>
+                </div>
+            </div>
+        </div>
+    </nav>
+
+    <!-- Основной контент -->
+    <main class="flex-grow">
+        <div class="max-w-4xl mx-auto py-10 px-4 sm:px-6 lg:px-8">
+            <!-- Сообщения об успехе -->
+            @if(session('success'))
+                <div class="mb-6 bg-green-50 border-l-4 border-green-400 p-4">
+                    <div class="flex">
+                        <div class="flex-shrink-0">
+                            <svg class="h-5 w-5 text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
+                        </div>
+                        <div class="ml-3">
+                            <p class="text-sm text-green-700">{{ session('success') }}</p>
+                        </div>
+                    </div>
+                </div>
+            @endif
+
+            @yield('content')
+        </div>
+    </main>
+
+    <!-- Футер -->
+    <footer class="bg-white border-t border-gray-200 mt-12">
+        <div class="max-w-4xl mx-auto py-6 px-4 overflow-hidden sm:px-6 lg:px-8">
+            <p class="text-center text-base text-gray-400">
+                &copy; {{ date('Y') }} Simple Blog. Сделано на Laravel.
+            </p>
+        </div>
+    </footer>
+
+</body>
+</html>

+ 40 - 0
resources/views/posts/index.blade.php

@@ -0,0 +1,40 @@
+@extends('layout')
+
+@section('content')
+    <div class="space-y-12">
+        <div class="border-b border-gray-200 pb-5">
+            <h1 class="text-3xl leading-6 font-bold text-gray-900">Последние публикации</h1>
+            <p class="mt-2 max-w-4xl text-sm text-gray-500">Читайте мысли, идеи и истории.</p>
+        </div>
+
+        <div class="grid gap-8">
+            @forelse($posts as $post)
+                <article class="flex flex-col items-start bg-white p-6 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 border border-gray-100">
+                    <div class="flex items-center gap-x-4 text-xs">
+                        <time datetime="{{ $post->published_at }}" class="text-gray-500">
+                            {{ $post->published_at ? $post->published_at->format('d M, Y') : 'Черновик' }}
+                        </time>
+                    </div>
+                    <div class="group relative mt-3">
+                        <h3 class="text-xl font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
+                            <a href="{{ route('posts.show', $post) }}">
+                                <span class="absolute inset-0"></span>
+                                {{ $post->title }}
+                            </a>
+                        </h3>
+                        <p class="mt-3 line-clamp-3 text-sm leading-6 text-gray-600">
+                            {{ Str::limit($post->content, 150) }}
+                        </p>
+                    </div>
+                </article>
+            @empty
+                <p class="text-gray-500 text-center py-10">Публикаций пока нет.</p>
+            @endforelse
+        </div>
+
+        <!-- Пагинация -->
+        <div class="mt-8">
+            {{ $posts->links() }}
+        </div>
+    </div>
+@endsection

+ 72 - 0
resources/views/posts/show.blade.php

@@ -0,0 +1,72 @@
+@extends('layout')
+
+@section('content')
+    <article class="prose prose-indigo prose-lg mx-auto bg-white p-8 rounded-xl shadow-sm border border-gray-100">
+        <div class="mb-8 border-b pb-8">
+            <h1 class="text-4xl font-bold text-gray-900 mb-2">{{ $post->title }}</h1>
+            <span class="text-gray-500 text-sm">
+                 Опубликовано: {{ $post->published_at ? $post->published_at->format('d F Y') : 'Не опубликовано' }}
+            </span>
+        </div>
+
+        <div class="text-gray-700 leading-relaxed mb-12">
+            {!! nl2br(e($post->content)) !!}
+        </div>
+
+        <!-- Секция комментариев -->
+        <div class="border-t pt-10">
+            <h3 class="text-2xl font-bold text-gray-900 mb-6">Комментарии ({{ $post->comments->where('is_approved', true)->count() }})</h3>
+
+            <!-- Форма добавления -->
+            <div class="mb-10 bg-gray-50 p-6 rounded-lg">
+                <h4 class="text-lg font-medium mb-4">Оставить комментарий</h4>
+                <form action="{{ route('comments.store', $post) }}" method="POST">
+                    @csrf
+                    <div class="mb-4">
+                        <label for="author_name" class="block text-sm font-medium text-gray-700">Ваше имя</label>
+                        <input type="text" name="author_name" id="author_name" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border">
+                    </div>
+                    <div class="mb-4">
+                        <label for="body" class="block text-sm font-medium text-gray-700">Сообщение</label>
+                        <textarea name="body" id="body" rows="3" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border"></textarea>
+                    </div>
+                    <button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 text-sm font-medium transition">
+                        Отправить
+                    </button>
+                </form>
+            </div>
+
+            <!-- Список комментариев -->
+            <div class="space-y-6">
+                @foreach($post->comments as $comment)
+                    @if($comment->is_approved)
+                        <div class="flex space-x-4">
+                            <div class="flex-shrink-0">
+                                <div class="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-bold">
+                                    {{ substr($comment->author_name, 0, 1) }}
+                                </div>
+                            </div>
+                            <div>
+                                <div class="text-sm font-bold text-gray-900">{{ $comment->author_name }}</div>
+                                <div class="text-xs text-gray-500">{{ $comment->created_at->diffForHumans() }}</div>
+                                <div class="mt-1 text-gray-700">
+                                    {{ $comment->body }}
+                                </div>
+                            </div>
+                        </div>
+                    @else
+                        @auth
+                            <!-- Админ видит неодобренные комменты -->
+                            <div class="flex space-x-4 opacity-50 border-l-2 border-yellow-400 pl-4">
+                                <div>
+                                    <div class="text-sm font-bold text-gray-900">{{ $comment->author_name }} (На модерации)</div>
+                                    <div class="mt-1 text-gray-700">{{ $comment->body }}</div>
+                                </div>
+                            </div>
+                        @endauth
+                    @endif
+                @endforeach
+            </div>
+        </div>
+    </article>
+@endsection

+ 15 - 3
routes/web.php

@@ -1,7 +1,19 @@
 <?php
 <?php
 
 
 use Illuminate\Support\Facades\Route;
 use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\PostController;
+use App\Http\Controllers\CommentController;
+use App\Http\Controllers\Admin\PostController as AdminPostController;
+use App\Http\Controllers\Admin\CommentController as AdminCommentController;
 
 
-Route::get('/', function () {
-    return view('welcome');
-});
+Route::get('/', [PostController::class, 'index'])->name('home');
+Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
+Route::post('/posts/{post}/comments', [CommentController::class, 'comments.store']);
+
+Route::middleware(['auth.basic'])->prefix('admin')->name('admin.')->group(function () {
+    Route::resource('posts', AdminPostController::class);
+
+    Route::get('/comments', [AdminCommentController::class, 'index'])->name('comments.index');
+    Route::patch('/comments/{comment}/approve', [AdminCommentController::class, 'approve'])->name('comments.approve');
+    Route::delete('/comments/{comment}', [AdminCommentController::class, 'destroy'])->name('comments.destroy');
+});