From eb7c63798bf1420c064d184e8a99e97751a57dca Mon Sep 17 00:00:00 2001 From: jask1m Date: Mon, 15 Dec 2025 22:56:45 -0800 Subject: [PATCH] Add live client-side search to user directory * Implemented instant search that filters users as they type in the search box * Fetch all users once on page load using page: -1 parameter instead of paginated requests * Updated backend to support page: -1 for fetching all users without pagination --- api/main_endpoints/routes/User.js | 22 ++- src/Pages/Overview/Overview.js | 223 ++++++++++++++++-------------- 2 files changed, 139 insertions(+), 106 deletions(-) diff --git a/api/main_endpoints/routes/User.js b/api/main_endpoints/routes/User.js index 1a48d45f7..13fc2315d 100644 --- a/api/main_endpoints/routes/User.js +++ b/api/main_endpoints/routes/User.js @@ -143,12 +143,24 @@ router.post('/users', async function(req, res) { }; const sortOrder = orderToInteger[req.query.order] || orderToInteger.default; - // make sure that the page we want to see is 0 by default - // and avoid negative page numbers - let skip = Math.max(Number(req.body.page) || 0, 0); - skip *= ROWS_PER_PAGE; + // Handle pagination: defaults to page 0 if not specified + // Special case: page -1 fetches all users without pagination + // All other negative page numbers are clamped to 0 + let skip, limit; + const pageNum = Number(req.body.page); + + if (pageNum === -1) { + // Fetch all users (no pagination) + skip = 0; + limit = 0; // MongoDB uses 0 to mean "no limit" + } else { + // Regular pagination: clamp negative pages to 0 + skip = Math.max(pageNum || 0, 0) * ROWS_PER_PAGE; + limit = ROWS_PER_PAGE; + } + const total = await User.count(maybeOr); - User.find(maybeOr, { password: 0, }, { skip, limit: ROWS_PER_PAGE, }) + User.find(maybeOr, { password: 0, }, { skip, limit }) .sort({ [sortColumn] : sortOrder }) .then(items => { res.status(OK).send({ items, total, rowsPerPage: ROWS_PER_PAGE, }); diff --git a/src/Pages/Overview/Overview.js b/src/Pages/Overview/Overview.js index b43b584c2..4b00bc5a2 100644 --- a/src/Pages/Overview/Overview.js +++ b/src/Pages/Overview/Overview.js @@ -14,13 +14,11 @@ export default function Overview() { const { user } = useSCE(); const [toggleDelete, setToggleDelete] = useState(false); const [loading, setLoading] = useState(false); - const [paginationText, setPaginationText] = useState(''); - const [users, setUsers] = useState([]); + const [allUsers, setAllUsers] = useState([]); + const [filteredUsers, setFilteredUsers] = useState([]); const [page, setPage] = useState(0); const [total, setTotal] = useState(0); const [userToDelete, setUserToDelete] = useState({}); - const [queryResult, setQueryResult] = useState([]); - const [rowsPerPage, setRowsPerPage] = useState(0); const [query, setQuery] = useState(''); const [currentSortColumn, setCurrentSortColumn] = useState('joinDate'); const [currentSortOrder, setCurrentSortOrder] = useState('desc'); @@ -29,6 +27,22 @@ export default function Overview() { // const [currentQueryType, setCurrentQueryType] = useState('All'); // const queryTypes = ['All', 'Pending', 'Officer', 'Admin']; + async function callDatabase() { + setLoading(true); + const apiResponse = await getAllUsers({ + token: user.token, + query: '', // Filter on client, not server + page: -1, // Special value to fetch all users + sortColumn: 'joinDate', + sortOrder: 'desc' + }); + if (!apiResponse.error) { + setAllUsers(apiResponse.responseData.items); + setTotal(apiResponse.responseData.total); + } + setLoading(false); + } + async function deleteUser(userToDel) { const response = await deleteUserByID( userToDel._id, @@ -36,6 +50,7 @@ export default function Overview() { ); if (response.error) { alert('unable to delete user, check logs'); + return; } if (userToDel._id === user._id) { // logout @@ -43,42 +58,16 @@ export default function Overview() { window.location.reload(); return window.alert('Self-deprecation is an art'); } - setUsers( - users.filter( - child => !child._id.includes(userToDel._id) - ) - ); - setTotal(total - 1); - setQueryResult( - queryResult.filter( - child => !child._id.includes(userToDel._id) - ) - ); + + // Refetch all users after deletion + await callDatabase(); + // The filtering useEffect will automatically reapply current search } function mark(bool) { return bool ? svg.checkMark() : svg.xMark(); } - async function callDatabase() { - setLoading(true); - const sortColumn = currentSortOrder === 'none' ? 'joinDate' : currentSortColumn; - const sortOrder = currentSortOrder === 'none' ? 'desc' : currentSortOrder; - const apiResponse = await getAllUsers({ - token: user.token, - query: query, - page: page, - sortColumn: sortColumn, - sortOrder: sortOrder - }); - if (!apiResponse.error) { - setUsers(apiResponse.responseData.items); - setTotal(apiResponse.responseData.total); - setRowsPerPage(apiResponse.responseData.rowsPerPage); - } - setLoading(false); - } - async function getClubRevenueData() { const response = await getNewPaidMembersThisSemester(user.token); if(!response.error) { @@ -89,25 +78,61 @@ export default function Overview() { useEffect(() => { callDatabase(); getClubRevenueData(); - }, [page, currentSortColumn, currentSortOrder]); + }, []); + // Client-side filtering and sorting useEffect(() => { + if (!allUsers.length) { + setFilteredUsers([]); + return; + } - const amountOfUsersOnCurrentPage = Math.min((page + 1) * rowsPerPage, users.length); - const pageOffset = page * rowsPerPage; - const startingElementNumber = (page * rowsPerPage) + 1; - const endingElementNumber = amountOfUsersOnCurrentPage + pageOffset; - setPaginationText( - <> -

- {startingElementNumber} - {endingElementNumber} / {total} -

-

- Showing {startingElementNumber} to {endingElementNumber} of {total} results -

- - ); - }, [page, rowsPerPage, users, total]); + // Filter users based on query + let filtered = allUsers; + if (query.trim()) { + const searchTerm = query.trim().toLowerCase(); + filtered = allUsers.filter(user => { + return ( + user.firstName?.toLowerCase().includes(searchTerm) || + user.lastName?.toLowerCase().includes(searchTerm) || + user.email?.toLowerCase().includes(searchTerm) + ); + }); + } + + // Sort filtered results + if (currentSortOrder !== 'none') { + filtered = [...filtered].sort((a, b) => { + const aVal = a[currentSortColumn]; + const bVal = b[currentSortColumn]; + + // Handle null/undefined + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + + // Compare based on type + let comparison = 0; + if (typeof aVal === 'string') { + comparison = aVal.localeCompare(bVal); + } else if (typeof aVal === 'number') { + comparison = aVal - bVal; + } else { + // Handle dates + const dateA = new Date(aVal); + const dateB = new Date(bVal); + comparison = dateA.getTime() - dateB.getTime(); + } + + return currentSortOrder === 'asc' ? comparison : -comparison; + }); + } + + setFilteredUsers(filtered); + setTotal(filtered.length); + setPage(0); + + }, [allUsers, query, currentSortColumn, currentSortOrder]); function handleSortUsers(columnName) { if (columnName === null) { @@ -179,37 +204,48 @@ export default function Overview() { // } function maybeRenderPagination() { - const amountOfUsersOnCurrentPage = Math.min((page + 1) * rowsPerPage, users.length); - const pageOffset = page * rowsPerPage; - const endingElementNumber = amountOfUsersOnCurrentPage + pageOffset; - if (users.length) { - return ( - - ); - } - return <>; + const ROWS_PER_PAGE = 20; + const startIdx = page * ROWS_PER_PAGE; + const endIdx = Math.min(startIdx + ROWS_PER_PAGE, total); + + if (filteredUsers.length === 0) return <>; + + return ( + + ); } return ( @@ -254,29 +290,14 @@ export default function Overview() {
@@ -308,7 +329,7 @@ export default function Overview() { - {users.map((user) => ( + {filteredUsers.slice(page * 20, (page + 1) * 20).map((user) => ( @@ -349,7 +370,7 @@ export default function Overview() { - {users.length === 0 && ( + {filteredUsers.length === 0 && (

No results found!