sender page
This commit is contained in:
@@ -6,6 +6,26 @@ from api.models import UserProfile
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
class CountrySerializer(serializers.ModelSerializer):
|
||||||
|
value = serializers.CharField(source='international_name') # для совместимости с селектом на фронте
|
||||||
|
label = serializers.SerializerMethodField() # для отображения в селекте
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Country
|
||||||
|
fields = ['id', 'value', 'label', 'flag_img_url']
|
||||||
|
|
||||||
|
def get_label(self, obj):
|
||||||
|
return obj.international_name or obj.official_name
|
||||||
|
|
||||||
|
class CitySerializer(serializers.ModelSerializer):
|
||||||
|
value = serializers.CharField(source='name') # для совместимости с селектом
|
||||||
|
label = serializers.CharField(source='name') # для отображения в селекте
|
||||||
|
country_name = serializers.CharField(source='country.international_name')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = City
|
||||||
|
fields = ['id', 'value', 'label', 'country_name']
|
||||||
|
|
||||||
class RouteSerializer(serializers.ModelSerializer):
|
class RouteSerializer(serializers.ModelSerializer):
|
||||||
from_city_name = serializers.SerializerMethodField()
|
from_city_name = serializers.SerializerMethodField()
|
||||||
to_city_name = serializers.SerializerMethodField()
|
to_city_name = serializers.SerializerMethodField()
|
||||||
@@ -160,16 +180,16 @@ class CreateRouteSerializer(serializers.ModelSerializer):
|
|||||||
to_city = City.objects.get(name=validated_data.pop('city_to'), country=country_to)
|
to_city = City.objects.get(name=validated_data.pop('city_to'), country=country_to)
|
||||||
|
|
||||||
# обновляем номер телефона в профиле пользователя
|
# обновляем номер телефона в профиле пользователя
|
||||||
contact_number = validated_data.pop('contact_number')
|
phone_number = validated_data.pop('phone_number')
|
||||||
user_profile = get_object_or_404(UserProfile, user=self.context['request'].user)
|
user_profile = get_object_or_404(UserProfile, user=self.context['request'].user)
|
||||||
|
|
||||||
# проверяем, не используется ли этот номер другим пользователем
|
# проверяем, не используется ли этот номер другим пользователем
|
||||||
if UserProfile.objects.filter(phone_number=contact_number).exclude(user=self.context['request'].user).exists():
|
if UserProfile.objects.filter(phone_number=phone_number).exclude(user=self.context['request'].user).exists():
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
"contact_number": "Этот номер телефона уже используется другим пользователем"
|
"phone_number": "Этот номер телефона уже используется другим пользователем"
|
||||||
})
|
})
|
||||||
|
|
||||||
user_profile.phone_number = contact_number
|
user_profile.phone_number = phone_number
|
||||||
user_profile.save()
|
user_profile.save()
|
||||||
|
|
||||||
# создаем маршрут
|
# создаем маршрут
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ from rest_framework.response import Response
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
from api.auth.serializers import UserResponseSerializer
|
from api.auth.serializers import UserResponseSerializer
|
||||||
from api.models import UserProfile
|
from api.models import UserProfile
|
||||||
from api.utils.decorators import handle_exceptions
|
from api.utils.decorators import handle_exceptions
|
||||||
from routes.models import Route
|
from routes.models import Route, City, Country
|
||||||
from .serializers import RouteSerializer, CreateRouteSerializer
|
from .serializers import RouteSerializer, CreateRouteSerializer, CitySerializer, CountrySerializer
|
||||||
|
|
||||||
class UserDataView(ViewSet):
|
class UserDataView(ViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
@@ -95,8 +96,53 @@ class AccountActionsView(ViewSet):
|
|||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
@action(detail=False, methods=['post'])
|
||||||
@handle_exceptions
|
@handle_exceptions
|
||||||
def create_sender_route(self, request):
|
def create_route(self, request):
|
||||||
serializer = CreateRouteSerializer(data=request.data)
|
serializer = CreateRouteSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
class CityView(ViewSet):
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
@handle_exceptions
|
||||||
|
def get_cities(self, request):
|
||||||
|
# получаем параметр country_id из query params
|
||||||
|
country_id = request.query_params.get('country_id')
|
||||||
|
|
||||||
|
# базовый QuerySet
|
||||||
|
cities = City.objects.all()
|
||||||
|
|
||||||
|
# фильтруем города по стране, если указан country_id
|
||||||
|
if country_id:
|
||||||
|
cities = cities.filter(country_id=country_id)
|
||||||
|
|
||||||
|
# поиск по названию города
|
||||||
|
search = request.query_params.get('search')
|
||||||
|
if search:
|
||||||
|
cities = cities.filter(name__icontains=search)
|
||||||
|
|
||||||
|
# ограничиваем количество результатов и сортируем по имени
|
||||||
|
cities = cities.order_by('name')[:100]
|
||||||
|
|
||||||
|
return Response(CitySerializer(cities, many=True).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class CountryView(ViewSet):
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
@handle_exceptions
|
||||||
|
def get_countries(self, request):
|
||||||
|
# базовый QuerySet
|
||||||
|
countries = Country.objects.all()
|
||||||
|
|
||||||
|
# поиск по названию страны
|
||||||
|
search = request.query_params.get('search')
|
||||||
|
if search:
|
||||||
|
countries = countries.filter(
|
||||||
|
models.Q(international_name__icontains=search) |
|
||||||
|
models.Q(official_name__icontains=search)
|
||||||
|
)
|
||||||
|
|
||||||
|
# сортируем по международному названию
|
||||||
|
countries = countries.order_by('international_name')
|
||||||
|
|
||||||
|
return Response(CountrySerializer(countries, many=True).data, status=status.HTTP_200_OK)
|
||||||
@@ -8,7 +8,7 @@ LoginViewSet,
|
|||||||
LogoutView,
|
LogoutView,
|
||||||
RefreshTokenView)
|
RefreshTokenView)
|
||||||
|
|
||||||
from api.account.client.views import UserDataView, AccountActionsView
|
from api.account.client.views import UserDataView, AccountActionsView, CityView, CountryView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("v1/faq/", FAQView.as_view(), name='faqMain'),
|
path("v1/faq/", FAQView.as_view(), name='faqMain'),
|
||||||
@@ -24,5 +24,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
path("v1/account/change_main_data/", AccountActionsView.as_view({'patch':'change_data_main_tab'}), name='change_data_main_tab'),
|
path("v1/account/change_main_data/", AccountActionsView.as_view({'patch':'change_data_main_tab'}), name='change_data_main_tab'),
|
||||||
path("v1/account/routes/", AccountActionsView.as_view({'get':'user_routes'}), name='user_routes'),
|
path("v1/account/routes/", AccountActionsView.as_view({'get':'user_routes'}), name='user_routes'),
|
||||||
path("v1/account/create_sender/", AccountActionsView.as_view({'post':'create_sender_route'}), name='create_sender_route')
|
path("v1/account/create_route/", AccountActionsView.as_view({'post':'create_route'}), name='create_route'),
|
||||||
|
|
||||||
|
path("v1/cities/", CityView.as_view({'get':'get_cities'}), name='get_cities'),
|
||||||
|
path("v1/countries/", CountryView.as_view({'get':'get_countries'}), name='get_countries')
|
||||||
]
|
]
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import MultiSelect from '@/components/ui/Selector'
|
|
||||||
import TextInput from '@/components/ui/TextInput'
|
|
||||||
import PhoneInput from '@/components/ui/PhoneInput'
|
import PhoneInput from '@/components/ui/PhoneInput'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import TextAreaInput from '@/components/ui/TextAreaInput'
|
import TextAreaInput from '@/components/ui/TextAreaInput'
|
||||||
import CheckboxInput from '@/components/ui/CheckboxInput'
|
import CheckboxInput from '@/components/ui/CheckboxInput'
|
||||||
|
import LocationSelect from '@/components/ui/LocationSelect'
|
||||||
|
import SingleSelect from '@/components/ui/SingleSelect'
|
||||||
import { useForm } from '@/app/hooks/useForm'
|
import { useForm } from '@/app/hooks/useForm'
|
||||||
import useUserStore from '@/app/store/userStore'
|
import useUserStore from '@/app/store/userStore'
|
||||||
import showToast from '@/components/ui/Toast'
|
import showToast from '@/components/ui/Toast'
|
||||||
@@ -29,10 +29,10 @@ const formatDateToHTML = (date: Date) => {
|
|||||||
|
|
||||||
const validationRules = {
|
const validationRules = {
|
||||||
transport: { required: true },
|
transport: { required: true },
|
||||||
country_from: { required: true, minLength: 2 },
|
country_from_id: { required: true },
|
||||||
city_from: { required: true, minLength: 2 },
|
city_from: { required: true },
|
||||||
country_to: { required: true, minLength: 2 },
|
country_to_id: { required: true },
|
||||||
city_to: { required: true, minLength: 2 },
|
city_to: { required: true },
|
||||||
cargo_type: { required: true },
|
cargo_type: { required: true },
|
||||||
departure: {
|
departure: {
|
||||||
required: true,
|
required: true,
|
||||||
@@ -61,6 +61,8 @@ const SenderPage = () => {
|
|||||||
city_from: '',
|
city_from: '',
|
||||||
country_to: '',
|
country_to: '',
|
||||||
city_to: '',
|
city_to: '',
|
||||||
|
country_from_id: '',
|
||||||
|
country_to_id: '',
|
||||||
cargo_type: '',
|
cargo_type: '',
|
||||||
departure: '',
|
departure: '',
|
||||||
arrival: '',
|
arrival: '',
|
||||||
@@ -86,15 +88,26 @@ const SenderPage = () => {
|
|||||||
validationRules,
|
validationRules,
|
||||||
async values => {
|
async values => {
|
||||||
try {
|
try {
|
||||||
// await addNewSpecialist(values, selectedImage || undefined)
|
const response = await fetch('/api/account/sender', {
|
||||||
showToast({
|
method: 'PATCH',
|
||||||
type: 'success',
|
headers: {
|
||||||
message: 'Маршрут успешно создан!',
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(values),
|
||||||
})
|
})
|
||||||
} catch {
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Ошибка при обновлении данных')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
setUser(result.user)
|
||||||
|
showToast({ type: 'success', message: 'Данные успешно обновлены!' })
|
||||||
|
} catch (error) {
|
||||||
showToast({
|
showToast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: 'Упс, что то пошло не так...',
|
message: error instanceof Error ? error.message : 'Ой, что то пошло не так..',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,45 +128,27 @@ const SenderPage = () => {
|
|||||||
{/* тип груза и транспорта */}
|
{/* тип груза и транспорта */}
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="cargo_type" className="block text-sm font-medium text-gray-700">
|
<SingleSelect
|
||||||
Тип груза
|
|
||||||
</label>
|
|
||||||
<MultiSelect
|
|
||||||
value={values.cargo_type ? [parseInt(values.cargo_type)] : []}
|
|
||||||
handleChange={e => {
|
|
||||||
handleChange({
|
|
||||||
target: {
|
|
||||||
id: 'cargo_type',
|
|
||||||
value: e.target.value[0]?.toString() || '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
name="cargo_type"
|
name="cargo_type"
|
||||||
|
value={values.cargo_type}
|
||||||
|
handleChange={handleChange}
|
||||||
|
label="Тип груза"
|
||||||
|
placeholder="Выберите тип груза"
|
||||||
options={cargoOptions}
|
options={cargoOptions}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
placeholder="Выберите тип груза"
|
|
||||||
noOptionsMessage="Нет доступных типов груза"
|
noOptionsMessage="Нет доступных типов груза"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="transport" className="block text-sm font-medium text-gray-700">
|
<SingleSelect
|
||||||
Способ перевозки
|
|
||||||
</label>
|
|
||||||
<MultiSelect
|
|
||||||
value={values.transport ? [parseInt(values.transport)] : []}
|
|
||||||
handleChange={e => {
|
|
||||||
handleChange({
|
|
||||||
target: {
|
|
||||||
id: 'transport',
|
|
||||||
value: e.target.value[0]?.toString() || '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
name="transport"
|
name="transport"
|
||||||
|
value={values.transport}
|
||||||
|
handleChange={handleChange}
|
||||||
|
label="Способ перевозки"
|
||||||
|
placeholder="Выберите способ перевозки"
|
||||||
options={transportOptions}
|
options={transportOptions}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
placeholder="Выберите способ перевозки"
|
|
||||||
noOptionsMessage="Нет доступных способов перевозки"
|
noOptionsMessage="Нет доступных способов перевозки"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,41 +160,80 @@ const SenderPage = () => {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<TextInput
|
<LocationSelect
|
||||||
name="country_from"
|
name="country_from"
|
||||||
value={values.country_from}
|
value={values.country_from}
|
||||||
handleChange={handleChange}
|
handleChange={e => {
|
||||||
|
const selectedOption = e.target.selectedOption
|
||||||
|
handleChange({
|
||||||
|
target: {
|
||||||
|
id: 'country_from',
|
||||||
|
value: e.target.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
handleChange({
|
||||||
|
target: {
|
||||||
|
id: 'country_from_id',
|
||||||
|
value: selectedOption?.id?.toString() || '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
handleChange({
|
||||||
|
target: {
|
||||||
|
id: 'city_from',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
label="Страна отправления"
|
label="Страна отправления"
|
||||||
placeholder="Введите страну отправления"
|
placeholder="Выберите страну отправления"
|
||||||
style="register"
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<LocationSelect
|
||||||
name="country_to"
|
name="city_from"
|
||||||
value={values.country_to}
|
value={values.city_from}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
label="Страна назначения"
|
label="Город отправления"
|
||||||
placeholder="Введите страну назначения"
|
placeholder="Выберите город отправления"
|
||||||
style="register"
|
countryId={values.country_from_id}
|
||||||
|
isCity
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<TextInput
|
<LocationSelect
|
||||||
|
name="country_to"
|
||||||
|
value={values.country_to}
|
||||||
|
handleChange={e => {
|
||||||
|
const selectedOption = e.target.selectedOption
|
||||||
|
handleChange({
|
||||||
|
target: {
|
||||||
|
id: 'country_to',
|
||||||
|
value: e.target.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
handleChange({
|
||||||
|
target: {
|
||||||
|
id: 'country_to_id',
|
||||||
|
value: selectedOption?.id?.toString() || '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
handleChange({
|
||||||
|
target: {
|
||||||
|
id: 'city_to',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
label="Страна назначения"
|
||||||
|
placeholder="Выберите страну назначения"
|
||||||
|
/>
|
||||||
|
<LocationSelect
|
||||||
name="city_to"
|
name="city_to"
|
||||||
value={values.city_to}
|
value={values.city_to}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
label="Город назначения"
|
label="Город назначения"
|
||||||
placeholder="Введите город назначения"
|
placeholder="Выберите город назначения"
|
||||||
style="register"
|
countryId={values.country_to_id}
|
||||||
/>
|
isCity
|
||||||
|
|
||||||
<TextInput
|
|
||||||
name="country_to"
|
|
||||||
value={values.country_to}
|
|
||||||
handleChange={handleChange}
|
|
||||||
label="Страна назначения"
|
|
||||||
placeholder="Введите страну назначения"
|
|
||||||
style="register"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ export async function POST(req: NextRequest) {
|
|||||||
email_notification,
|
email_notification,
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/account/create_sender/`, {
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/account/create_route/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${session.accessToken}`,
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
owner_type: 'sender',
|
||||||
transport,
|
transport,
|
||||||
country_from,
|
country_from,
|
||||||
city_from,
|
city_from,
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ export interface SenderPageProps extends Record<string, FormValue> {
|
|||||||
city_from: string
|
city_from: string
|
||||||
country_to: string
|
country_to: string
|
||||||
city_to: string
|
city_to: string
|
||||||
|
country_from_id: string
|
||||||
|
country_to_id: string
|
||||||
cargo_type: string
|
cargo_type: string
|
||||||
departure: string
|
departure: string
|
||||||
arrival: string
|
arrival: string
|
||||||
|
|||||||
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