'use client' import React, { useState } from 'react' import Select from 'react-select' import Tooltip from './Tooltip' import { useClientFetch } from '@/app/hooks/useClientFetch' export interface Option { id: number value: string label: string } export interface DataItem { id: number [key: string]: unknown } export interface PaginatedResponse { results: T[] count: number next: string | null previous: string | null } export interface SelectorProps { name: string value?: Option | null handleChange: (e: { target: { id: string value: Option | null selectedOption?: Option } }) => void label?: string tooltip?: string | React.ReactNode placeholder?: string endpoint?: string mapDataToOptions: (data: T) => Option searchParam?: string staticOptions?: T[] config?: { params?: Record queryOptions?: { enabled?: boolean } } } const Selector = ({ name, value, handleChange, label, tooltip, placeholder, endpoint, mapDataToOptions, searchParam = 'search', staticOptions, config = {}, }: SelectorProps) => { const [search, setSearch] = useState('') const { data, isLoading, error } = useClientFetch>( endpoint || '/404-no-endpoint', { config: { params: { ...(search ? { [searchParam]: search } : {}), ...(config.params || {}), }, }, queryOptions: { staleTime: 60 * 60 * 1000, // кешируем на час refetchOnWindowFocus: false, enabled: !staticOptions && endpoint !== undefined && config.queryOptions?.enabled !== false, // отключаем запрос если используем статические опции, нет endpoint или явно выключен в конфиге }, } ) let options: Option[] = [] if (staticOptions) { // если есть статические опции используем их options = staticOptions.map(mapDataToOptions) } else { // иначе используем данные с бекенда const dataArray = Array.isArray(data) ? data : data?.results || [] options = dataArray.map(mapDataToOptions) } if (error) { console.error(`Error fetching data from ${endpoint}:`, error) } return (
{label && (
{tooltip && }
)} inputId={name} name={name} options={options} value={options.find(opt => opt.id === value?.id)} onChange={selectedOption => { handleChange({ target: { id: name, value: selectedOption ? { ...selectedOption, } : null, selectedOption: selectedOption || undefined, }, }) }} isLoading={isLoading} onInputChange={newValue => setSearch(newValue)} isSearchable isClearable placeholder={placeholder} noOptionsMessage={() => (isLoading ? 'Загрузка...' : 'Нет доступных вариантов')} classNamePrefix="select" className="rounded-lg" styles={{ control: base => ({ ...base, borderRadius: '0.75rem', backgroundColor: '#1B1E28', border: '1px solid #E5E7EB', padding: '2px', color: 'white', '&:hover': { borderColor: '#E5E7EB', }, '&:focus-within': { backgroundColor: '#1B1E28', borderColor: '#E5E7EB', boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.5)', }, }), singleValue: base => ({ ...base, color: 'white', }), input: base => ({ ...base, color: 'white', }), menu: base => ({ ...base, position: 'absolute', width: '100%', zIndex: 9999, marginTop: '4px', borderRadius: '0.75rem', overflow: 'hidden', backgroundColor: '#1B1E28', border: '1px solid #E5E7EB', }), option: (base, state) => ({ ...base, fontSize: '0.875rem', padding: '8px 12px', backgroundColor: state.isSelected ? '#2563EB' : state.isFocused ? '#2D3139' : '#1B1E28', color: 'white', cursor: 'pointer', '&:active': { backgroundColor: '#2D3139', }, '&:hover': { backgroundColor: state.isSelected ? '#2563EB' : '#2D3139', }, }), }} />
) } export default Selector