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,63 +152,120 @@ 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> <div className="flex justify-between items-center mb-4">
<label htmlFor="name-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Filters</h2>
Search by Name <button
</label> onClick={handleClearFilters}
<input className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
type="text" >
id="name-filter" Clear all filters
value={nameFilter} </button>
onChange={(e) => setNameFilter(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"
placeholder="Enter name..."
/>
</div> </div>
<div> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
<label htmlFor="email-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <div>
Search by Email <label htmlFor="name-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
</label> Search by Name
<input
type="text"
id="email-filter"
value={emailFilter}
onChange={(e) => setEmailFilter(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"
placeholder="Enter email..."
/>
</div>
<div>
<label htmlFor="url-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Search by URL
</label>
<input
type="text"
id="url-filter"
value={urlFilter}
onChange={(e) => setUrlFilter(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"
placeholder="Enter URL..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Email Filter
</label>
<div className="flex items-center">
<input
type="checkbox"
id="has-email"
checked={hasEmailFilter}
onChange={(e) => setHasEmailFilter(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"
/>
<label htmlFor="has-email" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
Has Email
</label> </label>
<div className="mt-1 relative rounded-md shadow-sm">
<input
type="text"
id="name-filter"
value={filters.name}
onChange={(e) => handleFilterChange('name', e.target.value)}
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..."
/>
{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>
<label htmlFor="email-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Search by Email
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input
type="text"
id="email-filter"
value={filters.email}
onChange={(e) => handleFilterChange('email', e.target.value)}
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..."
/>
{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>
<label htmlFor="url-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Search by URL
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input
type="text"
id="url-filter"
value={filters.url}
onChange={(e) => handleFilterChange('url', e.target.value)}
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..."
/>
{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>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Email Filter
</label>
<div className="flex items-center">
<input
type="checkbox"
id="has-email"
checked={filters.hasEmail}
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"
/>
<label htmlFor="has-email" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
Has Email
</label>
</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> </div>
{/* Error Message */} {/* Error Message */}