persistent filters
This commit is contained in:
@@ -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)}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const FoodsList: FC = () => {
|
|||||||
onChange={handleFiltersChange}
|
onChange={handleFiltersChange}
|
||||||
initialValues={filters}
|
initialValues={filters}
|
||||||
searchField="search"
|
searchField="search"
|
||||||
|
showClearButton
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user