mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-22 15:11:10 -05:00
feat: implement ride review components
- Add RideReviewComponent for submitting reviews - Star rating input with real-time validation - Rate limiting and anti-spam measures - Edit capabilities for own reviews - Add RideReviewListComponent for displaying reviews - Paginated list with sort/filter options - Helpful vote functionality - Statistics display with rating distribution - Add ReviewModerationComponent for review management - Review queue with status filters - Approve/reject functionality - Batch actions support - Edit capabilities - Update Memory Bank documentation - Document component implementations - Track feature completion - Update technical decisions
This commit is contained in:
280
resources/views/livewire/review-moderation-component.blade.php
Normal file
280
resources/views/livewire/review-moderation-component.blade.php
Normal file
@@ -0,0 +1,280 @@
|
||||
<div class="space-y-6">
|
||||
{{-- Message --}}
|
||||
@if ($message)
|
||||
<div @class([
|
||||
'p-4 mb-4 rounded-lg',
|
||||
'bg-green-100 dark:bg-green-800 text-green-700 dark:text-green-200' => !str_contains($message, 'error'),
|
||||
'bg-red-100 dark:bg-red-800 text-red-700 dark:text-red-200' => str_contains($message, 'error'),
|
||||
])>
|
||||
{{ $message }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Controls --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<div class="p-4 space-y-4">
|
||||
{{-- Status Tabs --}}
|
||||
<div class="border-b border-gray-200 dark:border-gray-700">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<button
|
||||
wire:click="filterByStatus('{{ ReviewStatus::PENDING->value }}')"
|
||||
@class([
|
||||
'pb-4 px-1 border-b-2 font-medium text-sm',
|
||||
'border-primary-500 text-primary-600' => $statusFilter === ReviewStatus::PENDING->value,
|
||||
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' => $statusFilter !== ReviewStatus::PENDING->value,
|
||||
])
|
||||
>
|
||||
Pending
|
||||
@if ($totalPending > 0)
|
||||
<span class="ml-2 bg-primary-100 text-primary-600 py-0.5 px-2 rounded-full text-xs">
|
||||
{{ $totalPending }}
|
||||
</span>
|
||||
@endif
|
||||
</button>
|
||||
<button
|
||||
wire:click="filterByStatus('{{ ReviewStatus::APPROVED->value }}')"
|
||||
@class([
|
||||
'pb-4 px-1 border-b-2 font-medium text-sm',
|
||||
'border-primary-500 text-primary-600' => $statusFilter === ReviewStatus::APPROVED->value,
|
||||
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' => $statusFilter !== ReviewStatus::APPROVED->value,
|
||||
])
|
||||
>
|
||||
Approved
|
||||
</button>
|
||||
<button
|
||||
wire:click="filterByStatus('{{ ReviewStatus::REJECTED->value }}')"
|
||||
@class([
|
||||
'pb-4 px-1 border-b-2 font-medium text-sm',
|
||||
'border-primary-500 text-primary-600' => $statusFilter === ReviewStatus::REJECTED->value,
|
||||
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' => $statusFilter !== ReviewStatus::REJECTED->value,
|
||||
])
|
||||
>
|
||||
Rejected
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{-- Search & Batch Actions --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="max-w-lg flex-1">
|
||||
<label for="search" class="sr-only">Search reviews</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<span class="text-gray-500 dark:text-gray-400">
|
||||
🔍
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
wire:model.live.debounce.300ms="search"
|
||||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-gray-700 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
placeholder="Search reviews..."
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (count($selected) > 0)
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ count($selected) }} selected
|
||||
</span>
|
||||
<button
|
||||
wire:click="batchApprove"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
Approve Selected
|
||||
</button>
|
||||
<button
|
||||
wire:click="batchReject"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
>
|
||||
Reject Selected
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Reviews List --}}
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg">
|
||||
<ul role="list" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@forelse ($reviews as $review)
|
||||
<li class="p-4">
|
||||
<div class="flex items-start space-x-4">
|
||||
{{-- Checkbox --}}
|
||||
<div class="flex-shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $review->id }}"
|
||||
wire:model.live="selected"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
>
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $review->user->name }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $review->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-yellow-400">
|
||||
@for ($i = 1; $i <= 5; $i++)
|
||||
<span @class(['opacity-40' => $i > $review->rating])>★</span>
|
||||
@endfor
|
||||
</div>
|
||||
@if ($review->title)
|
||||
<span class="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{{ $review->title }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<p class="mt-1 text-gray-600 dark:text-gray-400">
|
||||
{{ $review->content }}
|
||||
</p>
|
||||
<div class="mt-2 text-sm text-gray-500">
|
||||
For: {{ $review->ride->name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Actions --}}
|
||||
<div class="flex-shrink-0 flex items-center space-x-2">
|
||||
<button
|
||||
wire:click="editReview({{ $review->id }})"
|
||||
class="inline-flex items-center p-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
wire:click="approve({{ $review->id }})"
|
||||
class="inline-flex items-center p-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
<button
|
||||
wire:click="reject({{ $review->id }})"
|
||||
class="inline-flex items-center p-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="p-4 text-center text-gray-500 dark:text-gray-400">
|
||||
No reviews found.
|
||||
</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
|
||||
{{-- Pagination --}}
|
||||
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
{{ $reviews->links() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Edit Modal --}}
|
||||
@if ($showEditModal)
|
||||
<div
|
||||
class="fixed z-10 inset-0 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
{{-- Background overlay --}}
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
{{-- Modal panel --}}
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<form wire:submit="saveEdit">
|
||||
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="space-y-4">
|
||||
{{-- Rating --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Rating
|
||||
</label>
|
||||
<div class="mt-1 flex items-center space-x-2">
|
||||
@for ($i = 1; $i <= 5; $i++)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('form.rating', {{ $i }})"
|
||||
class="text-2xl focus:outline-none"
|
||||
>
|
||||
<span @class([
|
||||
'text-yellow-400' => $i <= $form['rating'],
|
||||
'text-gray-300 dark:text-gray-600' => $i > $form['rating'],
|
||||
])>★</span>
|
||||
</button>
|
||||
@endfor
|
||||
</div>
|
||||
@error('form.rating')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Title --}}
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
wire:model="form.title"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm dark:bg-gray-700"
|
||||
>
|
||||
@error('form.title')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
<div>
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Content
|
||||
</label>
|
||||
<textarea
|
||||
wire:model="form.content"
|
||||
rows="4"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm dark:bg-gray-700"
|
||||
></textarea>
|
||||
@error('form.content')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Modal footer --}}
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('showEditModal', false)"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
Reference in New Issue
Block a user