From 898bddf6147985251b87501e4655bc227908271b Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Sat, 21 Feb 2026 15:50:32 +0200 Subject: [PATCH] comment moderation improvments --- .../CommentModerationController.php | 147 +++++++++ app/Models/Comment.php | 45 +++ ...dd_moderation_fields_to_comments_table.php | 34 ++ .../views/components/layout/navbar.blade.php | 14 + .../views/moderation/comments/edit.blade.php | 211 +++++++++++++ .../views/moderation/comments/index.blade.php | 292 ++++++++++++++++++ routes/web.php | 12 + 7 files changed, 755 insertions(+) create mode 100644 app/Http/Controllers/CommentModerationController.php create mode 100644 database/migrations/2026_02_21_134015_add_moderation_fields_to_comments_table.php create mode 100644 resources/views/moderation/comments/edit.blade.php create mode 100644 resources/views/moderation/comments/index.blade.php diff --git a/app/Http/Controllers/CommentModerationController.php b/app/Http/Controllers/CommentModerationController.php new file mode 100644 index 0000000..62d8b32 --- /dev/null +++ b/app/Http/Controllers/CommentModerationController.php @@ -0,0 +1,147 @@ +filled('status')) { + $query->where('status', $request->status); + } else { + // Default to showing pending comments + $query->where('status', 'pending'); + } + + // Search in comment body or user name + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('body', 'like', "%{$search}%") + ->orWhereHas('user', function ($userQuery) use ($search) { + $userQuery->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + }) + ->orWhereHas('product', function ($productQuery) use ($search) { + $productQuery->where('name', 'like', "%{$search}%"); + }); + }); + } + + // Get counts for tabs + $pendingCount = Comment::where('status', 'pending')->count(); + $approvedCount = Comment::where('status', 'approved')->count(); + $rejectedCount = Comment::where('status', 'rejected')->count(); + + $comments = $query->latest()->paginate(20); + + return view('moderation.comments.index', compact( + 'comments', + 'pendingCount', + 'approvedCount', + 'rejectedCount' + )); + } + + /** + * Show the form for editing a comment. + */ + public function edit(Comment $comment) + { + $comment->load(['user', 'product', 'moderator']); + + return view('moderation.comments.edit', compact('comment')); + } + + /** + * Update the specified comment. + */ + public function update(Request $request, Comment $comment) + { + $validated = $request->validate([ + 'body' => ['required', 'string', 'max:1000'], + ]); + + $comment->update($validated); + + return redirect()->route('moderation.comments.index') + ->with('success', 'Comment updated successfully.'); + } + + /** + * Approve a comment. + */ + public function approve(Request $request, Comment $comment) + { + $validated = $request->validate([ + 'moderation_note' => ['nullable', 'string', 'max:500'], + ]); + + $comment->update([ + 'status' => 'approved', + 'moderated_by' => auth()->id(), + 'moderated_at' => now(), + 'moderation_note' => $validated['moderation_note'] ?? null, + ]); + + return back()->with('success', 'Comment approved successfully.'); + } + + /** + * Reject a comment. + */ + public function reject(Request $request, Comment $comment) + { + $validated = $request->validate([ + 'moderation_note' => ['nullable', 'string', 'max:500'], + ]); + + $comment->update([ + 'status' => 'rejected', + 'moderated_by' => auth()->id(), + 'moderated_at' => now(), + 'moderation_note' => $validated['moderation_note'] ?? null, + ]); + + return back()->with('success', 'Comment rejected successfully.'); + } + + /** + * Set comment back to pending status. + */ + public function setPending(Request $request, Comment $comment) + { + $validated = $request->validate([ + 'moderation_note' => ['nullable', 'string', 'max:500'], + ]); + + $comment->update([ + 'status' => 'pending', + 'moderated_by' => auth()->id(), + 'moderated_at' => now(), + 'moderation_note' => $validated['moderation_note'] ?? null, + ]); + + return back()->with('success', 'Comment status changed to pending review.'); + } + + /** + * Delete a comment. + */ + public function destroy(Comment $comment) + { + $comment->delete(); + + return redirect()->route('moderation.comments.index') + ->with('success', 'Comment deleted successfully.'); + } +} diff --git a/app/Models/Comment.php b/app/Models/Comment.php index 826be85..4ea54d3 100644 --- a/app/Models/Comment.php +++ b/app/Models/Comment.php @@ -16,6 +16,19 @@ class Comment extends Model 'user_id', 'product_id', 'body', + 'status', + 'moderated_by', + 'moderated_at', + 'moderation_note', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'moderated_at' => 'datetime', ]; /** @@ -33,4 +46,36 @@ class Comment extends Model { return $this->belongsTo(Product::class); } + + /** + * Get the moderator who moderated the comment. + */ + public function moderator(): BelongsTo + { + return $this->belongsTo(User::class, 'moderated_by'); + } + + /** + * Check if comment is pending moderation. + */ + public function isPending(): bool + { + return $this->status === 'pending'; + } + + /** + * Check if comment is approved. + */ + public function isApproved(): bool + { + return $this->status === 'approved'; + } + + /** + * Check if comment is rejected. + */ + public function isRejected(): bool + { + return $this->status === 'rejected'; + } } diff --git a/database/migrations/2026_02_21_134015_add_moderation_fields_to_comments_table.php b/database/migrations/2026_02_21_134015_add_moderation_fields_to_comments_table.php new file mode 100644 index 0000000..4572cc7 --- /dev/null +++ b/database/migrations/2026_02_21_134015_add_moderation_fields_to_comments_table.php @@ -0,0 +1,34 @@ +string('status')->default('pending')->after('body'); // pending, approved, rejected + $table->foreignId('moderated_by')->nullable()->after('status')->constrained('users')->nullOnDelete(); + $table->timestamp('moderated_at')->nullable()->after('moderated_by'); + $table->text('moderation_note')->nullable()->after('moderated_at'); + + $table->index('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('comments', function (Blueprint $table) { + $table->dropForeign(['moderated_by']); + $table->dropColumn(['status', 'moderated_by', 'moderated_at', 'moderation_note']); + }); + } +}; diff --git a/resources/views/components/layout/navbar.blade.php b/resources/views/components/layout/navbar.blade.php index a33c170..318f5dc 100644 --- a/resources/views/components/layout/navbar.blade.php +++ b/resources/views/components/layout/navbar.blade.php @@ -164,6 +164,16 @@ Manage Categories + +
+ + + + Moderate Comments +
+
@endif
@@ -234,6 +244,10 @@ class="block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('categories.*') ? 'border-primary-500 text-primary-700 bg-primary-50 dark:bg-primary-900 dark:text-primary-200' : 'border-transparent text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:text-gray-700 dark:hover:text-gray-300' }}"> Manage Categories + + Moderate Comments + @endif @endauth diff --git a/resources/views/moderation/comments/edit.blade.php b/resources/views/moderation/comments/edit.blade.php new file mode 100644 index 0000000..7b21fb9 --- /dev/null +++ b/resources/views/moderation/comments/edit.blade.php @@ -0,0 +1,211 @@ +@extends('layouts.app') + +@section('content') +
+
+ +
+
+
+

Edit Comment

+

Modify comment content

+
+ + Back to Comments + +
+
+ + +
+

Comment Information

+
+ + +
+ Posted: + {{ $comment->created_at->format('M d, Y h:i A') }} +
+
+ Status: + @if($comment->isPending()) + + Pending + + @elseif($comment->isApproved()) + + Approved + + @else + + Rejected + + @endif +
+
+ + @if($comment->moderated_at) +
+
+ Moderated by: {{ $comment->moderator->name ?? 'Unknown' }} + • + Date: {{ $comment->moderated_at->format('M d, Y h:i A') }} +
+ @if($comment->moderation_note) +
+ Note: {{ $comment->moderation_note }} +
+ @endif +
+ @endif +
+ + + + @csrf + @method('PUT') + +
+
+ + + @error('body') +

{{ $message }}

+ @enderror +

+ Edit the comment content if needed (max 1000 characters) +

+
+
+ + +
+ + Cancel + + +
+ + + +
+

Change Comment Status

+ + @if($comment->isPending()) +
+
+ @csrf + +
+ +
+ @csrf + +
+
+ @elseif($comment->isApproved()) +
+
+ @csrf + +
+ +
+ @csrf + +
+
+ @else +
+
+ @csrf + +
+ +
+ @csrf + +
+
+ @endif +
+
+
+@endsection diff --git a/resources/views/moderation/comments/index.blade.php b/resources/views/moderation/comments/index.blade.php new file mode 100644 index 0000000..c5d9b12 --- /dev/null +++ b/resources/views/moderation/comments/index.blade.php @@ -0,0 +1,292 @@ +@extends('layouts.app') + +@section('content') +
+
+ +
+
+
+

Comment Moderation

+

Review and moderate user comments

+
+
+
+ + + @if(session('success')) +
+ {{ session('success') }} +
+ @endif + + @if(session('error')) +
+ {{ session('error') }} +
+ @endif + + + + + +
+ @forelse($comments as $comment) +
+
+ +
+
+
+ {{ strtoupper(substr($comment->user->name, 0, 1)) }} +
+
+
+ + {{ $comment->user->name }} + + • + + {{ $comment->created_at->diffForHumans() }} + +
+ +
+
+ + +
+ @if($comment->isPending()) + + Pending Review + + @elseif($comment->isApproved()) + + Approved + + @else + + Rejected + + @endif +
+
+ + +
+

{{ $comment->body }}

+
+ + + @if($comment->moderated_at) +
+
+ Moderated by: {{ $comment->moderator->name ?? 'Unknown' }} + • + Date: {{ $comment->moderated_at->format('M d, Y h:i A') }} +
+ @if($comment->moderation_note) +
+ Note: {{ $comment->moderation_note }} +
+ @endif +
+ @endif + + +
+ @if($comment->isPending()) + +
+ @csrf + +
+ + +
+ @csrf + +
+ @elseif($comment->isApproved()) + +
+ @csrf + +
+ + +
+ @csrf + +
+ @else + +
+ @csrf + +
+ + +
+ @csrf + +
+ @endif + + + + + + + Edit + + + +
+ @csrf + @method('DELETE') + +
+
+
+
+ @empty +
+ + + +

No comments found

+
+ @endforelse +
+ + + @if($comments->hasPages()) +
+ {{ $comments->links() }} +
+ @endif +
+
+@endsection diff --git a/routes/web.php b/routes/web.php index ed68d21..5b72132 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,6 +6,7 @@ use App\Http\Controllers\AdminController; use App\Http\Controllers\BookmarkController; use App\Http\Controllers\CategoryManagementController; use App\Http\Controllers\CommentController; +use App\Http\Controllers\CommentModerationController; use App\Http\Controllers\ProductController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\UpvoteController; @@ -63,6 +64,17 @@ Route::middleware(['auth', 'moderator'])->group(function () { // Category Management Routes (Moderators & Admins) Route::resource('categories', CategoryManagementController::class); + + // Comment Moderation Routes (Moderators & Admins) + Route::prefix('moderation')->name('moderation.')->group(function () { + Route::get('/comments', [CommentModerationController::class, 'index'])->name('comments.index'); + Route::get('/comments/{comment}/edit', [CommentModerationController::class, 'edit'])->name('comments.edit'); + Route::put('/comments/{comment}', [CommentModerationController::class, 'update'])->name('comments.update'); + Route::post('/comments/{comment}/approve', [CommentModerationController::class, 'approve'])->name('comments.approve'); + Route::post('/comments/{comment}/reject', [CommentModerationController::class, 'reject'])->name('comments.reject'); + Route::post('/comments/{comment}/pending', [CommentModerationController::class, 'setPending'])->name('comments.setPending'); + Route::delete('/comments/{comment}', [CommentModerationController::class, 'destroy'])->name('comments.destroy'); + }); }); // Admin Routes