Files
clinicpro/assets/admin/components/ui/SearchableSelect.tsx
T
hamed 06ab875693 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.
2026-06-10 13:02:47 +03:30

122 lines
3.6 KiB
TypeScript

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}
/>
);
}