diff --git a/backend/api/search/serializers.py b/backend/api/search/serializers.py index bfa408f..f23494f 100644 --- a/backend/api/search/serializers.py +++ b/backend/api/search/serializers.py @@ -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) diff --git a/backend/api/search/views.py b/backend/api/search/views.py index 0e0e137..a639fb7 100644 --- a/backend/api/search/views.py +++ b/backend/api/search/views.py @@ -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) diff --git a/frontend/app/(urls)/news/[slug]/page.tsx b/frontend/app/(urls)/news/[slug]/page.tsx index 2868bf2..b1935a3 100644 --- a/frontend/app/(urls)/news/[slug]/page.tsx +++ b/frontend/app/(urls)/news/[slug]/page.tsx @@ -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
page
+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 ( +
+
+
+ {newsItem.title} +
+

+ {newsItem.title} +

+
+
+
+ +
+
{newsItem.content}
+
+
+ ) +} diff --git a/frontend/app/(urls)/search/[category]/[route]/page.tsx b/frontend/app/(urls)/search/[category]/[route]/page.tsx index 99e9ebc..7bb532e 100644 --- a/frontend/app/(urls)/search/[category]/[route]/page.tsx +++ b/frontend/app/(urls)/search/[category]/[route]/page.tsx @@ -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 { + 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 (
@@ -33,14 +84,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/(urls)/search/components/SearchCard.tsx b/frontend/app/(urls)/search/components/SearchCard.tsx index 56af4b9..bdac3f2 100644 --- a/frontend/app/(urls)/search/components/SearchCard.tsx +++ b/frontend/app/(urls)/search/components/SearchCard.tsx @@ -108,12 +108,14 @@ const SearchCard = ({ onClick={handleLeadClick} /> - -
-
- {comment} + {comment && ( +
+
+ {comment} +
-
+ )} +
Объявление № {id}
diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico index 718d6fe..067fc6c 100644 Binary files a/frontend/app/favicon.ico and b/frontend/app/favicon.ico differ diff --git a/frontend/app/types/index.ts b/frontend/app/types/index.ts index 81db2b2..4133297 100644 --- a/frontend/app/types/index.ts +++ b/frontend/app/types/index.ts @@ -70,6 +70,8 @@ export interface NewsItem { content: string titleImage: string slug: string + path: string + filename: string } export interface NewsProps { diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 093ca61..ec31bce 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -13,7 +13,7 @@ const nextConfig: NextConfig = { protocol: 'http', // для локал девеломпента hostname: '127.0.0.1', port: '8000', - pathname: '/media/uploads/**', + pathname: '/media/**', }, { protocol: 'https',