Files
aerbim-ht-monitor/frontend/components/ui/Selector.tsx
Timofey Syrokvashko 65a63235e5 feat / AEB-26 login page
2025-09-01 11:34:30 +03:00

191 lines
5.0 KiB
TypeScript
Raw Permalink 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.
'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<T> {
results: T[]
count: number
next: string | null
previous: string | null
}
export interface SelectorProps<T extends DataItem> {
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<string, string | number | boolean | undefined>
queryOptions?: {
enabled?: boolean
}
}
}
const Selector = <T extends DataItem>({
name,
value,
handleChange,
label,
tooltip,
placeholder,
endpoint,
mapDataToOptions,
searchParam = 'search',
staticOptions,
config = {},
}: SelectorProps<T>) => {
const [search, setSearch] = useState('')
const { data, isLoading, error } = useClientFetch<PaginatedResponse<T>>(
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 (
<div>
{label && (
<div className="my-2 flex items-center gap-2">
<label className="text-sm font-medium text-gray-500" htmlFor={name}>
{label}
</label>
{tooltip && <Tooltip content={tooltip} />}
</div>
)}
<Select<Option>
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',
},
}),
}}
/>
</div>
)
}
export default Selector