From e9a662d1e2ed56c6bc327808773247bc1fc88627 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 23 Sep 2025 10:58:01 -0700 Subject: [PATCH] feat(admin): add user search and sorting --- components/admin/AdminInterface.tsx | 393 +++++++++++++++++++--------- 1 file changed, 272 insertions(+), 121 deletions(-) diff --git a/components/admin/AdminInterface.tsx b/components/admin/AdminInterface.tsx index 6ed51c7..c6c910b 100644 --- a/components/admin/AdminInterface.tsx +++ b/components/admin/AdminInterface.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { User, UserRole } from '../../types'; import { AccountStatus } from '../../services/auth/auth.constants'; import { databaseService } from '../../services/database'; @@ -15,12 +15,19 @@ const AdminInterface: React.FC = ({ onClose }) => { const [error, setError] = useState(''); const [selectedUser, setSelectedUser] = useState(null); const [newPassword, setNewPassword] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [sortField, setSortField] = useState< + 'createdAt' | 'status' | 'role' | 'username' + >('createdAt'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); useEffect(() => { loadUsers(); }, []); const loadUsers = async () => { + setLoading(true); + setError(''); try { const users = await databaseService.getAllUsers(); setUsers(users); @@ -107,6 +114,88 @@ const AdminInterface: React.FC = ({ onClose }) => { : 'text-blue-600 bg-blue-100'; }; + const statusPriority: Record = { + [AccountStatus.ACTIVE]: 0, + [AccountStatus.PENDING]: 1, + [AccountStatus.SUSPENDED]: 2, + }; + + const rolePriority: Record = { + [UserRole.ADMIN]: 0, + [UserRole.USER]: 1, + }; + + const getStatusPriority = (status?: AccountStatus) => + statusPriority[status as AccountStatus] ?? Number.MAX_SAFE_INTEGER; + + const getRolePriority = (role?: UserRole) => + rolePriority[role as UserRole] ?? Number.MAX_SAFE_INTEGER; + + const sortedUsers = useMemo(() => { + const copy = [...users]; + copy.sort((a, b) => { + let result = 0; + + switch (sortField) { + case 'status': + result = getStatusPriority(a.status) - getStatusPriority(b.status); + break; + case 'role': + result = getRolePriority(a.role) - getRolePriority(b.role); + break; + case 'username': + result = (a.username || '').localeCompare( + b.username || '', + undefined, + { + sensitivity: 'base', + } + ); + break; + case 'createdAt': + default: { + const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0; + const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0; + result = timeA - timeB; + break; + } + } + + if (result === 0) { + result = (a.username || '').localeCompare(b.username || '', undefined, { + sensitivity: 'base', + }); + } + + if (result === 0) { + result = (a.email || '').localeCompare(b.email || '', undefined, { + sensitivity: 'base', + }); + } + + return sortDirection === 'asc' ? result : -result; + }); + + return copy; + }, [users, sortField, sortDirection]); + + const filteredUsers = useMemo(() => { + if (!searchTerm.trim()) { + return sortedUsers; + } + + const query = searchTerm.trim().toLowerCase(); + return sortedUsers.filter(user => { + const username = user.username?.toLowerCase() ?? ''; + const email = user.email?.toLowerCase() ?? ''; + return username.includes(query) || email.includes(query); + }); + }, [sortedUsers, searchTerm]); + + const visibleUsersLabel = searchTerm.trim() + ? `${filteredUsers.length} of ${users.length} users` + : `${filteredUsers.length} users`; + if (currentUser?.role !== UserRole.ADMIN) { return (
@@ -173,7 +262,7 @@ const AdminInterface: React.FC = ({ onClose }) => {

- User Management ({users.length} users) + User Management ({visibleUsersLabel})

- - - - - - - - - - - - - {users.map(user => ( - - + + + + + ))} + +
- User - - Email - - Status - - Role - - Created - - Actions -
-
- {user.avatar ? ( - {user.username} - ) : ( -
- - {user.username.charAt(0).toUpperCase()} - -
- )} -
-
- {user.username} -
-
- ID: {user._id.slice(-8)} +
+
+ + setSearchTerm(e.target.value)} + placeholder='Search by username or email' + className='w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:text-slate-100' + /> +
+
+ + + +
+
+ {filteredUsers.length === 0 ? ( +

+ No users match the current filters. +

+ ) : ( + + + + + + + + + + + + + {filteredUsers.map(user => ( + + - - - - - + + - - ))} - -
+ User + + Email + + Status + + Role + + Created + + Actions +
+
+ {user.avatar ? ( + {user.username} + ) : ( +
+ + {user.username.charAt(0).toUpperCase()} + +
+ )} +
+
+ {user.username} +
+
+ ID: {user._id.slice(-8)} +
- -
- {user.email} - {user.emailVerified && ( - - )} - - - {user.status || 'Unknown'} - - - - {user.role || 'USER'} - - - {user.createdAt - ? new Date(user.createdAt).toLocaleDateString() - : 'Unknown'} - -
- {user.status === AccountStatus.ACTIVE ? ( - - ) : ( - +
+ {user.email} + {user.emailVerified && ( + )} - - {user.password && ( - - )} - - + - Delete - - -
+ {user.status || 'Unknown'} + +
+ + {user.role || 'USER'} + + + {user.createdAt + ? new Date(user.createdAt).toLocaleDateString() + : 'Unknown'} + +
+ {user.status === AccountStatus.ACTIVE ? ( + + ) : ( + + )} + + {user.password && ( + + )} + + +
+
+ )}
)}