Improve customer list filtering UI

- Add dedicated filter section with improved styling
- Add debouncing to filter inputs
- Add clear filters functionality
- Add individual clear buttons for text inputs
- Add active filters display
- Improve overall filter section layout
This commit is contained in:
Ken Yasue
2025-03-25 13:59:44 +01:00
parent 0e712f2e2f
commit 45164faa9d

View File

@ -40,10 +40,13 @@ export default function AdminCustomers() {
const [pageSize, setPageSize] = useState(initialPageSize); const [pageSize, setPageSize] = useState(initialPageSize);
// Search filters state // Search filters state
const [nameFilter, setNameFilter] = useState(''); const [filters, setFilters] = useState({
const [emailFilter, setEmailFilter] = useState(''); name: '',
const [urlFilter, setUrlFilter] = useState(''); email: '',
const [hasEmailFilter, setHasEmailFilter] = useState(false); url: '',
hasEmail: false
});
const [debouncedFilters, setDebouncedFilters] = useState(filters);
// State for data // State for data
const [customers, setCustomers] = useState<Customer[]>([]); const [customers, setCustomers] = useState<Customer[]>([]);
@ -68,10 +71,10 @@ export default function AdminCustomers() {
params.append('page', initialPage.toString()); params.append('page', initialPage.toString());
params.append('pageSize', initialPageSize.toString()); params.append('pageSize', initialPageSize.toString());
if (nameFilter) params.append('name', nameFilter); if (debouncedFilters.name) params.append('name', debouncedFilters.name);
if (emailFilter) params.append('email', emailFilter); if (debouncedFilters.email) params.append('email', debouncedFilters.email);
if (urlFilter) params.append('url', urlFilter); if (debouncedFilters.url) params.append('url', debouncedFilters.url);
if (hasEmailFilter) params.append('hasEmail', 'true'); if (debouncedFilters.hasEmail) params.append('hasEmail', 'true');
const response = await fetch(`/api/customers?${params.toString()}`); const response = await fetch(`/api/customers?${params.toString()}`);
@ -91,7 +94,31 @@ export default function AdminCustomers() {
}; };
fetchCustomers(); fetchCustomers();
}, [initialPage, initialPageSize, nameFilter, emailFilter, urlFilter, hasEmailFilter]); }, [initialPage, initialPageSize, debouncedFilters]);
// Debounce filter changes
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedFilters(filters);
}, 300);
return () => clearTimeout(timer);
}, [filters]);
// Handle filter changes
const handleFilterChange = (key: keyof typeof filters, value: string | boolean) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
// Clear all filters
const handleClearFilters = () => {
setFilters({
name: '',
email: '',
url: '',
hasEmail: false
});
};
// Handle page change // Handle page change
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
@ -125,45 +152,91 @@ export default function AdminCustomers() {
</div> </div>
{/* Search Filters */} {/* Search Filters */}
<div className="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div className="mb-6 bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Filters</h2>
<button
onClick={handleClearFilters}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
Clear all filters
</button>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
<div> <div>
<label htmlFor="name-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label htmlFor="name-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Search by Name Search by Name
</label> </label>
<div className="mt-1 relative rounded-md shadow-sm">
<input <input
type="text" type="text"
id="name-filter" id="name-filter"
value={nameFilter} value={filters.name}
onChange={(e) => setNameFilter(e.target.value)} onChange={(e) => handleFilterChange('name', e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm" className="block w-full rounded-md border-gray-300 pr-10 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
placeholder="Enter name..." placeholder="Enter name..."
/> />
{filters.name && (
<button
onClick={() => handleFilterChange('name', '')}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg className="h-4 w-4 text-gray-400 hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
</div> </div>
<div> <div>
<label htmlFor="email-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label htmlFor="email-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Search by Email Search by Email
</label> </label>
<div className="mt-1 relative rounded-md shadow-sm">
<input <input
type="text" type="text"
id="email-filter" id="email-filter"
value={emailFilter} value={filters.email}
onChange={(e) => setEmailFilter(e.target.value)} onChange={(e) => handleFilterChange('email', e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm" className="block w-full rounded-md border-gray-300 pr-10 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
placeholder="Enter email..." placeholder="Enter email..."
/> />
{filters.email && (
<button
onClick={() => handleFilterChange('email', '')}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg className="h-4 w-4 text-gray-400 hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
</div> </div>
<div> <div>
<label htmlFor="url-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label htmlFor="url-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Search by URL Search by URL
</label> </label>
<div className="mt-1 relative rounded-md shadow-sm">
<input <input
type="text" type="text"
id="url-filter" id="url-filter"
value={urlFilter} value={filters.url}
onChange={(e) => setUrlFilter(e.target.value)} onChange={(e) => handleFilterChange('url', e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm" className="block w-full rounded-md border-gray-300 pr-10 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
placeholder="Enter URL..." placeholder="Enter URL..."
/> />
{filters.url && (
<button
onClick={() => handleFilterChange('url', '')}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg className="h-4 w-4 text-gray-400 hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
@ -173,8 +246,8 @@ export default function AdminCustomers() {
<input <input
type="checkbox" type="checkbox"
id="has-email" id="has-email"
checked={hasEmailFilter} checked={filters.hasEmail}
onChange={(e) => setHasEmailFilter(e.target.checked)} onChange={(e) => handleFilterChange('hasEmail', e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600" className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600"
/> />
<label htmlFor="has-email" className="ml-2 block text-sm text-gray-700 dark:text-gray-300"> <label htmlFor="has-email" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
@ -183,6 +256,17 @@ export default function AdminCustomers() {
</div> </div>
</div> </div>
</div> </div>
{(filters.name || filters.email || filters.url || filters.hasEmail) && (
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400">
Active filters: {[
filters.name && 'Name',
filters.email && 'Email',
filters.url && 'URL',
filters.hasEmail && 'Has Email'
].filter(Boolean).join(', ')}
</div>
)}
</div>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (