feat: enhance RepresentationsPage with searchable city filter and form improvements
- Updated RepresentationsPage to use SearchableSelect for city filtering. - Modified form handling to use Controller from react-hook-form for city selection. - Improved city filter handling to support null values. - Added city_id to Representation interface in types. - Implemented uploadLogo endpoint in CategoryController for logo uploads. - Added logo_url field to Category entity and updated related services. - Created SearchableSelect component for better user experience in selecting options. - Added migration to include logo_url in categories table.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import Select, { StylesConfig, GroupBase } from 'react-select';
|
||||
import { useUiStore } from '../../stores/uiStore';
|
||||
|
||||
export interface SelectOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
options: SelectOption[];
|
||||
value?: string | number | null;
|
||||
onChange?: (value: string | number | null) => void;
|
||||
placeholder?: string;
|
||||
isLoading?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isClearable?: boolean;
|
||||
noOptionsMessage?: string;
|
||||
inputId?: string;
|
||||
}
|
||||
|
||||
export default function SearchableSelect({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'انتخاب کنید...',
|
||||
isLoading,
|
||||
isDisabled,
|
||||
isClearable,
|
||||
noOptionsMessage = 'موردی یافت نشد',
|
||||
inputId,
|
||||
}: Props) {
|
||||
const darkMode = useUiStore((s) => s.darkMode);
|
||||
|
||||
const selected = useMemo(
|
||||
() => options.find((o) => o.value === value) ?? null,
|
||||
[options, value],
|
||||
);
|
||||
|
||||
const styles: StylesConfig<SelectOption, false, GroupBase<SelectOption>> = {
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
background: darkMode ? '#111827' : '#ffffff',
|
||||
borderColor: state.isFocused ? '#7c3aed' : darkMode ? '#4b5563' : '#cbd5e1',
|
||||
boxShadow: state.isFocused ? '0 0 0 2px rgba(124,58,237,0.25)' : 'none',
|
||||
borderRadius: '0.75rem',
|
||||
minHeight: '2.75rem',
|
||||
fontSize: '0.875rem',
|
||||
'&:hover': {
|
||||
borderColor: state.isFocused ? '#7c3aed' : darkMode ? '#6b7280' : '#94a3b8',
|
||||
},
|
||||
}),
|
||||
menu: (base) => ({
|
||||
...base,
|
||||
background: darkMode ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${darkMode ? '#374151' : '#e2e8f0'}`,
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
|
||||
borderRadius: '0.75rem',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
background: state.isSelected
|
||||
? '#7c3aed'
|
||||
: state.isFocused
|
||||
? darkMode ? '#374151' : '#f1f5f9'
|
||||
: 'transparent',
|
||||
color: state.isSelected ? '#ffffff' : darkMode ? '#e5e7eb' : '#1e293b',
|
||||
fontSize: '0.875rem',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
singleValue: (base) => ({ ...base, color: darkMode ? '#f3f4f6' : '#1e293b' }),
|
||||
input: (base) => ({ ...base, color: darkMode ? '#f3f4f6' : '#1e293b' }),
|
||||
placeholder: (base) => ({ ...base, color: darkMode ? '#6b7280' : '#94a3b8' }),
|
||||
indicatorSeparator: (base) => ({
|
||||
...base,
|
||||
background: darkMode ? '#374151' : '#e2e8f0',
|
||||
}),
|
||||
dropdownIndicator: (base) => ({
|
||||
...base,
|
||||
color: darkMode ? '#6b7280' : '#94a3b8',
|
||||
'&:hover': { color: darkMode ? '#9ca3af' : '#64748b' },
|
||||
}),
|
||||
clearIndicator: (base) => ({
|
||||
...base,
|
||||
color: darkMode ? '#6b7280' : '#94a3b8',
|
||||
'&:hover': { color: '#ef4444' },
|
||||
}),
|
||||
loadingIndicator: (base) => ({ ...base, color: '#7c3aed' }),
|
||||
noOptionsMessage: (base) => ({
|
||||
...base,
|
||||
color: darkMode ? '#6b7280' : '#94a3b8',
|
||||
fontSize: '0.875rem',
|
||||
}),
|
||||
loadingMessage: (base) => ({
|
||||
...base,
|
||||
color: darkMode ? '#6b7280' : '#94a3b8',
|
||||
fontSize: '0.875rem',
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<Select<SelectOption>
|
||||
options={options}
|
||||
value={selected}
|
||||
onChange={(opt) => onChange?.(opt ? opt.value : null)}
|
||||
placeholder={placeholder}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isDisabled}
|
||||
isClearable={isClearable}
|
||||
styles={styles}
|
||||
isRtl
|
||||
menuPortalTarget={typeof document !== 'undefined' ? document.body : undefined}
|
||||
menuPosition="fixed"
|
||||
noOptionsMessage={() => noOptionsMessage}
|
||||
loadingMessage={() => 'در حال بارگذاری...'}
|
||||
inputId={inputId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user