persistent filters

This commit is contained in:
2026-06-18 18:46:51 +03:30
parent 59f892cb10
commit c588b6f6b4
3 changed files with 130 additions and 13 deletions
+53 -3
View File
@@ -49,6 +49,7 @@ interface FiltersProps {
className?: string; className?: string;
fieldClassName?: string; fieldClassName?: string;
searchField?: string; searchField?: string;
showClearButton?: boolean;
} }
const Filters: FC<FiltersProps> = ({ const Filters: FC<FiltersProps> = ({
@@ -57,7 +58,8 @@ const Filters: FC<FiltersProps> = ({
initialValues = {}, initialValues = {},
className = "flex flex-col md:flex-row justify-between items-start md:items-center gap-4", className = "flex flex-col md:flex-row justify-between items-start md:items-center gap-4",
fieldClassName = "flex flex-col sm:flex-row gap-3 md:gap-4 w-full md:w-auto", fieldClassName = "flex flex-col sm:flex-row gap-3 md:gap-4 w-full md:w-auto",
searchField = "search" searchField = "search",
showClearButton = false,
}) => { }) => {
const [filters, setFilters] = useState<FilterValues>({}); const [filters, setFilters] = useState<FilterValues>({});
const [isFiltersOpen, setIsFiltersOpen] = useState(false); const [isFiltersOpen, setIsFiltersOpen] = useState(false);
@@ -155,6 +157,42 @@ const Filters: FC<FiltersProps> = ({
setInputValues(prev => ({ ...prev, [name]: value })); setInputValues(prev => ({ ...prev, [name]: value }));
}; };
const hasActiveFilters = fields.some((field) => {
const value = field.type === 'input'
? inputValues[field.name]
: field.type === 'multiselect'
? multiSelectValues[field.name]
: filters[field.name];
if (Array.isArray(value)) {
return value.length > 0;
}
return Boolean(value);
});
const handleClear = () => {
setFilters({});
setInputValues({});
setMultiSelectValues({});
onChange({});
};
const renderClearButton = () => {
if (!showClearButton) {
return null;
}
return (
<Button
label="پاک کردن فیلترها"
onClick={handleClear}
disabled={!hasActiveFilters}
className="w-fit min-w-[140px] px-4 bg-transparent text-foreground border border-border hover:bg-secondary disabled:opacity-50"
/>
);
};
const renderField = (field: FieldType) => { const renderField = (field: FieldType) => {
const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name]; const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name];
const currentMultiSelectValue = field.type === 'multiselect' const currentMultiSelectValue = field.type === 'multiselect'
@@ -179,7 +217,7 @@ const Filters: FC<FiltersProps> = ({
key={field.name} key={field.name}
placeholder={field.placeholder} placeholder={field.placeholder}
onChange={(e) => handleChange(field.name, e.target.value)} onChange={(e) => handleChange(field.name, e.target.value)}
defaultValue={currentValue as string || field.defaultValue || ''} value={(currentValue as string) || field.defaultValue || ''}
items={field.options} items={field.options}
className='min-w-[170px]' className='min-w-[170px]'
/> />
@@ -237,6 +275,7 @@ const Filters: FC<FiltersProps> = ({
<div className="hidden md:flex md:flex-row justify-between items-center gap-4 w-full"> <div className="hidden md:flex md:flex-row justify-between items-center gap-4 w-full">
<div className={fieldClassName}> <div className={fieldClassName}>
{fields.map(renderField)} {fields.map(renderField)}
{renderClearButton()}
</div> </div>
</div> </div>
@@ -258,7 +297,18 @@ const Filters: FC<FiltersProps> = ({
</div> </div>
))} ))}
</div> </div>
<div className='mt-6'> <div className='mt-6 flex flex-col gap-3'>
{showClearButton && (
<Button
label="پاک کردن فیلترها"
onClick={() => {
handleClear();
setIsFiltersOpen(false);
}}
disabled={!hasActiveFilters}
className="w-full bg-transparent text-foreground border border-border hover:bg-secondary disabled:opacity-50"
/>
)}
<Button <Button
label='بستن' label='بستن'
onClick={() => setIsFiltersOpen(false)} onClick={() => setIsFiltersOpen(false)}
+1
View File
@@ -55,6 +55,7 @@ const FoodsList: FC = () => {
onChange={handleFiltersChange} onChange={handleFiltersChange}
initialValues={filters} initialValues={filters}
searchField="search" searchField="search"
showClearButton
/> />
</div> </div>
+76 -10
View File
@@ -1,15 +1,69 @@
import { useState, useMemo } from "react"; import { useState, useMemo, useCallback, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import type { FilterValues } from "@/components/Filters"; import type { FilterValues } from "@/components/Filters";
import type { GetFoodsParams } from "../types/Types"; import type { GetFoodsParams } from "../types/Types";
const DEFAULT_PAGE = 1; const DEFAULT_PAGE = 1;
const DEFAULT_LIMIT = 10; const DEFAULT_LIMIT = 10;
const getFiltersFromParams = (searchParams: URLSearchParams): FilterValues => {
const filters: FilterValues = {};
const search = searchParams.get("search");
const categoryId = searchParams.get("categoryId");
if (search) filters.search = search;
if (categoryId) filters.categoryId = categoryId;
return filters;
};
const getPageFromParams = (searchParams: URLSearchParams): number => {
const page = searchParams.get("page");
const parsed = page ? parseInt(page, 10) : DEFAULT_PAGE;
return Number.isNaN(parsed) || parsed < 1 ? DEFAULT_PAGE : parsed;
};
const buildSearchParams = (filters: FilterValues, page: number): URLSearchParams => {
const params = new URLSearchParams();
if (page > DEFAULT_PAGE) {
params.set("page", String(page));
}
if (filters.search) {
params.set("search", filters.search as string);
}
if (filters.categoryId) {
params.set("categoryId", filters.categoryId as string);
}
return params;
};
export const useFoodFilters = () => { export const useFoodFilters = () => {
const [filters, setFilters] = useState<FilterValues>({}); const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
const [filters, setFilters] = useState<FilterValues>(() =>
getFiltersFromParams(searchParams),
);
const [currentPage, setCurrentPage] = useState(() =>
getPageFromParams(searchParams),
);
const [limit] = useState(DEFAULT_LIMIT); const [limit] = useState(DEFAULT_LIMIT);
useEffect(() => {
setFilters(getFiltersFromParams(searchParams));
setCurrentPage(getPageFromParams(searchParams));
}, [searchParams]);
const updateUrlParams = useCallback(
(newFilters: FilterValues, page: number) => {
setSearchParams(buildSearchParams(newFilters, page), { replace: true });
},
[setSearchParams],
);
const apiParams = useMemo<GetFoodsParams>(() => { const apiParams = useMemo<GetFoodsParams>(() => {
const params: GetFoodsParams = { const params: GetFoodsParams = {
page: currentPage, page: currentPage,
@@ -20,17 +74,29 @@ export const useFoodFilters = () => {
params.search = filters.search as string; params.search = filters.search as string;
} }
if (filters.categoryId) {
params.categoryId = filters.categoryId as string;
}
return params; return params;
}, [filters, currentPage, limit]); }, [filters, currentPage, limit]);
const handleFiltersChange = (newFilters: FilterValues) => { const handleFiltersChange = useCallback(
setFilters(newFilters); (newFilters: FilterValues) => {
setCurrentPage(DEFAULT_PAGE); setFilters(newFilters);
}; setCurrentPage(DEFAULT_PAGE);
updateUrlParams(newFilters, DEFAULT_PAGE);
},
[updateUrlParams],
);
const handlePageChange = (page: number) => { const handlePageChange = useCallback(
setCurrentPage(page); (page: number) => {
}; setCurrentPage(page);
updateUrlParams(filters, page);
},
[filters, updateUrlParams],
);
return { return {
filters, filters,