diff --git a/backend/api/search/__init__.py b/backend/api/search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/search/serializers.py b/backend/api/search/serializers.py new file mode 100644 index 0000000..87f3ade --- /dev/null +++ b/backend/api/search/serializers.py @@ -0,0 +1,10 @@ +from api.main.serializers import HomePageRouteSerializer +from routes.models import Route + + +class SearchRouteSerializer(HomePageRouteSerializer): + class Meta(HomePageRouteSerializer.Meta): + model = Route + fields = HomePageRouteSerializer.Meta.fields + + diff --git a/backend/api/search/views.py b/backend/api/search/views.py new file mode 100644 index 0000000..a705a78 --- /dev/null +++ b/backend/api/search/views.py @@ -0,0 +1,24 @@ +from rest_framework import generics +from routes.models import Route +from .serializers import SearchRouteSerializer +from api.utils.pagination import StandardResultsSetPagination +from rest_framework.exceptions import ValidationError +from routes.constants.routeChoices import owner_type_choices + + +class SearchRouteListView(generics.ListAPIView): + serializer_class = SearchRouteSerializer + pagination_class = StandardResultsSetPagination + + def get_queryset(self): + owner_type = self.kwargs.get('owner_type') + valid_types = [choice[0] for choice in owner_type_choices] + + if not owner_type or owner_type not in valid_types: + raise ValidationError("Invalid or missing owner_type. Must be either 'customer' or 'mover'") + + queryset = Route.objects.filter( + owner_type=owner_type + ).order_by('-arrival_DT') + + return queryset diff --git a/backend/api/urls.py b/backend/api/urls.py index 3290e5c..8794848 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -16,6 +16,8 @@ CountryView, GetMembershipData, ChangeUserMembership) +from api.search.views import SearchRouteListView + urlpatterns = [ path("v1/faq/", FAQView.as_view(), name='faqMain'), path("v1/news/", NewsView.as_view(), name="newsmain"), @@ -38,4 +40,6 @@ urlpatterns = [ path("v1/plans/", GetMembershipData.as_view({'get':'get_pricing_data'}), name='get_pricing_data'), path("v1/account/change_membership/", ChangeUserMembership.as_view({'patch':'change_plan'}), name='change_plan'), + + path('v1/search//', SearchRouteListView.as_view(), name='search-routes'), ] \ No newline at end of file diff --git a/backend/api/utils/pagination.py b/backend/api/utils/pagination.py new file mode 100644 index 0000000..bfc41a3 --- /dev/null +++ b/backend/api/utils/pagination.py @@ -0,0 +1,6 @@ +from rest_framework.pagination import PageNumberPagination + +class StandardResultsSetPagination(PageNumberPagination): + page_size = 25 + page_size_query_param = 'page_size' + max_page_size = 25 \ No newline at end of file diff --git a/frontend/app/(urls)/search/[category]/page.tsx b/frontend/app/(urls)/search/[category]/page.tsx index 5691df0..6ec1604 100644 --- a/frontend/app/(urls)/search/[category]/page.tsx +++ b/frontend/app/(urls)/search/[category]/page.tsx @@ -1,26 +1,58 @@ import React, { Suspense } from 'react' +import type { Metadata } from 'next' import SearchCard from '../components/SearchCard' +import { SearchCardProps, SearchPageProps } from '@/app/types' +import { fetchRoutes } from '@/lib/search/fetchRoutes' -interface SearchPageProps { - params: { - category: string +export async function generateMetadata(): Promise { + return { + title: 'Поиск перевозчиков и посылок | Tripwb', + description: + 'Найдите самые быстрые варианты по перевозке своих посылок | Tripwb - текст текст текст', + openGraph: { + title: 'Поиск текст текст | Tripwb - текст текст текст', + description: 'Найдите лучшие что то', + url: 'https://tripwb.com/search', + siteName: 'TripWB', + images: [ + { + url: 'https://i.ibb.co/gmqzzmb/header-logo-mod-1200x630.png', + width: 1200, + height: 630, + alt: 'Tripwb - текст текст текст', + }, + ], + locale: 'ru_RU', + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title: 'Поиск текст текст | TripWB', + description: 'Ттекст текст текст текст', + images: [ + { + url: 'https://i.ibb.co/gmqzzmb/header-logo-mod-1200x630.png', + width: 1200, + height: 630, + alt: 'Tripwb - текст текст текст', + }, + ], + }, + alternates: { + canonical: 'https://tripwb.com/search/', + }, + robots: { + index: true, + follow: true, + }, } - searchParams: { - [key: string]: string | string[] | undefined - } -} - -// получаем все предложения по выбранному owner_type -async function fetchSearch(category: string, query: string = '') { - // get search api(owner_type) - return [] } export default async function SearchPage(props: SearchPageProps) { const params = await props.params const { category } = params - const initialData = await fetchSearch(category) + const { results, count } = await fetchRoutes(category) return (
@@ -29,14 +61,12 @@ export default async function SearchPage(props: SearchPageProps) { Загрузка результатов...
}> - {/* Здесь будет компонент с результатами поиска */}
- {initialData.map((item: any, index: number) => ( -
- {/* Здесь будет карточка с результатом */} -

Результат поиска {index + 1}

-
- ))} + {results.length > 0 ? ( + results.map((item: SearchCardProps) => ) + ) : ( +
Объявления не найдены
+ )}
diff --git a/frontend/app/types/index.ts b/frontend/app/types/index.ts index a808da8..81a087d 100644 --- a/frontend/app/types/index.ts +++ b/frontend/app/types/index.ts @@ -226,3 +226,17 @@ export interface PricingCardProps { isActive?: boolean onPlanChange?: () => void } + +export interface SearchResponse { + count: number + results: SearchCardProps[] +} + +export interface SearchPageProps { + params: { + category: string + } + searchParams: { + [key: string]: string | string[] | undefined + } +} diff --git a/frontend/components/ui/LocationSelect.tsx b/frontend/components/ui/LocationSelect.tsx index 0284d09..c37b178 100644 --- a/frontend/components/ui/LocationSelect.tsx +++ b/frontend/components/ui/LocationSelect.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react' import Select from 'react-select' import { SelectOption } from '@/app/types' import axios from 'axios' +import Tooltip from './Tooltip' interface LocationOption extends SelectOption { value: string // добавляем поле value к базовому интерфейсу SelectOption @@ -19,6 +20,7 @@ interface LocationSelectProps { placeholder: string countryId?: string isCity?: boolean + tooltip?: string | React.ReactNode } const LocationSelect: React.FC = ({ @@ -26,6 +28,7 @@ const LocationSelect: React.FC = ({ value, handleChange, label, + tooltip, placeholder, countryId, isCity = false, @@ -61,9 +64,14 @@ const LocationSelect: React.FC = ({ return (
- + {label && ( +
+ + {tooltip && } +
+ )} inputId={name} name={name} @@ -90,7 +98,6 @@ const LocationSelect: React.FC = ({ control: base => ({ ...base, borderRadius: '0.75rem', - backgroundColor: '#F3F4F6', border: '1px solid #E5E7EB', padding: '2px', '&:hover': { diff --git a/frontend/lib/search/fetchRoutes.ts b/frontend/lib/search/fetchRoutes.ts new file mode 100644 index 0000000..903f60e --- /dev/null +++ b/frontend/lib/search/fetchRoutes.ts @@ -0,0 +1,23 @@ +import { SearchResponse } from '@/app/types' + +// получаем все предложения по выбранному owner_type +export async function fetchRoutes(category: string, query: string = ''): Promise { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/search/${category}/${query ? `?${query}` : ''}`, + { + cache: 'no-store', + } + ) + + if (!response.ok) { + throw new Error('Failed to fetch search results') + } + + const data = await response.json() + return data + } catch (error) { + console.error('Error fetching search results:', error) + return { results: [], count: 0 } + } +}