food admin
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
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, number> = {};
|
||||
|
||||
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;
|
||||
@@ -34,9 +34,9 @@ const Input: FC<Props> = (props: Props) => {
|
||||
const [search, setSearch] = useState<string>('')
|
||||
|
||||
const inputClass = clx(
|
||||
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||
'w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
||||
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
|
||||
props.variant === 'search' && 'border-0 ps-10',
|
||||
props.className
|
||||
);
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
|
||||
return createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="fixed py-3 md:py-3 bg-white dark:bg-[#252526] rounded-xl md:rounded-2xl shadow-lg z-[5] min-w-[140px] md:min-w-[150px] border border-[#d0d0d0]"
|
||||
className="fixed py-2 bg-white rounded-lg shadow-xl z-[5] min-w-[140px] md:min-w-[150px] border border-gray-200"
|
||||
style={{
|
||||
top: `${dropdownPosition.top}px`,
|
||||
left: `${dropdownPosition.left}px`,
|
||||
@@ -112,10 +112,10 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleActionClick(action)}
|
||||
className={`w-full mt-1 text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 dark:hover:bg-[#2d2d30] ${index === 0 ? 'rounded-t-lg' : ''} ${action.className || ''}`}
|
||||
className={`w-full text-right px-3 py-2.5 text-sm flex items-center gap-2 hover:bg-gray-50 transition-colors ${index === 0 ? 'rounded-t-lg' : ''} ${index === actions.length - 1 ? 'rounded-b-lg' : ''} ${action.className || ''}`}
|
||||
>
|
||||
{action.icon && <span className="ml-2">{action.icon}</span>}
|
||||
<span className={`${action.label === 'حذف' ? 'text-red-500' : ''} dark:text-gray-200`}>{action.label}</span>
|
||||
{action.icon && <span className="ml-2 flex-shrink-0">{action.icon}</span>}
|
||||
<span className={`flex-1 ${action.label === 'حذف' ? 'text-red-500' : 'text-gray-700'}`}>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
@@ -128,12 +128,12 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={handleToggle}
|
||||
className="mt-2 hover:bg-gray-100 dark:hover:bg-[#2d2d30] rounded-full transition-colors"
|
||||
className={`p-1.5 hover:bg-gray-100 rounded-lg transition-colors ${isOpen ? 'bg-gray-100' : ''}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{trigger ? trigger : (
|
||||
<More size={16} color={'black'} className="rotate-90" />
|
||||
<More size={18} color="#6B7280" className="rotate-90" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user