dynamic routes for main page

This commit is contained in:
2025-05-23 11:48:45 +03:00
parent efccb591ff
commit 45e5e78df2
10 changed files with 234 additions and 141 deletions

View File

@@ -1,9 +1,7 @@
from rest_framework import serializers
from routes.models import Route
from sitemanagement.models import FAQ, News
from django.conf import settings
import pytz
from routes.constants.routeChoices import cargo_type_choices, type_transport_choices
from routes.models import Country
from api.account.client.serializers import RouteSerializer
class FAQMainSerializer(serializers.ModelSerializer):
class Meta:
@@ -37,4 +35,88 @@ class TelegramSerializer(serializers.Serializer):
def create(self, validated_data):
return type('TelegramMessage', (), validated_data)
class HomePageRouteSerializer(RouteSerializer):
username = serializers.SerializerMethodField()
userImg = serializers.SerializerMethodField()
start_point = serializers.SerializerMethodField()
country_from = serializers.SerializerMethodField()
country_from_icon = serializers.SerializerMethodField()
country_from_code = serializers.SerializerMethodField()
end_point = serializers.SerializerMethodField()
country_to = serializers.SerializerMethodField()
country_to_icon = serializers.SerializerMethodField()
country_to_code = serializers.SerializerMethodField()
cargo_type = serializers.SerializerMethodField()
user_request = serializers.SerializerMethodField()
user_comment = serializers.CharField(source='comment')
moving_type = serializers.SerializerMethodField()
estimated_date = serializers.SerializerMethodField()
day_out = serializers.DateTimeField(source='departure_DT')
day_in = serializers.DateTimeField(source='arrival_DT')
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_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_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_from_code(self, obj):
country = self.get_from_country_name(obj)
return country[:3].upper() if country else None
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_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:
print(f"Country not found: {country}")
return None
def get_country_to_code(self, obj):
country = self.get_to_country_name(obj)
return country[:3].upper() if country else None
def get_cargo_type(self, obj):
return self.get_formatted_cargo_type(obj)
def get_user_request(self, obj):
return 'Нужен перевозчик' if obj.owner_type == 'customer' else 'Могу перевезти'
def get_moving_type(self, obj):
return self.get_formatted_transport(obj)
def get_estimated_date(self, obj):
return obj.arrival_DT

View File

@@ -4,12 +4,10 @@ from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from api.utils.decorators import handle_exceptions
from django.db.models import Q
from routes.models import Route
from routes.constants.routeChoices import owner_type_choices
from api.main.serializers import FAQMainSerializer, NewsMainSerializer, TelegramSerializer
from api.account.client.serializers import RouteSerializer
from api.main.serializers import FAQMainSerializer, NewsMainSerializer, TelegramSerializer, HomePageRouteSerializer
from sitemanagement.models import FAQ, News
class FAQView(APIView):
@@ -39,16 +37,20 @@ class NewsView(APIView):
class LatestRoutesView(APIView):
@handle_exceptions
def get(self, request):
"""Получаем последние 5 маршрутов для каждого типа owner_type"""
"""Получаем последние маршруты"""
latest_routes = {}
routes = []
owner_types = dict(owner_type_choices).keys()
for owner_type in owner_types:
routes = Route.objects.filter(owner_type=owner_type).order_by('-id')[:5]
latest_routes[owner_type] = RouteSerializer(routes, many=True).data
routes.extend(
HomePageRouteSerializer(
Route.objects.filter(owner_type=owner_type).order_by('-id')[:5],
many=True
).data
)
return Response(latest_routes, status=status.HTTP_200_OK)
return Response(routes, status=status.HTTP_200_OK)
class TelegramMessageView(APIView):
@handle_exceptions

View File

@@ -1,42 +1,43 @@
import React from 'react'
import Image from 'next/image'
import Button from '@/components/ui/Button'
import { SearchCardProps } from '@/app/types/index'
import { SearchCardProps } from '@/app/types'
import noPhoto from '../../../../public/images/noPhoto.png'
const SearchCard = ({
id,
username,
owner_type,
from_city_name,
from_country_name,
to_city_name,
to_country_name,
formatted_cargo_type,
formatted_transport,
type_transport,
userImg,
start_point,
country_from,
comment,
formatted_departure,
formatted_arrival,
country_from_icon,
country_from_code,
end_point,
country_to,
country_to_icon,
country_to_code,
cargo_type,
user_request,
user_comment,
moving_type,
estimated_date,
day_out,
day_in,
}: SearchCardProps) => {
const getUserRequestStyles = () => {
if (user_request === 'Нужен перевозчик') {
if (owner_type === 'customer') {
return 'text-[#065bff]'
}
return 'text-[#45c226]'
}
const setMovingTypeIcon = () => {
if (moving_type === 'Авиатранспорт') {
if (type_transport === 'air') {
return '/images/airplane.png'
}
return '/images/car.png'
}
const userRequest = owner_type === 'customer' ? 'Нужен перевозчик' : 'Могу перевезти'
return (
<>
{/* десктоп */}
@@ -46,21 +47,22 @@ const SearchCard = ({
<div className="flex items-center gap-5">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-200">
<Image
src={userImg}
alt={username}
src={userImg || noPhoto}
alt={`User ${username}`}
width={52}
height={52}
className="rounded-full object-cover"
className="aspect-square w-full rounded-full object-cover md:w-[84px]"
/>
</div>
<div className="flex items-center gap-3">
<div className="text-base font-semibold">{username}</div>
<div className="text-gray-500">|</div>
<div className={`text-base font-semibold ${getUserRequestStyles()}`}>
{user_request}
{userRequest}
</div>
<div className="ml-1">
Тип посылки: <span className="text-orange ml-1 font-semibold">{cargo_type}</span>
Тип посылки:{' '}
<span className="text-orange ml-1 font-semibold">{formatted_cargo_type}</span>
</div>
</div>
</div>
@@ -72,30 +74,37 @@ const SearchCard = ({
<div className="rounded-lg bg-[#f8f8f8] p-5">
<div className="flex items-baseline gap-2">
<span className="text-gray-600">{user_comment}</span>
<span className="text-gray-600">{comment}</span>
</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">
<div className="flex flex-col">
{user_request === 'Нужен перевозчик' ? (
{userRequest === 'Нужен перевозчик' ? (
<span className="text-gray-500">Забрать из:</span>
) : (
<span className="text-gray-500">Выезжаю из:</span>
)}
<div className="flex flex-col">
<div className="flex items-center">
<Image src={country_from_icon} width={26} height={13} alt={country_from_code} />
<span className="pr-2 pl-1 text-gray-400">{country_from_code}</span>
<Image
src={country_from_icon}
width={26}
height={13}
alt={from_country_name.substring(0, 3)}
/>
<span className="pr-2 pl-1 text-gray-400">
{from_country_name.substring(0, 3).toUpperCase()}
</span>
<span className="text-base font-semibold">
{start_point} / {country_from}
{from_city_name} / {from_country_name}
</span>
</div>
{user_request === 'Могу перевезти' && (
{userRequest === 'Могу перевезти' && (
<div className="mt-1 text-sm text-gray-500">
<span className="text-sm font-normal">Отправление:</span>{' '}
<span className="text-sm font-semibold">{day_out?.toLocaleDateString()}</span>
<span className="text-sm font-semibold">{formatted_departure}</span>
</div>
)}
</div>
@@ -103,7 +112,7 @@ const SearchCard = ({
<div className="text-center">
<div className="flex items-center justify-center gap-2">
<span className="text-base font-semibold">{moving_type}</span>
<span className="text-base font-semibold">{formatted_transport}</span>
<Image
src={setMovingTypeIcon()}
width={15}
@@ -124,18 +133,16 @@ const SearchCard = ({
<div className="relative z-10 h-5 w-5 rounded-full border-3 border-[#45c226] bg-white" />
</div>
{user_request === 'Нужен перевозчик' && (
{userRequest === 'Нужен перевозчик' && (
<div className="text-sm text-gray-500">
<span className="text-sm font-normal">Дата доставки:</span>{' '}
<span className="text-sm font-semibold">
{estimated_date.toLocaleDateString()}
</span>
<span className="text-sm font-semibold">{formatted_arrival}</span>
</div>
)}
</div>
<div className="-mb-[14px] flex flex-col">
{user_request === 'Нужен перевозчик' ? (
{userRequest === 'Нужен перевозчик' ? (
<div className="text-base text-gray-500">Доставить в:</div>
) : (
<div className="text-base text-gray-500">Прибываю в:</div>
@@ -143,16 +150,23 @@ const SearchCard = ({
<div className="flex flex-col">
<div className="flex items-center">
<Image src={country_to_icon} width={26} height={13} alt={country_to_code} />
<span className="pr-2 pl-1 text-gray-400">{country_to_code}</span>
<Image
src={country_to_icon}
width={26}
height={13}
alt={to_country_name.substring(0, 3)}
/>
<span className="pr-2 pl-1 text-gray-400">
{to_country_name.substring(0, 3).toUpperCase()}
</span>
<span className="text-base font-semibold">
{end_point} / {country_to}
{to_city_name} / {to_country_name}
</span>
</div>
{user_request === 'Могу перевезти' && (
{userRequest === 'Могу перевезти' && (
<div className="text-sm text-gray-500">
<span className="text-sm font-normal">Прибытие:</span>{' '}
<span className="text-sm font-semibold">{day_in?.toLocaleDateString()}</span>
<span className="text-sm font-semibold">{formatted_arrival}</span>
</div>
)}
</div>
@@ -165,28 +179,26 @@ const SearchCard = ({
<div className="block sm:hidden">
<div className="my-4 w-full rounded-xl bg-white p-4 shadow-lg">
<div className="flex items-center justify-between">
<div className={`text-sm font-semibold ${getUserRequestStyles()}`}>{user_request}</div>
<div className={`text-sm font-semibold ${getUserRequestStyles()}`}>{userRequest}</div>
<div className="text-sm font-semibold">
Тип посылки: <span className="text-orange">{cargo_type}</span>
Тип посылки: <span className="text-orange">{formatted_cargo_type}</span>
</div>
</div>
<div className="mt-5 mb-2 flex flex-row items-center justify-between gap-3">
<div className="flex h-16 w-16 min-w-[64px] shrink-0 items-center justify-center rounded-full bg-gray-200">
<Image
src={userImg}
alt={username}
src={noPhoto}
alt={`User ${username}`}
width={52}
height={52}
className="aspect-square h-[52px] w-[52px] rounded-full object-cover"
/>
</div>
<div className="flex-1 rounded-lg bg-[#f8f8f8] p-4 text-sm font-normal">
{user_comment}
</div>
<div className="flex-1 rounded-lg bg-[#f8f8f8] p-4 text-sm font-normal">{comment}</div>
</div>
<div className="flex justify-end text-xs text-gray-500">Объявление {id}</div>
{user_request === 'Нужен перевозчик' ? (
{userRequest === 'Нужен перевозчик' ? (
<span className="pl-7 text-sm text-gray-500">Забрать из:</span>
) : (
<span className="pl-7 text-sm text-gray-500">Выезжаю из:</span>
@@ -207,16 +219,23 @@ const SearchCard = ({
</div>
<div className="flex flex-1 flex-col justify-between">
<div className="-mt-[14px] flex items-center">
<Image src={country_from_icon} width={26} height={13} alt={country_from_code} />
<span className="pr-2 pl-1 text-sm text-gray-400">{country_from_code}</span>
<Image
src={`/images/flags/${from_country_name.toLowerCase()}.png`}
width={26}
height={13}
alt={from_country_name.substring(0, 3)}
/>
<span className="pr-2 pl-1 text-sm text-gray-400">
{from_country_name.substring(0, 3).toUpperCase()}
</span>
<span className="text-base font-semibold">
{start_point} / {country_from}
{from_city_name} / {from_country_name}
</span>
</div>
<div className="flex flex-col">
<div className="flex items-center gap-4">
<span className="text-base">{moving_type}</span>
<span className="text-base">{formatted_transport}</span>
<Image
src={setMovingTypeIcon()}
width={15}
@@ -226,30 +245,37 @@ const SearchCard = ({
/>
</div>
<div className="my-2 h-[2px] w-[165px] bg-gray-200" />
<div className="text-sm">Дата доставки: {estimated_date.toLocaleDateString()}</div>
<div className="text-sm">Дата доставки: {formatted_arrival}</div>
</div>
<div className="-mb-[14px] flex flex-col">
{user_request === 'Нужен перевозчик' ? (
{userRequest === 'Нужен перевозчик' ? (
<div className="text-sm text-gray-500">Доставить в:</div>
) : (
<div className="text-sm text-gray-500">Прибываю в:</div>
)}
<div className="flex items-center">
<Image src={country_to_icon} width={26} height={13} alt={country_to_code} />
<span className="pr-2 pl-1 text-gray-400">{country_to_code}</span>
<Image
src={`/images/flags/${to_country_name.toLowerCase()}.png`}
width={26}
height={13}
alt={to_country_name.substring(0, 3)}
/>
<span className="pr-2 pl-1 text-gray-400">
{to_country_name.substring(0, 3).toUpperCase()}
</span>
<span className="text-base font-semibold">
{end_point} / {country_to}
{to_city_name} / {to_country_name}
</span>
</div>
</div>
</div>
</div>
{user_request === 'Могу перевезти' && (
{userRequest === 'Могу перевезти' && (
<div className="mt-3 ml-7 text-sm text-gray-500">
<span className="text-sm font-normal">Прибытие:</span>{' '}
<span className="text-sm font-semibold">{day_in?.toLocaleDateString()}</span>
<span className="text-sm font-semibold">{formatted_arrival}</span>
</div>
)}
</div>

View File

@@ -1,55 +1,7 @@
import avatar from '../../public/images/avatar.png'
import belarusIcon from '../../public/images/belarus.png'
import russiaIcon from '../../public/images/russia.png'
import { CargoType, TransportType } from '../types'
const userImg = avatar
const blIcon = belarusIcon
const ruIcon = russiaIcon
export const routes = 12845
export const data = [
{
id: 1123,
username: 'John Doe',
userImg: userImg,
start_point: 'Минск',
country_from: 'Беларусь',
end_point: 'Москва',
country_to: 'Россия',
cargo_type: 'Документы',
user_request: 'Нужен перевозчик',
user_comment: 'Нужно перевезти документы из Минска в Москву',
country_from_icon: blIcon,
country_to_icon: ruIcon,
country_from_code: 'BY',
country_to_code: 'RU',
moving_type: 'Авиатранспорт',
estimated_date: new Date(2025, 4, 15),
},
{
id: 2423,
username: 'John Doe',
userImg: userImg,
start_point: 'Минск',
country_from: 'Беларусь',
end_point: 'Москва',
country_to: 'Россия',
cargo_type: 'Документы',
user_request: 'Могу перевезти',
user_comment: 'Нужно перевезти документы из Минска в Москву',
moving_type: 'Автоперевозка',
estimated_date: new Date(2025, 5, 18),
country_from_icon: blIcon,
country_to_icon: ruIcon,
country_from_code: 'BY',
country_to_code: 'RU',
day_out: new Date(2025, 5, 21),
day_in: new Date(2025, 5, 25),
},
]
export const cargo_types: CargoType[] = ['letter', 'package', 'passenger', 'parcel', 'cargo']
export const cargo_type_translations: Record<CargoType, string> = {

View File

@@ -3,16 +3,17 @@ import Image from 'next/image'
import AddressSelector from '@/components/AddressSelector'
import SearchCard from '@/app/(urls)/search/components/SearchCard'
import FAQ from '@/components/FAQ'
import { data } from '@/app/constants'
import { routes } from '@/app/constants'
import Button from '@/components/ui/Button'
import News from '@/components/News'
import { getFAQs } from '@/lib/main/fetchFAQ'
import { getNews } from '@/lib/main/fetchNews'
import { getFirstRoutes } from '@/lib/main/fetchFirstRoutes'
export default async function Home() {
const faqs = await getFAQs()
const news = await getNews()
const latestRoutes = await getFirstRoutes()
return (
<div className="mx-auto flex max-w-[93%] flex-col items-center justify-center">
@@ -69,9 +70,11 @@ export default async function Home() {
{/* первые пять серч карточек -- бекенд??? */}
<div className="mx-auto w-full max-w-[1250px]">
<div className="grid w-full grid-cols-1 gap-4">
{data.map(card => (
<SearchCard key={card.id} {...card} />
))}
{Array.isArray(latestRoutes) && latestRoutes.length > 0 ? (
latestRoutes.map(card => <SearchCard key={card.id} {...card} />)
) : (
<div className="py-4 text-center text-gray-600">Нет доступных маршрутов</div>
)}
</div>
<div className="flex justify-center py-4">
<Button

View File

@@ -29,23 +29,24 @@ export interface ButtonProps {
export interface SearchCardProps {
id: number
username: string
userImg: string | StaticImageData
start_point: string
country_from: string
country_from_icon: string | StaticImageData
country_from_code: string
end_point: string
country_to: string
country_to_icon: string | StaticImageData
country_to_code: string
username: number
userImg: string
owner_type: string
from_city_name: string
from_country_name: string
to_city_name: string
to_country_name: string
cargo_type: string
user_request: string
moving_type: string
estimated_date: Date
user_comment: string
day_out?: Date
day_in?: Date
formatted_cargo_type: string
formatted_transport: string
type_transport: string
comment: string
departure_DT: string
arrival_DT: string
formatted_departure: string
formatted_arrival: string
country_from_icon: string
country_to_icon: string
}
export interface AccordionProps {

View File

@@ -0,0 +1,24 @@
import { SearchCardProps } from '@/app/types'
export async function getFirstRoutes(): Promise<SearchCardProps[]> {
const API_URL = process.env.NEXT_PUBLIC_API_URL
try {
const response = await fetch(`${API_URL}/latest-routes/`, {
next: {
revalidate: 86400, // один день
},
})
if (!response.ok) {
console.error('Failed to fetch latest routes:', response.statusText)
return []
}
const routes = (await response.json()) as SearchCardProps[]
return Array.isArray(routes) ? routes : []
} catch (error) {
console.error('Error fetching latest routes:', error)
return []
}
}

View File

@@ -1,7 +1,6 @@
import type { NextConfig } from 'next'
const API_URL =
process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8000/api/v1'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8000/api/v1'
const nextConfig: NextConfig = {
images: {
@@ -20,6 +19,10 @@ const nextConfig: NextConfig = {
protocol: 'https',
hostname: 'tripwb.com',
},
{
protocol: 'https',
hostname: 'i.ibb.co',
},
],
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB