diff --git a/app/Livewire/AdminCommentSearch.php b/app/Livewire/AdminCommentSearch.php index eac49d1..bef600d 100644 --- a/app/Livewire/AdminCommentSearch.php +++ b/app/Livewire/AdminCommentSearch.php @@ -2,7 +2,10 @@ namespace App\Livewire; +use App\Enums\UserRole; use App\Models\Comment; +use App\Models\User; +use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; @@ -10,37 +13,147 @@ class AdminCommentSearch extends Component { use WithPagination; + #[Url(history: true)] public $search = ''; + #[Url(history: true)] public $userSearch = ''; - public function updatingSearch(): void + #[Url(history: true)] + public $sortField = 'created_at'; + + #[Url(history: true)] + public $sortDirection = 'desc'; + + #[Url(history: true)] + public $perPage = 20; + + public $selected = []; + + public $selectPage = false; + + protected $queryString = [ + 'search' => ['except' => ''], + 'userSearch' => ['except' => ''], + 'sortField' => ['except' => 'created_at'], + 'sortDirection' => ['except' => 'desc'], + 'perPage' => ['except' => 20], + ]; + + protected $allowedSortFields = ['id', 'body', 'created_at', 'user_id']; + + protected $allowedPerPages = [10, 20, 50, 100]; + + public function updatedPerPage(): void { $this->resetPage(); } - public function updatingUserSearch(): void + public function updatedPage(): void { + $this->selectPage = false; + $this->selected = []; + } + + public function updatedSelectPage($value): void + { + if ($value) { + $this->selected = $this->comments->pluck('id')->map(fn ($id) => (string) $id)->toArray(); + } else { + $this->selected = []; + } + } + + public function sortBy(string $field): void + { + if (! in_array($field, $this->allowedSortFields)) { + return; + } + + if ($this->sortField === $field) { + $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + $this->sortField = $field; + $this->sortDirection = 'asc'; + } + } + + public function clearFilters(): void + { + $this->search = ''; + $this->userSearch = ''; + $this->sortField = 'created_at'; + $this->sortDirection = 'desc'; + $this->perPage = 20; $this->resetPage(); } - public function deleteComment($commentId) + public function deleteComment(int $commentId): void { - $comment = Comment::where('id', (int) $commentId)->firstOrFail(); + $comment = Comment::findOrFail($commentId); $comment->delete(); + cache()->flush(); + $this->dispatch('notify', type: 'success', message: 'Comment deleted successfully.'); + } + + public function bulkDelete(): void + { + $count = Comment::whereIn('id', array_map('intval', $this->selected))->delete(); + cache()->flush(); + $this->selected = []; + $this->selectPage = false; + $this->dispatch('notify', type: 'success', message: "{$count} comment(s) deleted."); + } + + public function banCommentAuthor(int $commentId): void + { + $comment = Comment::findOrFail($commentId); + $user = $comment->user; + + if ($user && ! $user->hasRole(UserRole::BANNED)) { + $user->addRole(UserRole::BANNED); + cache()->flush(); + $this->dispatch('notify', type: 'success', message: "{$user->name} has been banned."); + } else { + $this->dispatch('notify', type: 'error', message: 'User is already banned or not found.'); + } + } + + public function bulkBanAuthors(): void + { + $count = 0; + $userIds = Comment::whereIn('id', array_map('intval', $this->selected)) + ->pluck('user_id') + ->unique(); + + foreach ($userIds as $userId) { + $user = User::find($userId); + if ($user && ! $user->hasRole(UserRole::BANNED)) { + $user->addRole(UserRole::BANNED); + $count++; + } + } cache()->flush(); + $this->selected = []; + $this->selectPage = false; + $this->dispatch('notify', type: 'success', message: "{$count} comment author(s) banned."); + } + + public function getCommentsProperty() + { + return Comment::query() + ->with('user') + ->when($this->search !== '', fn ($query) => $query->where('body', 'LIKE', "%{$this->search}%")) + ->when($this->userSearch !== '', fn ($query) => $query->whereHas('user', fn ($q) => $q->where('name', 'LIKE', "%{$this->userSearch}%"))) + ->orderBy($this->sortField, $this->sortDirection) + ->paginate((int) $this->perPage); } public function render() { - $comments = Comment::when($this->search !== '', fn ($query) => $query->where('body', 'LIKE', "%$this->search%")) - ->when($this->userSearch !== '', fn ($query) => $query->whereHas('user', fn ($query) => $query->where('name', 'LIKE', "%{$this->userSearch}%"))) - ->orderBy('created_at', 'DESC') - ->paginate(12); - return view('livewire.admin-comment-search', [ - 'comments' => $comments, + 'comments' => $this->comments, ]); } -} +} \ No newline at end of file diff --git a/app/Livewire/AdminUserSearch.php b/app/Livewire/AdminUserSearch.php index e8ffae2..4893e9e 100644 --- a/app/Livewire/AdminUserSearch.php +++ b/app/Livewire/AdminUserSearch.php @@ -20,32 +20,239 @@ class AdminUserSearch extends Component public $discordId = ''; #[Url(history: true)] - public $patreon = []; + public $email = ''; #[Url(history: true)] - public $banned = []; + public $roleFilter = []; - public function deleteUserComments(int $userID) + #[Url(history: true)] + public $sortField = 'created_at'; + + #[Url(history: true)] + public $sortDirection = 'desc'; + + #[Url(history: true)] + public $perPage = 20; + + public $selected = []; + + public $selectAll = false; + + public $selectPage = false; + + // Modal state + public $showUserModal = false; + + public $modalUser = null; + + public $modalUserComments = []; + + protected $queryString = [ + 'search' => ['except' => ''], + 'discordId' => ['except' => ''], + 'email' => ['except' => ''], + 'roleFilter' => ['except' => []], + 'sortField' => ['except' => 'created_at'], + 'sortDirection' => ['except' => 'desc'], + 'perPage' => ['except' => 20], + ]; + + protected $allowedSortFields = ['id', 'name', 'email', 'discord_id', 'created_at', 'updated_at']; + + protected $allowedPerPages = [10, 20, 50, 100]; + + public function updatedPerPage(): void { - $user = User::where('id', $userID) - ->firstOrFail(); + $this->resetPage(); + } - Comment::where('user_id', $user->id) - ->delete(); + public function updatedPage(): void + { + $this->selectPage = false; + $this->selected = []; + } + public function updatedSelectPage($value): void + { + if ($value) { + $this->selected = $this->users->pluck('id')->map(fn ($id) => (string) $id)->toArray(); + } else { + $this->selected = []; + } + } + + public function sortBy(string $field): void + { + if (! in_array($field, $this->allowedSortFields)) { + return; + } + + if ($this->sortField === $field) { + $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + $this->sortField = $field; + $this->sortDirection = 'asc'; + } + } + + public function clearFilters(): void + { + $this->search = ''; + $this->discordId = ''; + $this->email = ''; + $this->roleFilter = []; + $this->sortField = 'created_at'; + $this->sortDirection = 'desc'; + $this->perPage = 20; + $this->resetPage(); + } + + public function viewUser(int $userId): void + { + $this->modalUser = User::find($userId); + + if ($this->modalUser) { + $this->modalUserComments = $this->modalUser->comments() + ->orderBy('created_at', 'desc') + ->limit(10) + ->get(); + $this->showUserModal = true; + } + } + + public function closeModal(): void + { + $this->showUserModal = false; + $this->modalUser = null; + $this->modalUserComments = []; + } + + public function banUser(int $userId): void + { + $user = User::findOrFail($userId); + $user->addRole(UserRole::BANNED); cache()->flush(); + $this->dispatch('notify', type: 'success', message: "{$user->name} has been banned."); + + if ($this->showUserModal && $this->modalUser?->id === $userId) { + $this->modalUser->refresh(); + } + } + + public function unbanUser(int $userId): void + { + $user = User::findOrFail($userId); + $user->removeRole(UserRole::BANNED); + cache()->flush(); + $this->dispatch('notify', type: 'success', message: "{$user->name} has been unbanned."); + + if ($this->showUserModal && $this->modalUser?->id === $userId) { + $this->modalUser->refresh(); + } + } + + public function grantModerator(int $userId): void + { + $user = User::findOrFail($userId); + $user->addRole(UserRole::MODERATOR); + cache()->flush(); + $this->dispatch('notify', type: 'success', message: "{$user->name} has been granted Moderator role."); + + if ($this->showUserModal && $this->modalUser?->id === $userId) { + $this->modalUser->refresh(); + } + } + + public function revokeModerator(int $userId): void + { + $user = User::findOrFail($userId); + $user->removeRole(UserRole::MODERATOR); + cache()->flush(); + $this->dispatch('notify', type: 'success', message: "Moderator role revoked from {$user->name}."); + + if ($this->showUserModal && $this->modalUser?->id === $userId) { + $this->modalUser->refresh(); + } + } + + public function deleteUserComments(int $userId): void + { + $user = User::findOrFail($userId); + Comment::where('user_id', $user->id)->delete(); + cache()->flush(); + $this->dispatch('notify', type: 'success', message: "All comments from {$user->name} have been deleted."); + + if ($this->showUserModal && $this->modalUser?->id === $userId) { + $this->modalUserComments = collect(); + } + } + + public function bulkBan(): void + { + $count = 0; + foreach ($this->selected as $userId) { + $user = User::find($userId); + if ($user && ! $user->hasRole(UserRole::BANNED)) { + $user->addRole(UserRole::BANNED); + $count++; + } + } + cache()->flush(); + $this->selected = []; + $this->selectPage = false; + $this->dispatch('notify', type: 'success', message: "{$count} user(s) have been banned."); + } + + public function bulkUnban(): void + { + $count = 0; + foreach ($this->selected as $userId) { + $user = User::find($userId); + if ($user && $user->hasRole(UserRole::BANNED)) { + $user->removeRole(UserRole::BANNED); + $count++; + } + } + cache()->flush(); + $this->selected = []; + $this->selectPage = false; + $this->dispatch('notify', type: 'success', message: "{$count} user(s) have been unbanned."); + } + + public function bulkDeleteComments(): void + { + $count = 0; + foreach ($this->selected as $userId) { + $deleted = Comment::where('user_id', (int) $userId)->delete(); + if ($deleted > 0) { + $count++; + } + } + cache()->flush(); + $this->selected = []; + $this->selectPage = false; + $this->dispatch('notify', type: 'success', message: "Deleted comments from {$count} user(s)."); + } + + public function getUsersProperty() + { + return User::query() + ->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%')) + ->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId)) + ->when($this->email !== '', fn ($query) => $query->where('email', 'like', '%'.$this->email.'%')) + ->when(! empty($this->roleFilter), function ($query) { + foreach ($this->roleFilter as $role) { + $query->whereJsonContains('roles', $role); + } + }) + ->orderBy($this->sortField, $this->sortDirection) + ->paginate((int) $this->perPage); } public function render() { - $users = User::when($this->patreon !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::SUPPORTER->value)) - ->when($this->banned !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::BANNED->value)) - ->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%')) - ->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId)) - ->paginate(20); - return view('livewire.admin-user-search', [ - 'users' => $users, + 'users' => $this->users, ]); } -} +} \ No newline at end of file diff --git a/resources/views/livewire/admin-comment-search.blade.php b/resources/views/livewire/admin-comment-search.blade.php index ad9ad46..7d9101d 100644 --- a/resources/views/livewire/admin-comment-search.blade.php +++ b/resources/views/livewire/admin-comment-search.blade.php @@ -1,60 +1,291 @@ -
-
-
-
+
+ + {{-- Notifications --}} +
+ +
+ + {{-- Confirmation Modal --}} +
+
+
+

+

+
+
+ + +
+
+
+ + {{-- Main Content --}} +
+
+ {{-- Filter Bar --}} +
+
+
+ + +
+
+ + +
+
+ +
+
+ + {{-- Per-page and bulk actions bar --}} +
+
+ + + per page +
+ + {{-- Bulk Actions --}} + @if(count($selected) > 0) +
+ {{ count($selected) }} selected + + +
+ @endif +
+
+ + {{-- Table --}} +
- + - - - - + - @foreach($comments as $comment) - - + - - - + + + - @endforeach + @empty + + + + @endforelse
- User - + + - Comment - + +
+ Author + @if($sortField === 'user_id') + + @if($sortDirection === 'asc') + + @else + + @endif + + @endif +
+ +
+ Comment + @if($sortField === 'body') + + @if($sortDirection === 'asc') + + @else + + @endif + + @endif +
- Actions + +
+ Date + @if($sortField === 'created_at') + + @if($sortDirection === 'asc') + + @else + + @endif + + @endif +
Actions
- {{ $comment->user->name }} + @forelse($comments as $comment) +
+ - {{ $comment->body }} - - {{ $comment->created_at }} - - + +
+ @if($comment->user) + +
+ {{ $comment->user->name }} + @if($comment->user->hasRole(\App\Enums\UserRole::BANNED)) + Banned + @endif +
+ @else + Unknown + @endif +
+
+
+

+ {{ $comment->body }} +

+

+ {{ $comment->body }} +

+ @if(strlen($comment->body) > 150) + + @endif +
+
+ + {{ $comment->created_at->diffForHumans() }} + + +
+ + @if($comment->user && !$comment->user->hasRole(\App\Enums\UserRole::BANNED)) + + @endif +
+ +

No comments found matching your filters.

+
+ + {{-- Pagination --}} +
+
+ @if($comments->total() > 0) + Showing {{ $comments->firstItem() }} to {{ $comments->lastItem() }} of {{ $comments->total() }} comments + @endif +
+
+ {{ $comments->links('pagination::tailwind') }} +
+
- {{ $comments->links('pagination::tailwind') }}
-
+
\ No newline at end of file diff --git a/resources/views/livewire/admin-user-search.blade.php b/resources/views/livewire/admin-user-search.blade.php index a1f4bd9..7cec424 100644 --- a/resources/views/livewire/admin-user-search.blade.php +++ b/resources/views/livewire/admin-user-search.blade.php @@ -1,105 +1,435 @@ -
-
-
-
+
+ + {{-- Notifications --}} +
+ +
+ + {{-- Confirmation Modal --}} +
+
+
+

+

+
+
+ + +
+
+
+ + {{-- User Detail Modal --}} + @if($showUserModal && $modalUser) +
+
+ {{-- Modal Header --}} +
+

+ {{ $modalUser->name }} + {{ $modalUser->name }} +

+ +
+ + {{-- Modal Body --}} +
+ {{-- User Info Grid --}} +
+
+
User ID
+
#{{ $modalUser->id }}
+
+
+
Email
+
{{ $modalUser->email ?? 'n/a' }}
+
+
+
Discord ID
+
{{ $modalUser->discord_id ?? 'n/a' }}
+
+
+
Registered
+
{{ $modalUser->created_at->format('Y-m-d H:i') }}
+
+
+
Last Updated
+
{{ $modalUser->updated_at->format('Y-m-d H:i') }}
+
+
+
Comments
+
{{ $modalUser->commentCount() }}
+
+
+ + {{-- Roles --}} +
+
Roles
+
+ @php $roleLabels = [ + \App\Enums\UserRole::ADMINISTRATOR->value => ['bg-purple-600 text-white', 'Admin'], + \App\Enums\UserRole::MODERATOR->value => ['bg-blue-600 text-white', 'Moderator'], + \App\Enums\UserRole::SUPPORTER->value => ['bg-pink-600 text-white', 'Patreon'], + \App\Enums\UserRole::BANNED->value => ['bg-red-600 text-white', 'Banned'], + ]; @endphp + @foreach($roleLabels as $role => $classes) + @if($modalUser->hasRole(\App\Enums\UserRole::from($role))) + {{ $classes[1] }} + @endif + @endforeach + @if(empty($modalUser->roles)) + No special roles + @endif +
+
+ + {{-- Modal Actions --}} +
+ @if($modalUser->hasRole(\App\Enums\UserRole::BANNED)) + + @else + + @endif + + @if($modalUser->hasRole(\App\Enums\UserRole::MODERATOR)) + + @else + + @endif + + +
+ + {{-- Recent Comments --}} + @if($modalUserComments->isNotEmpty()) +
+

Recent Comments (last {{ $modalUserComments->count() }})

+
+ @foreach($modalUserComments as $comment) +
+

{{ $comment->body }}

+ {{ $comment->created_at->diffForHumans() }} +
+ @endforeach +
+
+ @endif +
+ + {{-- Modal Footer --}} +
+ +
+
+
+ @endif + + {{-- Main Table Area --}} +
+
+ {{-- Filter Bar --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ @foreach([ + 'admin' => 'Admin', + 'moderator' => 'Moderator', + 'supporter' => 'Supporter', + 'banned' => 'Banned', + ] as $val => $label) + + @endforeach +
+
+
+ +
+
+ + {{-- Per-page and bulk actions bar --}} +
+
+ + + per page +
+ + {{-- Bulk Actions --}} + @if(count($selected) > 0) +
+ {{ count($selected) }} selected + + + +
+ @endif +
+
+ + {{-- Table --}} +
- + - - - - - - - - + @foreach([ + 'id' => 'ID', + 'discord_id' => 'Discord ID', + 'name' => 'Username', + 'email' => 'Email', + 'created_at' => 'Registered', + 'updated_at' => 'Updated', + ] as $field => $label) + + @endforeach + + - @foreach($users as $user) - - - + - - - - - - + + + + + + + - @endforeach + @empty + + + + @endforelse
- ID - - Discord ID - - - Username - - - Patreon - - - Banned - - - Created at - - Updated at - - Actions + + +
+ {{ $label }} + @if($sortField === $field) + + @if($sortDirection === 'asc') + + @else + + @endif + + @endif +
+
RolesActions
- {{ $user->id }} - - {{ $user->discord_id ?? 'n/a' }} + @forelse($users as $user) +
+ - {{ $user->name }} - - {{ $user->hasRole(\App\Enums\UserRole::SUPPORTER) ? 'Yes' : 'No' }} - - {{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'Yes' : 'No' }} - - {{ $user->created_at->format('Y-m-d') }} - - {{ $user->updated_at->format('Y-m-d') }} - -
- @csrf - - - -
-
{{ $user->id }}{{ $user->discord_id ?? 'n/a' }} + {{ $user->email ?? 'n/a' }}{{ $user->created_at->format('Y-m-d') }}{{ $user->updated_at->format('Y-m-d') }} +
+ @if($user->hasRole(\App\Enums\UserRole::ADMINISTRATOR)) + Admin + @endif + @if($user->hasRole(\App\Enums\UserRole::MODERATOR)) + Mod + @endif + @if($user->hasRole(\App\Enums\UserRole::SUPPORTER)) + Supp. + @endif + @if($user->hasRole(\App\Enums\UserRole::BANNED)) + Banned + @endif +
+
+
+ @if($user->hasRole(\App\Enums\UserRole::BANNED)) + + @else + + @endif + + @if($user->hasRole(\App\Enums\UserRole::MODERATOR)) + + @else + + @endif + + +
+
+ +

No users found matching your filters.

+
+ + {{-- Pagination --}} +
+
+ @if($users->total() > 0) + Showing {{ $users->firstItem() }} to {{ $users->lastItem() }} of {{ $users->total() }} users + @endif +
+
+ {{ $users->links('pagination::tailwind') }} +
+
- {{ $users->links('pagination::tailwind') }}
-
+
\ No newline at end of file