Skip to content

Commit c583f2c

Browse files
committed
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
1 parent c08535b commit c583f2c

2 files changed

Lines changed: 128 additions & 95 deletions

File tree

api/main_endpoints/routes/User.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,24 @@ router.post('/users', async function(req, res) {
143143
};
144144
const sortOrder = orderToInteger[req.query.order] || orderToInteger.default;
145145

146-
// make sure that the page we want to see is 0 by default
147-
// and avoid negative page numbers
148-
let skip = Math.max(Number(req.body.page) || 0, 0);
149-
skip *= ROWS_PER_PAGE;
146+
// Handle pagination: defaults to page 0 if not specified
147+
// Special case: page -1 fetches all users without pagination
148+
// All other negative page numbers are clamped to 0
149+
let skip, limit;
150+
const pageNum = Number(req.body.page);
151+
152+
if (pageNum === -1) {
153+
// Fetch all users (no pagination)
154+
skip = 0;
155+
limit = 0; // MongoDB uses 0 to mean "no limit"
156+
} else {
157+
// Regular pagination: clamp negative pages to 0
158+
skip = Math.max(pageNum || 0, 0) * ROWS_PER_PAGE;
159+
limit = ROWS_PER_PAGE;
160+
}
161+
150162
const total = await User.count(maybeOr);
151-
User.find(maybeOr, { password: 0, }, { skip, limit: ROWS_PER_PAGE, })
163+
User.find(maybeOr, { password: 0, }, { skip, limit })
152164
.sort({ [sortColumn] : sortOrder })
153165
.then(items => {
154166
res.status(OK).send({ items, total, rowsPerPage: ROWS_PER_PAGE, });

src/Pages/Overview/Overview.js

Lines changed: 111 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@ export default function Overview() {
1414
const { user } = useSCE();
1515
const [toggleDelete, setToggleDelete] = useState(false);
1616
const [loading, setLoading] = useState(false);
17-
const [paginationText, setPaginationText] = useState('');
18-
const [users, setUsers] = useState([]);
17+
const [allUsers, setAllUsers] = useState([]);
18+
const [filteredUsers, setFilteredUsers] = useState([]);
1919
const [page, setPage] = useState(0);
2020
const [total, setTotal] = useState(0);
2121
const [userToDelete, setUserToDelete] = useState({});
22-
const [queryResult, setQueryResult] = useState([]);
23-
const [rowsPerPage, setRowsPerPage] = useState(0);
2422
const [query, setQuery] = useState('');
2523
const [currentSortColumn, setCurrentSortColumn] = useState('joinDate');
2624
const [currentSortOrder, setCurrentSortOrder] = useState('desc');
@@ -36,24 +34,18 @@ export default function Overview() {
3634
);
3735
if (response.error) {
3836
alert('unable to delete user, check logs');
37+
return;
3938
}
4039
if (userToDel._id === user._id) {
4140
// logout
4241
window.localStorage.removeItem('jwtToken');
4342
window.location.reload();
4443
return window.alert('Self-deprecation is an art');
4544
}
46-
setUsers(
47-
users.filter(
48-
child => !child._id.includes(userToDel._id)
49-
)
50-
);
51-
setTotal(total - 1);
52-
setQueryResult(
53-
queryResult.filter(
54-
child => !child._id.includes(userToDel._id)
55-
)
56-
);
45+
46+
// Refetch all users after deletion
47+
await callDatabase();
48+
// The filtering useEffect will automatically reapply current search
5749
}
5850

5951
function mark(bool) {
@@ -62,19 +54,16 @@ export default function Overview() {
6254

6355
async function callDatabase() {
6456
setLoading(true);
65-
const sortColumn = currentSortOrder === 'none' ? 'joinDate' : currentSortColumn;
66-
const sortOrder = currentSortOrder === 'none' ? 'desc' : currentSortOrder;
6757
const apiResponse = await getAllUsers({
6858
token: user.token,
69-
query: query,
70-
page: page,
71-
sortColumn: sortColumn,
72-
sortOrder: sortOrder
59+
query: '', // Filter on client, not server
60+
page: -1, // Special value to fetch all users
61+
sortColumn: 'joinDate',
62+
sortOrder: 'desc'
7363
});
7464
if (!apiResponse.error) {
75-
setUsers(apiResponse.responseData.items);
65+
setAllUsers(apiResponse.responseData.items);
7666
setTotal(apiResponse.responseData.total);
77-
setRowsPerPage(apiResponse.responseData.rowsPerPage);
7867
}
7968
setLoading(false);
8069
}
@@ -89,25 +78,61 @@ export default function Overview() {
8978
useEffect(() => {
9079
callDatabase();
9180
getClubRevenueData();
92-
}, [page, currentSortColumn, currentSortOrder]);
81+
}, []);
9382

83+
// Client-side filtering and sorting
9484
useEffect(() => {
85+
if (!allUsers.length) {
86+
setFilteredUsers([]);
87+
return;
88+
}
9589

96-
const amountOfUsersOnCurrentPage = Math.min((page + 1) * rowsPerPage, users.length);
97-
const pageOffset = page * rowsPerPage;
98-
const startingElementNumber = (page * rowsPerPage) + 1;
99-
const endingElementNumber = amountOfUsersOnCurrentPage + pageOffset;
100-
setPaginationText(
101-
<>
102-
<p className='md:hidden text-gray-700 dark:text-white'>
103-
{startingElementNumber} - {endingElementNumber} / {total}
104-
</p>
105-
<p className="hidden md:inline-block text-gray-700 dark:text-white">
106-
Showing <span className='font-medium'>{startingElementNumber}</span> to <span className='font-medium'>{endingElementNumber}</span> of <span className='font-medium'>{total}</span> results
107-
</p>
108-
</>
109-
);
110-
}, [page, rowsPerPage, users, total]);
90+
// Filter users based on query
91+
let filtered = allUsers;
92+
if (query.trim()) {
93+
const searchTerm = query.trim().toLowerCase();
94+
filtered = allUsers.filter(user => {
95+
return (
96+
user.firstName?.toLowerCase().includes(searchTerm) ||
97+
user.lastName?.toLowerCase().includes(searchTerm) ||
98+
user.email?.toLowerCase().includes(searchTerm)
99+
);
100+
});
101+
}
102+
103+
// Sort filtered results
104+
if (currentSortOrder !== 'none') {
105+
filtered = [...filtered].sort((a, b) => {
106+
const aVal = a[currentSortColumn];
107+
const bVal = b[currentSortColumn];
108+
109+
// Handle null/undefined
110+
if (aVal == null && bVal == null) return 0;
111+
if (aVal == null) return 1;
112+
if (bVal == null) return -1;
113+
114+
// Compare based on type
115+
let comparison = 0;
116+
if (typeof aVal === 'string') {
117+
comparison = aVal.localeCompare(bVal);
118+
} else if (typeof aVal === 'number') {
119+
comparison = aVal - bVal;
120+
} else {
121+
// Handle dates
122+
const dateA = new Date(aVal);
123+
const dateB = new Date(bVal);
124+
comparison = dateA.getTime() - dateB.getTime();
125+
}
126+
127+
return currentSortOrder === 'asc' ? comparison : -comparison;
128+
});
129+
}
130+
131+
setFilteredUsers(filtered);
132+
setTotal(filtered.length);
133+
setPage(0);
134+
135+
}, [allUsers, query, currentSortColumn, currentSortOrder]);
111136

112137
function handleSortUsers(columnName) {
113138
if (columnName === null) {
@@ -179,37 +204,48 @@ export default function Overview() {
179204
// }
180205

181206
function maybeRenderPagination() {
182-
const amountOfUsersOnCurrentPage = Math.min((page + 1) * rowsPerPage, users.length);
183-
const pageOffset = page * rowsPerPage;
184-
const endingElementNumber = amountOfUsersOnCurrentPage + pageOffset;
185-
if (users.length) {
186-
return (
187-
<nav className='flex justify-start py-6'>
188-
<div className='flex items-center navbar-start'>
189-
<span className="text-gray-700 dark:text-white">
190-
{loading ? '...' : paginationText}
191-
</span>
192-
</div>
193-
<div className='flex justify-end space-x-3 navbar-end'>
194-
<button
195-
className='btn btn-neutral text-gray-800 bg-gray-500 hover:bg-gray-300 dark:text-white dark:bg-gray-700 dark:hover:bg-gray-600'
196-
onClick={() => setPage(page - 1)}
197-
disabled={page === 0 || loading}
198-
>
199-
previous
200-
</button>
201-
<button
202-
className='btn btn-neutral text-gray-800 bg-gray-200 hover:bg-gray-300 dark:text-white dark:bg-gray-700 dark:hover:bg-gray-600'
203-
onClick={() => setPage(page + 1)}
204-
disabled={endingElementNumber >= total || loading}
205-
>
206-
next
207-
</button>
208-
</div>
209-
</nav>
210-
);
211-
}
212-
return <></>;
207+
const ROWS_PER_PAGE = 20;
208+
const startIdx = page * ROWS_PER_PAGE;
209+
const endIdx = Math.min(startIdx + ROWS_PER_PAGE, total);
210+
211+
if (filteredUsers.length === 0) return <></>;
212+
213+
return (
214+
<nav className='flex justify-start py-6'>
215+
<div className='flex items-center navbar-start'>
216+
<span className="text-gray-700 dark:text-white">
217+
{loading ? '...' : (
218+
<>
219+
<p className='md:hidden text-gray-700 dark:text-white'>
220+
{startIdx + 1} - {endIdx} / {total}
221+
</p>
222+
<p className="hidden md:inline-block text-gray-700 dark:text-white">
223+
Showing <span className='font-medium'>{startIdx + 1}</span> to{' '}
224+
<span className='font-medium'>{endIdx}</span> of{' '}
225+
<span className='font-medium'>{total}</span> results
226+
</p>
227+
</>
228+
)}
229+
</span>
230+
</div>
231+
<div className='flex justify-end space-x-3 navbar-end'>
232+
<button
233+
className='btn btn-neutral text-gray-800 bg-gray-500 hover:bg-gray-300 dark:text-white dark:bg-gray-700 dark:hover:bg-gray-600'
234+
onClick={() => setPage(page - 1)}
235+
disabled={page === 0 || loading}
236+
>
237+
previous
238+
</button>
239+
<button
240+
className='btn btn-neutral text-gray-800 bg-gray-200 hover:bg-gray-300 dark:text-white dark:bg-gray-700 dark:hover:bg-gray-600'
241+
onClick={() => setPage(page + 1)}
242+
disabled={endIdx >= total || loading}
243+
>
244+
next
245+
</button>
246+
</div>
247+
</nav>
248+
);
213249
}
214250

215251
return (
@@ -254,29 +290,14 @@ export default function Overview() {
254290
<div className='py-6'>
255291
<label className="w-full form-control">
256292
<div className="label">
257-
<span className="label-text text-md text-gray-700 dark:text-white">Type a search, followed by the enter key</span>
293+
<span className="label-text text-md text-gray-700 dark:text-white">Type a search</span>
258294
</div>
259295
<input
260296
className="w-full text-sm input input-bordered text-gray-900 dark:text-white sm:text-base"
261297
type="text"
262298
placeholder="search by first name, last name, or email"
263-
onKeyDown={(event) => {
264-
if (event.key === 'Enter') {
265-
// instead of calling the backend directory, set
266-
// the page we are on to zero if the current page
267-
// we are on isn't the first page (value of 0).
268-
// by doing this, the useEffect will call the backend
269-
// for us with the correct page and query.
270-
if (page) {
271-
setPage(0);
272-
} else {
273-
callDatabase();
274-
}
275-
}
276-
}}
277-
onChange={event => {
278-
setQuery(event.target.value);
279-
}}
299+
value={query}
300+
onChange={event => setQuery(event.target.value)}
280301
/>
281302
</label>
282303
</div>
@@ -308,7 +329,7 @@ export default function Overview() {
308329
</tr>
309330
</thead>
310331
<tbody>
311-
{users.map((user) => (
332+
{filteredUsers.slice(page * 20, (page + 1) * 20).map((user) => (
312333
<tr className='break-all !rounded md:break-keep hover:bg-gray-100 dark:hover:bg-white/10' key={user.email}>
313334
<td className=''>
314335
<a className='link link-hover text-blue-600 dark:text-blue-400' target="_blank" rel="noopener noreferrer" href={`/user/edit/${user._id}`}>
@@ -349,7 +370,7 @@ export default function Overview() {
349370

350371
</tbody>
351372
</table>
352-
{users.length === 0 && (
373+
{filteredUsers.length === 0 && (
353374
<div className='flex flex-row w-100 justify-center'>
354375
<p className='text-lg text-gray-700 dark:text-white/70 mt-5 mb-5'>No results found!</p>
355376
</div>

0 commit comments

Comments
 (0)