sender page
This commit is contained in:
134
frontend/components/ui/LocationSelect.tsx
Normal file
134
frontend/components/ui/LocationSelect.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Select from 'react-select'
|
||||
import { SelectOption } from '@/app/types'
|
||||
import axios from 'axios'
|
||||
|
||||
interface LocationOption extends SelectOption {
|
||||
value: string // добавляем поле value к базовому интерфейсу SelectOption
|
||||
}
|
||||
|
||||
interface LocationSelectProps {
|
||||
name: string
|
||||
value: string
|
||||
handleChange: (e: {
|
||||
target: { id: string; value: string; selectedOption?: LocationOption }
|
||||
}) => void
|
||||
label: string
|
||||
placeholder: string
|
||||
countryId?: string
|
||||
isCity?: boolean
|
||||
}
|
||||
|
||||
const LocationSelect: React.FC<LocationSelectProps> = ({
|
||||
name,
|
||||
value,
|
||||
handleChange,
|
||||
label,
|
||||
placeholder,
|
||||
countryId,
|
||||
isCity = false,
|
||||
}) => {
|
||||
const [options, setOptions] = useState<LocationOption[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||
useEffect(() => {
|
||||
const fetchOptions = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const url = isCity
|
||||
? `${API_URL}/cities/?${countryId ? `country_id=${countryId}&` : ''}search=${search}`
|
||||
: `${API_URL}/countries/?search=${search}`
|
||||
|
||||
const response = await axios.get(url)
|
||||
setOptions(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching options:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce search
|
||||
const timeoutId = setTimeout(() => {
|
||||
fetchOptions()
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [search, countryId, isCity, API_URL])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={name} className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
<Select<LocationOption>
|
||||
inputId={name}
|
||||
name={name}
|
||||
options={options}
|
||||
value={options.find(opt => opt.value === value)}
|
||||
onChange={selectedOption => {
|
||||
handleChange({
|
||||
target: {
|
||||
id: name,
|
||||
value: selectedOption?.value || '',
|
||||
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: '#F3F4F6',
|
||||
border: '1px solid #E5E7EB',
|
||||
padding: '2px',
|
||||
'&:hover': {
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
'&:focus-within': {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#E5E7EB',
|
||||
boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.5)',
|
||||
},
|
||||
}),
|
||||
menu: base => ({
|
||||
...base,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
zIndex: 9999,
|
||||
marginTop: '4px',
|
||||
borderRadius: '0.75rem',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
fontSize: '0.875rem',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: state.isSelected ? '#EFF6FF' : state.isFocused ? '#F3F4F6' : 'white',
|
||||
color: state.isSelected ? '#2563EB' : '#1F2937',
|
||||
cursor: 'pointer',
|
||||
'&:active': {
|
||||
backgroundColor: '#DBEAFE',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: state.isSelected ? '#EFF6FF' : '#F3F4F6',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LocationSelect
|
||||
97
frontend/components/ui/SingleSelect.tsx
Normal file
97
frontend/components/ui/SingleSelect.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import Select from 'react-select'
|
||||
import { SelectOption } from '@/app/types'
|
||||
|
||||
interface SingleSelectProps {
|
||||
name: string
|
||||
value: string
|
||||
handleChange: (e: { target: { id: string; value: string } }) => void
|
||||
label: string
|
||||
placeholder: string
|
||||
options: SelectOption[]
|
||||
className?: string
|
||||
noOptionsMessage?: string
|
||||
}
|
||||
|
||||
const SingleSelect: React.FC<SingleSelectProps> = ({
|
||||
name,
|
||||
value,
|
||||
handleChange,
|
||||
label,
|
||||
placeholder,
|
||||
options,
|
||||
className = '',
|
||||
noOptionsMessage = 'Нет доступных вариантов',
|
||||
}) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<label htmlFor={name} className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
<Select<SelectOption>
|
||||
inputId={name}
|
||||
name={name}
|
||||
options={options}
|
||||
value={options.find(opt => opt.id.toString() === value)}
|
||||
onChange={selectedOption => {
|
||||
handleChange({
|
||||
target: {
|
||||
id: name,
|
||||
value: selectedOption?.id.toString() || '',
|
||||
},
|
||||
})
|
||||
}}
|
||||
isSearchable
|
||||
isClearable
|
||||
placeholder={placeholder}
|
||||
noOptionsMessage={() => noOptionsMessage}
|
||||
classNamePrefix="select"
|
||||
className="rounded-lg"
|
||||
styles={{
|
||||
control: base => ({
|
||||
...base,
|
||||
borderRadius: '0.75rem',
|
||||
backgroundColor: '#F3F4F6',
|
||||
border: '1px solid #E5E7EB',
|
||||
padding: '2px',
|
||||
'&:hover': {
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
'&:focus-within': {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#E5E7EB',
|
||||
boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.5)',
|
||||
},
|
||||
}),
|
||||
menu: base => ({
|
||||
...base,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
zIndex: 9999,
|
||||
marginTop: '4px',
|
||||
borderRadius: '0.75rem',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
fontSize: '0.875rem',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: state.isSelected ? '#EFF6FF' : state.isFocused ? '#F3F4F6' : 'white',
|
||||
color: state.isSelected ? '#2563EB' : '#1F2937',
|
||||
cursor: 'pointer',
|
||||
'&:active': {
|
||||
backgroundColor: '#DBEAFE',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: state.isSelected ? '#EFF6FF' : '#F3F4F6',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleSelect
|
||||
Reference in New Issue
Block a user