feat: implement UsersContext and refactor user management hooks for improved user data handling

This commit is contained in:
2026-01-25 12:17:30 -06:00
parent f2dd053320
commit 4548d959d7
10 changed files with 576 additions and 120 deletions

View File

@@ -2,12 +2,23 @@ import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';
import api from '../utils/api';
const DEFAULT_SEARCH_FIELDS = ['first_name', 'last_name', 'email'];
/**
* Hook for fetching users from a custom endpoint (e.g., member-facing directory).
* For admin pages, use hooks from use-users.js instead which share a centralized context.
*/
const useMembers = ({
endpoint = '/admin/users',
initialFilter = 'active',
initialSearch = '',
filterKey = 'status',
allowedRoles = ['member'],
searchFields = DEFAULT_SEARCH_FIELDS,
fetchErrorMessage = 'Failed to fetch members',
searchAccessor,
transform,
onFetchError,
} = {}) => {
const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]);
@@ -17,18 +28,25 @@ const useMembers = ({
const fetchMembers = useCallback(async () => {
try {
const response = await api.get('/admin/users');
const response = await api.get(endpoint);
let filtered = response.data;
if (typeof transform === 'function') {
filtered = transform(filtered);
}
if (allowedRoles && allowedRoles.length) {
filtered = filtered.filter(user => allowedRoles.includes(user.role));
}
setUsers(filtered);
} catch (error) {
toast.error(fetchErrorMessage);
if (typeof onFetchError === 'function') {
onFetchError(error);
} else {
toast.error(fetchErrorMessage);
}
} finally {
setLoading(false);
}
}, [allowedRoles, fetchErrorMessage]);
}, [allowedRoles, endpoint, fetchErrorMessage, onFetchError, transform]);
useEffect(() => {
fetchMembers();
@@ -43,15 +61,19 @@ const useMembers = ({
if (searchQuery) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(user =>
user.first_name.toLowerCase().includes(query) ||
user.last_name.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query)
);
filtered = filtered.filter(user => {
const values = typeof searchAccessor === 'function'
? searchAccessor(user)
: searchFields.map(field => user?.[field]);
return values
.filter(Boolean)
.some(value => value.toString().toLowerCase().includes(query));
});
}
setFilteredUsers(filtered);
}, [users, searchQuery, filterKey, filterValue]);
}, [users, searchQuery, filterKey, filterValue, searchAccessor, searchFields]);
return {
users,

171
src/hooks/use-users.js Normal file
View File

@@ -0,0 +1,171 @@
import { useState, useMemo } from 'react';
import { useUsers } from '../context/UsersContext';
const DEFAULT_SEARCH_FIELDS = ['first_name', 'last_name', 'email'];
/**
* Base hook that adds search and filter functionality to any user list
*/
const useFilteredUsers = ({
users,
initialFilter = 'all',
filterKey = 'status',
searchFields = DEFAULT_SEARCH_FIELDS,
searchAccessor,
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [filterValue, setFilterValue] = useState(initialFilter);
const filteredUsers = useMemo(() => {
let filtered = users;
// Apply filter
if (filterValue && filterValue !== 'all') {
filtered = filtered.filter(user => user[filterKey] === filterValue);
}
// Apply search
if (searchQuery) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(user => {
const values = typeof searchAccessor === 'function'
? searchAccessor(user)
: searchFields.map(field => user?.[field]);
return values
.filter(Boolean)
.some(value => value.toString().toLowerCase().includes(query));
});
}
return filtered;
}, [users, searchQuery, filterKey, filterValue, searchAccessor, searchFields]);
return {
filteredUsers,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
};
};
/**
* Hook for staff users (admin, superadmin, finance roles)
*/
export const useStaff = ({
initialFilter = 'all',
filterKey = 'role',
searchFields = DEFAULT_SEARCH_FIELDS,
searchAccessor,
} = {}) => {
const { staff, loading, error, refetch, updateUser, removeUser } = useUsers();
const {
filteredUsers,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
} = useFilteredUsers({
users: staff,
initialFilter,
filterKey,
searchFields,
searchAccessor,
});
return {
users: staff,
filteredUsers,
loading,
error,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
refetch,
updateUser,
removeUser,
};
};
/**
* Hook for member users (non-admin roles)
*/
export const useMembers = ({
initialFilter = 'active',
filterKey = 'status',
searchFields = DEFAULT_SEARCH_FIELDS,
searchAccessor,
} = {}) => {
const { members, loading, error, refetch, updateUser, removeUser } = useUsers();
const {
filteredUsers,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
} = useFilteredUsers({
users: members,
initialFilter,
filterKey,
searchFields,
searchAccessor,
});
return {
users: members,
filteredUsers,
loading,
error,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
refetch,
updateUser,
removeUser,
};
};
/**
* Hook for all users (both staff and members)
*/
export const useAllUsers = ({
initialFilter = 'all',
filterKey = 'status',
searchFields = DEFAULT_SEARCH_FIELDS,
searchAccessor,
} = {}) => {
const { users, loading, error, refetch, updateUser, removeUser } = useUsers();
const {
filteredUsers,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
} = useFilteredUsers({
users,
initialFilter,
filterKey,
searchFields,
searchAccessor,
});
return {
users,
filteredUsers,
loading,
error,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
refetch,
updateUser,
removeUser,
};
};