search page without from-to
This commit is contained in:
0
backend/api/search/__init__.py
Normal file
0
backend/api/search/__init__.py
Normal file
10
backend/api/search/serializers.py
Normal file
10
backend/api/search/serializers.py
Normal file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
24
backend/api/search/views.py
Normal file
24
backend/api/search/views.py
Normal file
@@ -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
|
||||||
@@ -16,6 +16,8 @@ CountryView,
|
|||||||
GetMembershipData,
|
GetMembershipData,
|
||||||
ChangeUserMembership)
|
ChangeUserMembership)
|
||||||
|
|
||||||
|
from api.search.views import SearchRouteListView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("v1/faq/", FAQView.as_view(), name='faqMain'),
|
path("v1/faq/", FAQView.as_view(), name='faqMain'),
|
||||||
path("v1/news/", NewsView.as_view(), name="newsmain"),
|
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/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/account/change_membership/", ChangeUserMembership.as_view({'patch':'change_plan'}), name='change_plan'),
|
||||||
|
|
||||||
|
path('v1/search/<str:owner_type>/', SearchRouteListView.as_view(), name='search-routes'),
|
||||||
]
|
]
|
||||||
6
backend/api/utils/pagination.py
Normal file
6
backend/api/utils/pagination.py
Normal file
@@ -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
|
||||||
@@ -1,26 +1,58 @@
|
|||||||
import React, { Suspense } from 'react'
|
import React, { Suspense } from 'react'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
import SearchCard from '../components/SearchCard'
|
import SearchCard from '../components/SearchCard'
|
||||||
|
import { SearchCardProps, SearchPageProps } from '@/app/types'
|
||||||
|
import { fetchRoutes } from '@/lib/search/fetchRoutes'
|
||||||
|
|
||||||
interface SearchPageProps {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
params: {
|
return {
|
||||||
category: string
|
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) {
|
export default async function SearchPage(props: SearchPageProps) {
|
||||||
const params = await props.params
|
const params = await props.params
|
||||||
const { category } = params
|
const { category } = params
|
||||||
|
|
||||||
const initialData = await fetchSearch(category)
|
const { results, count } = await fetchRoutes(category)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
@@ -29,14 +61,12 @@ export default async function SearchPage(props: SearchPageProps) {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Suspense fallback={<div>Загрузка результатов...</div>}>
|
<Suspense fallback={<div>Загрузка результатов...</div>}>
|
||||||
{/* Здесь будет компонент с результатами поиска */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{initialData.map((item: any, index: number) => (
|
{results.length > 0 ? (
|
||||||
<div key={index} className="rounded-lg border p-4">
|
results.map((item: SearchCardProps) => <SearchCard key={item.id} {...item} />)
|
||||||
{/* Здесь будет карточка с результатом */}
|
) : (
|
||||||
<p>Результат поиска {index + 1}</p>
|
<div className="text-center text-gray-500">Объявления не найдены</div>
|
||||||
</div>
|
)}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -226,3 +226,17 @@ export interface PricingCardProps {
|
|||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
onPlanChange?: () => void
|
onPlanChange?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchResponse {
|
||||||
|
count: number
|
||||||
|
results: SearchCardProps[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchPageProps {
|
||||||
|
params: {
|
||||||
|
category: string
|
||||||
|
}
|
||||||
|
searchParams: {
|
||||||
|
[key: string]: string | string[] | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import Select from 'react-select'
|
import Select from 'react-select'
|
||||||
import { SelectOption } from '@/app/types'
|
import { SelectOption } from '@/app/types'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import Tooltip from './Tooltip'
|
||||||
|
|
||||||
interface LocationOption extends SelectOption {
|
interface LocationOption extends SelectOption {
|
||||||
value: string // добавляем поле value к базовому интерфейсу SelectOption
|
value: string // добавляем поле value к базовому интерфейсу SelectOption
|
||||||
@@ -19,6 +20,7 @@ interface LocationSelectProps {
|
|||||||
placeholder: string
|
placeholder: string
|
||||||
countryId?: string
|
countryId?: string
|
||||||
isCity?: boolean
|
isCity?: boolean
|
||||||
|
tooltip?: string | React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocationSelect: React.FC<LocationSelectProps> = ({
|
const LocationSelect: React.FC<LocationSelectProps> = ({
|
||||||
@@ -26,6 +28,7 @@ const LocationSelect: React.FC<LocationSelectProps> = ({
|
|||||||
value,
|
value,
|
||||||
handleChange,
|
handleChange,
|
||||||
label,
|
label,
|
||||||
|
tooltip,
|
||||||
placeholder,
|
placeholder,
|
||||||
countryId,
|
countryId,
|
||||||
isCity = false,
|
isCity = false,
|
||||||
@@ -61,9 +64,14 @@ const LocationSelect: React.FC<LocationSelectProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={name} className="mb-1 block text-sm font-medium text-gray-700">
|
{label && (
|
||||||
{label}
|
<div className="my-2 flex items-center gap-2">
|
||||||
</label>
|
<label className="text-sm font-medium text-gray-500" htmlFor={name}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{tooltip && <Tooltip content={tooltip} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Select<LocationOption>
|
<Select<LocationOption>
|
||||||
inputId={name}
|
inputId={name}
|
||||||
name={name}
|
name={name}
|
||||||
@@ -90,7 +98,6 @@ const LocationSelect: React.FC<LocationSelectProps> = ({
|
|||||||
control: base => ({
|
control: base => ({
|
||||||
...base,
|
...base,
|
||||||
borderRadius: '0.75rem',
|
borderRadius: '0.75rem',
|
||||||
backgroundColor: '#F3F4F6',
|
|
||||||
border: '1px solid #E5E7EB',
|
border: '1px solid #E5E7EB',
|
||||||
padding: '2px',
|
padding: '2px',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
|
|||||||
23
frontend/lib/search/fetchRoutes.ts
Normal file
23
frontend/lib/search/fetchRoutes.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { SearchResponse } from '@/app/types'
|
||||||
|
|
||||||
|
// получаем все предложения по выбранному owner_type
|
||||||
|
export async function fetchRoutes(category: string, query: string = ''): Promise<SearchResponse> {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user