/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { Component, OnInit, inject, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; import { UserService } from '../../services/user.service'; import { AuthService } from '../../services/auth.service'; import { PaginationComponent } from '../shared/pagination.component'; import { User } from '@library/shared-types'; @Component({ selector: 'app-admin-users', standalone: true, imports: [CommonModule, PaginationComponent], template: `

User Management

@if (loading()) {

Loading users...

} @else if (error()) {

{{ error() }}

} @else {
@for (user of paginatedUsers(); track user.id) {
} @empty {

No users found.

}
}
`, styles: [` .admin-container { max-width: 800px; margin: 0 auto; padding: 2rem; } h2 { color: var(--witch-purple); margin-bottom: 2rem; } .loading, .error, .no-users { text-align: center; padding: 2rem; color: var(--witch-mauve); } .error { color: var(--witch-rose); } .users-list { display: flex; flex-direction: column; gap: 1rem; } .user-card { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; background: var(--witch-moon); border-radius: 8px; box-shadow: 0 2px 8px var(--witch-shadow); transition: all 0.3s; } .user-card.banned { opacity: 0.7; background: var(--witch-lavender); } .user-info { display: flex; align-items: center; gap: 1rem; } .avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; } .avatar-placeholder { width: 48px; height: 48px; border-radius: 50%; background: var(--witch-purple); color: var(--witch-moon); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.2rem; } .user-details { display: flex; flex-direction: column; gap: 0.25rem; } .username { font-weight: 600; color: var(--witch-purple); } .email { font-size: 0.85rem; color: var(--witch-mauve); } .admin-badge { display: inline-block; background: var(--witch-rose); color: var(--witch-moon); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; width: fit-content; } .banned-badge { display: inline-block; background: var(--witch-plum); color: var(--witch-moon); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; width: fit-content; } .discord-badge { display: inline-block; background: #5865f2; color: var(--witch-moon); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; width: fit-content; } .vip-badge { display: inline-block; background: linear-gradient(135deg, #ffd700, #ffaa00); color: #1a1a1a; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; width: fit-content; } .mod-badge { display: inline-block; background: linear-gradient(135deg, #00b894, #00cec9); color: var(--witch-moon); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; width: fit-content; } .staff-badge { display: inline-block; background: linear-gradient(135deg, #e84393, #fd79a8); color: var(--witch-moon); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; width: fit-content; } .user-actions { display: flex; gap: 0.5rem; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s; } .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 8px var(--witch-shadow); } .btn-ban { background: var(--witch-rose); color: var(--witch-moon); } .btn-ban:hover { background: var(--witch-plum); } .btn-unban { background: var(--witch-purple); color: var(--witch-moon); } .btn-unban:hover { background: var(--witch-mauve); color: var(--witch-purple); } `] }) export class AdminUsersComponent implements OnInit { private userService = inject(UserService); private authService = inject(AuthService); private router = inject(Router); users = signal([]); loading = signal(true); error = signal(null); // Pagination state currentPage = signal(1); pageSize = signal(25); paginatedUsers = computed(() => { const start = (this.currentPage() - 1) * this.pageSize(); const end = start + this.pageSize(); return this.users().slice(start, end); }); totalUsers = computed(() => this.users().length); ngOnInit(): void { if (!this.authService.isAdmin()) { this.router.navigate(['/']); return; } this.loadUsers(); } private loadUsers(): void { this.loading.set(true); this.error.set(null); this.userService.getAllUsers().subscribe({ next: (users) => { this.users.set(users); this.loading.set(false); }, error: (err) => { this.error.set(err.message ?? 'Failed to load users'); this.loading.set(false); } }); } banUser(userId: string): void { this.userService.banUser(userId).subscribe({ next: (updatedUser) => { this.users.update(users => users.map(u => u.id === userId ? updatedUser : u) ); }, error: (err) => { this.error.set(err.message ?? 'Failed to ban user'); } }); } unbanUser(userId: string): void { this.userService.unbanUser(userId).subscribe({ next: (updatedUser) => { this.users.update(users => users.map(u => u.id === userId ? updatedUser : u) ); }, error: (err) => { this.error.set(err.message ?? 'Failed to unban user'); } }); } onPageChange(page: number): void { this.currentPage.set(page); } onPageSizeChange(pageSize: number): void { this.pageSize.set(pageSize); // Calculate new current page to stay on approximately the same content const firstItemIndex = (this.currentPage() - 1) * this.pageSize(); const newPage = Math.floor(firstItemIndex / pageSize) + 1; this.currentPage.set(newPage); } }