Files
dmenu-admin/src/components/Filters.tsx
T
hamid zarghami 683c9ff8ad fix build
2025-11-17 09:16:15 +03:30

224 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type FC, useEffect, useState, type ChangeEvent, useRef } from 'react';
import { Filter } from 'iconsax-react';
import DatePicker from './DatePicker';
import Input from './Input';
import Select, { type ItemsSelectType } from './Select';
import DefaulModal from './DefaulModal';
import moment from 'moment-jalaali'
import Button from './Button';
export type DateFieldType = {
type: 'date';
name: string;
placeholder: string;
defaultValue?: string;
};
export type SelectFieldType = {
type: 'select';
name: string;
placeholder: string;
options: ItemsSelectType[];
defaultValue?: string;
};
export type InputFieldType = {
type: 'input';
name: string;
placeholder: string;
defaultValue?: string;
};
export type FieldType = DateFieldType | SelectFieldType | InputFieldType;
export type FilterValues = Record<string, string | null>;
interface FiltersProps {
fields: FieldType[];
onChange: (filters: FilterValues) => void;
initialValues?: FilterValues;
className?: string;
fieldClassName?: string;
searchField?: string;
}
const Filters: FC<FiltersProps> = ({
fields,
onChange,
initialValues = {},
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",
searchField = "search"
}) => {
const [filters, setFilters] = useState<FilterValues>({});
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
const [inputValues, setInputValues] = useState<Record<string, string>>({});
const onChangeRef = useRef(onChange);
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
useEffect(() => {
if (Object.keys(initialValues).length > 0) {
setFilters(initialValues);
const inputFields: Record<string, string> = {};
fields.forEach(field => {
if (field.type === 'input' && initialValues[field.name]) {
inputFields[field.name] = initialValues[field.name] as string;
}
});
setInputValues(inputFields);
} else {
const defaultValues: FilterValues = {};
const defaultInputs: Record<string, string> = {};
fields.forEach(field => {
if ('defaultValue' in field && field.defaultValue !== undefined) {
defaultValues[field.name] = field.defaultValue;
if (field.type === 'input') {
defaultInputs[field.name] = field.defaultValue;
}
}
});
if (Object.keys(defaultValues).length > 0) {
setFilters(defaultValues);
setInputValues(defaultInputs);
onChange(defaultValues);
}
}
}, [fields, initialValues, onChange]);
useEffect(() => {
const timeouts: Record<string, ReturnType<typeof setTimeout>> = {};
Object.keys(inputValues).forEach(fieldName => {
timeouts[fieldName] = setTimeout(() => {
setFilters(currentFilters => {
const currentFilter = currentFilters[fieldName];
const newValue = inputValues[fieldName];
if (currentFilter !== newValue) {
const newFilters = { ...currentFilters, [fieldName]: newValue || null };
onChangeRef.current(newFilters);
return newFilters;
}
return currentFilters;
});
}, 1000);
});
return () => {
Object.values(timeouts).forEach(timeout => clearTimeout(timeout));
};
}, [inputValues]);
const handleChange = (name: string, value: string | null) => {
const newFilters = { ...filters, [name]: value };
setFilters(newFilters);
onChange(newFilters);
};
const handleInputChange = (name: string, event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setInputValues(prev => ({ ...prev, [name]: value }));
};
const renderField = (field: FieldType) => {
const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name];
switch (field.type) {
case 'date':
return (
<DatePicker
key={field.name}
placeholder={field.placeholder}
onChange={(value) => handleChange(field.name, moment(value, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'))}
defaulValue={currentValue || field.defaultValue || ''}
/>
);
case 'select':
return (
<Select
key={field.name}
placeholder={field.placeholder}
onChange={(e) => handleChange(field.name, e.target.value)}
defaultValue={currentValue || field.defaultValue || ''}
items={field.options}
/>
);
case 'input':
return (
<Input
key={field.name}
placeholder={field.placeholder}
variant="search"
className="w-full md:min-w-[230px] md:max-w-[230px]"
value={currentValue || field.defaultValue || ''}
onChange={(e) => handleInputChange(field.name, e)}
/>
);
}
};
const searchFieldObj = fields.find(field => field.name === searchField);
const otherFields = fields.filter(field => field.name !== searchField);
return (
<div className={className}>
<div className="flex md:hidden items-center justify-between w-full mb-2">
<button
onClick={() => setIsFiltersOpen(!isFiltersOpen)}
className='flex items-center gap-2 px-3 py-2 rounded-lg transition-colors bg-card text-card-foreground border border-border hover:bg-secondary hover:bg-opacity-80'
>
<Filter
size={18}
color='#000000'
/>
<span className="text-sm">فیلترها</span>
</button>
{searchFieldObj && (
<div className="flex-1 mr-4">
{renderField(searchFieldObj)}
</div>
)}
</div>
<div className="hidden md:flex md:flex-row justify-between items-center gap-4 w-full">
<div className={fieldClassName}>
{fields.map(renderField)}
</div>
</div>
<DefaulModal
open={isFiltersOpen}
close={() => setIsFiltersOpen(false)}
isHeader={true}
title_header="فیلتر کردن نتایج"
>
<div className="pb-5 gap-4 flex flex-col-reverse mt-3">
{otherFields.map(field => (
<div key={field.name} className="w-full">
<label className="block text-sm font-medium text-foreground mb-2">
{field.placeholder}
</label>
<div className="w-full">
{renderField(field)}
</div>
</div>
))}
</div>
<div className='mt-6'>
<Button
label='بستن'
onClick={() => setIsFiltersOpen(false)}
/>
</div>
</DefaulModal>
</div>
);
};
export default Filters;