from-to search route

This commit is contained in:
2025-05-26 17:36:30 +03:00
parent c761c60818
commit 46945e32a8
8 changed files with 207 additions and 25 deletions

View File

@@ -1,8 +1,81 @@
from api.main.serializers import HomePageRouteSerializer
from routes.models import Route
from rest_framework import serializers
from routes.models import Route, Country
from api.main.serializers import RouteSerializer
class SearchRouteSerializer(RouteSerializer):
id = serializers.IntegerField()
username = serializers.SerializerMethodField()
owner_type = serializers.CharField()
from_city_name = serializers.SerializerMethodField('get_start_point')
from_country_name = serializers.SerializerMethodField('get_country_from')
to_city_name = serializers.SerializerMethodField('get_end_point')
to_country_name = serializers.SerializerMethodField('get_country_to')
formatted_cargo_type = serializers.SerializerMethodField('get_cargo_type')
formatted_transport = serializers.SerializerMethodField('get_moving_type')
type_transport = serializers.CharField()
userImg = serializers.SerializerMethodField()
comment = serializers.CharField()
formatted_departure = serializers.DateTimeField(source='departure_DT')
formatted_arrival = serializers.DateTimeField(source='arrival_DT')
country_from_icon = serializers.SerializerMethodField()
country_to_icon = serializers.SerializerMethodField()
class SearchRouteSerializer(HomePageRouteSerializer):
class Meta(HomePageRouteSerializer.Meta):
class Meta:
model = Route
fields = HomePageRouteSerializer.Meta.fields
fields = (
'id', 'username', 'owner_type', 'from_city_name', 'from_country_name',
'to_city_name', 'to_country_name', 'formatted_cargo_type',
'formatted_transport', 'type_transport', 'userImg', 'comment',
'formatted_departure', 'formatted_arrival', 'country_from_icon',
'country_to_icon'
)
def get_username(self, obj):
return obj.owner.first_name if obj.owner else None
def get_userImg(self, obj):
try:
if obj.owner and hasattr(obj.owner, 'userprofile') and obj.owner.userprofile.image:
return obj.owner.userprofile.image.url
return None
except Exception as e:
print(f"Error in get_userImg: {e}")
return None
def get_country_from_icon(self, obj):
country = self.get_from_country_name(obj)
if not country:
return None
try:
country_obj = Country.objects.get(international_name=country)
return country_obj.flag_img_url
except Country.DoesNotExist:
return None
def get_country_to_icon(self, obj):
country = self.get_to_country_name(obj)
if not country:
return None
try:
country_obj = Country.objects.get(international_name=country)
return country_obj.flag_img_url
except Country.DoesNotExist:
return None
def get_start_point(self, obj):
return self.get_from_city_name(obj)
def get_country_from(self, obj):
return self.get_from_country_name(obj)
def get_end_point(self, obj):
return self.get_to_city_name(obj)
def get_country_to(self, obj):
return self.get_to_country_name(obj)
def get_cargo_type(self, obj):
return self.get_formatted_cargo_type(obj)
def get_moving_type(self, obj):
return self.get_formatted_transport(obj)

View File

@@ -12,6 +12,9 @@ class SearchRouteListView(generics.ListAPIView):
def get_queryset(self):
owner_type = self.kwargs.get('owner_type')
from_city = self.request.query_params.get('from')
to_city = self.request.query_params.get('to')
valid_types = [choice[0] for choice in owner_type_choices]
current_time = timezone.now()
@@ -23,6 +26,12 @@ class SearchRouteListView(generics.ListAPIView):
owner_type=owner_type,
status="actual")
# фильтруем по городам если они указаны
if from_city:
queryset = queryset.filter(from_city__name__iexact=from_city)
if to_city:
queryset = queryset.filter(to_city__name__iexact=to_city)
# фильтруем по времени в зависимости от типа
if owner_type == 'mover':
queryset = queryset.filter(departure_DT__gt=current_time)

View File

@@ -1,7 +1,54 @@
import React from 'react'
import Image from 'next/image'
import { getNews } from '@/lib/main/fetchNews'
import { notFound } from 'next/navigation'
const page = () => {
return <div>page</div>
interface PageProps {
params: Promise<{
slug: string
}>
}
export default page
async function getNewsItem(slug: string) {
const news = await getNews()
return news.find(item => item.slug === slug)
}
export default async function NewsPage({ params }: PageProps) {
const { slug } = await params
const newsItem = await getNewsItem(slug)
if (!newsItem) {
notFound()
}
return (
<article className="mx-auto max-w-5xl px-4 py-8">
<div className="w-full overflow-hidden rounded-2xl bg-white shadow-lg">
<div className="relative h-[400px] w-full">
<Image
src={
newsItem.titleImage.startsWith('http')
? newsItem.titleImage
: `http://127.0.0.1:8000${newsItem.titleImage}` || '/placeholder-image.jpg'
}
alt={newsItem.title}
fill
className="object-cover"
priority
sizes="(max-width: 1280px) 100vw, 1280px"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/40">
<h1 className="px-4 text-center text-4xl font-bold text-white md:text-5xl">
{newsItem.title}
</h1>
</div>
</div>
</div>
<div className="prose prose-lg mt-8 max-w-none">
<div className="whitespace-pre-wrap">{newsItem.content}</div>
</div>
</article>
)
}

View File

@@ -1,5 +1,7 @@
import React, { Suspense } from 'react'
import type { Metadata } from 'next'
import SearchCard from '../../components/SearchCard'
import { SearchCardProps } from '@/app/types'
interface SearchPageProps {
params: {
@@ -8,10 +10,59 @@ interface SearchPageProps {
}
}
// дернуть search api для from-to параметров
async function fetchSearch(category: string, from: string, to: string) {
// get search api(owner_type, from, to)
return []
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/search/${category}/?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
{
cache: 'no-store',
}
)
if (!response.ok) {
throw new Error('Failed to fetch search results')
}
const data = await response.json()
return {
results: data.results,
count: data.results.length,
}
}
export async function generateMetadata({
params,
}: {
params: SearchPageProps['params']
}): Promise<Metadata> {
const [fromCity, toCity] = params.route.split('-')
return {
title: `Поиск ${params.category === 'mover' ? 'перевозчика' : 'посылки'} ${fromCity}${toCity} | Tripwb`,
description: `Найдите ${params.category === 'mover' ? 'перевозчика' : 'посылку'} по маршруту ${fromCity}${toCity} | Tripwb`,
openGraph: {
title: `Поиск ${params.category === 'mover' ? 'перевозчика' : 'посылки'} ${fromCity}${toCity} | Tripwb`,
description: `Найдите ${params.category === 'mover' ? 'перевозчика' : 'посылку'} по маршруту ${fromCity}${toCity}`,
url: `https://tripwb.com/search/${params.category}/${params.route}`,
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',
},
alternates: {
canonical: `https://tripwb.com/search/${params.category}/${params.route}`,
},
robots: {
index: true,
follow: true,
},
}
}
export default async function SearchPage(props: SearchPageProps) {
@@ -19,7 +70,7 @@ export default async function SearchPage(props: SearchPageProps) {
const { category, route } = params
const [fromCity, toCity] = route.split('-')
const initialData = await fetchSearch(category, fromCity, toCity)
const { results, count } = await fetchSearch(category, fromCity, toCity)
return (
<div className="container mx-auto p-4">
@@ -33,14 +84,12 @@ export default async function SearchPage(props: SearchPageProps) {
</div>
<Suspense fallback={<div>Загрузка результатов...</div>}>
{/* результаты поиска */}
<div className="space-y-4">
{initialData.map((item: any, index: number) => (
<div key={index} className="rounded-lg border p-4">
{/* Здесь будет карточка с результатом */}
<p>Результат поиска {index + 1}</p>
</div>
))}
{results.length > 0 ? (
results.map((item: SearchCardProps) => <SearchCard key={item.id} {...item} />)
) : (
<div className="text-center text-gray-500">По данному маршруту ничего не найдено</div>
)}
</div>
</Suspense>
</div>

View File

@@ -108,12 +108,14 @@ const SearchCard = ({
onClick={handleLeadClick}
/>
</div>
<div className="rounded-lg bg-[#f8f8f8] p-5">
<div className="flex items-baseline gap-2">
<span className="text-gray-600">{comment}</span>
{comment && (
<div className="rounded-lg bg-[#f8f8f8] p-5">
<div className="flex items-baseline gap-2">
<span className="text-gray-600">{comment}</span>
</div>
</div>
</div>
)}
<div className="flex justify-end pt-2 text-sm text-gray-500">Объявление {id}</div>
<div className="mt-6 flex items-center justify-between">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -70,6 +70,8 @@ export interface NewsItem {
content: string
titleImage: string
slug: string
path: string
filename: string
}
export interface NewsProps {

View File

@@ -13,7 +13,7 @@ const nextConfig: NextConfig = {
protocol: 'http', // для локал девеломпента
hostname: '127.0.0.1',
port: '8000',
pathname: '/media/uploads/**',
pathname: '/media/**',
},
{
protocol: 'https',