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:
@ -40,10 +40,13 @@ export default function AdminCustomers() {
|
||||
const [pageSize, setPageSize] = useState(initialPageSize);
|
||||
|
||||
// Search filters state
|
||||
const [nameFilter, setNameFilter] = useState('');
|
||||
const [emailFilter, setEmailFilter] = useState('');
|
||||
const [urlFilter, setUrlFilter] = useState('');
|
||||
const [hasEmailFilter, setHasEmailFilter] = useState(false);
|
||||
const [filters, setFilters] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
url: '',
|
||||
hasEmail: false
|
||||
});
|
||||
const [debouncedFilters, setDebouncedFilters] = useState(filters);
|
||||
|
||||
// State for data
|
||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||
@ -68,10 +71,10 @@ export default function AdminCustomers() {
|
||||
params.append('page', initialPage.toString());
|
||||
params.append('pageSize', initialPageSize.toString());
|
||||
|
||||
if (nameFilter) params.append('name', nameFilter);
|
||||
if (emailFilter) params.append('email', emailFilter);
|
||||
if (urlFilter) params.append('url', urlFilter);
|
||||
if (hasEmailFilter) params.append('hasEmail', 'true');
|
||||
if (debouncedFilters.name) params.append('name', debouncedFilters.name);
|
||||
if (debouncedFilters.email) params.append('email', debouncedFilters.email);
|
||||
if (debouncedFilters.url) params.append('url', debouncedFilters.url);
|
||||
if (debouncedFilters.hasEmail) params.append('hasEmail', 'true');
|
||||
|
||||
const response = await fetch(`/api/customers?${params.toString()}`);
|
||||
|
||||
@ -91,7 +94,31 @@ export default function AdminCustomers() {
|
||||
};
|
||||
|
||||
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
|
||||
const handlePageChange = (newPage: number) => {
|
||||
@ -125,63 +152,120 @@ export default function AdminCustomers() {
|
||||
</div>
|
||||
|
||||
{/* Search Filters */}
|
||||
<div className="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<label htmlFor="name-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Search by Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name-filter"
|
||||
value={nameFilter}
|
||||
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 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>
|
||||
<label htmlFor="email-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Search by Email
|
||||
</label>
|
||||
<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
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<label htmlFor="name-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Search by Name
|
||||
</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>
|
||||
{(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 */}
|
||||
|
||||
Reference in New Issue
Block a user