implement pagination

This commit is contained in:
2025-05-28 12:30:08 +03:00
parent 9eaaff9eb2
commit a673210f9f
7 changed files with 215 additions and 30 deletions

View File

@@ -233,7 +233,7 @@ class LeadViewSet(ViewSet):
if is_valid:
try:
lead = serializer.save()
lead: Leads = serializer.save()
# собираем ответ с данными о заявке для фронта
response_data = {
@@ -247,16 +247,31 @@ class LeadViewSet(ViewSet):
}
}
# Тестовая отправка email
# отправляем емаил владельцу маршрута
try:
route_owner_email = route.owner.email
email_subject = f"Новая заявка на перевозку #{lead.id}"
email_content = f"""
<h2>Получена новая заявка на перевозку</h2>
<p>Детали заявки:</p>
<ul>
<li>Номер заявки: {lead.id}</li>
<li>Маршрут: {route.id}</li>
<li>Предложенная цена: {lead.moving_price}</li>
<li>Дата перевозки: {lead.moving_date}</li>
</ul>
"""
email_result = send_email(
to=["timofey.syr1704@yandex.by"],
subject="Тестовое письмо от TripWB",
html_content="<h1>Тест отправки письма</h1><p>Если вы видите это сообщение, значит отправка работает!</p>"
to=[route_owner_email],
subject=email_subject,
html_content=email_content
)
print(f"Email sending result: {email_result}")
if "Ошибка" in email_result:
print(f"Warning: Failed to send email notification: {email_result}")
except Exception as email_error:
print(f"Error sending email: {str(email_error)}")
print(f"Warning: Error while sending email notification: {str(email_error)}")
return Response(response_data, status=status.HTTP_201_CREATED)
except Exception as e:

View File

@@ -51,7 +51,7 @@ export async function generateMetadata(): Promise<Metadata> {
export default async function SearchPage(props: SearchPageProps) {
const params = await props.params
const { results, count } = await fetchRoutes(params.category || '')
const { results, count, next, previous } = await fetchRoutes(params.category || '')
return (
<div className="container mx-auto p-4">
@@ -63,7 +63,12 @@ export default async function SearchPage(props: SearchPageProps) {
<AddressSelector is_search={true} />
<Suspense fallback={<div>Загрузка результатов...</div>}>
<ClientResults initialResults={results} />
<ClientResults
initialResults={results}
initialCount={count}
initialNext={next}
initialPrevious={previous}
/>
</Suspense>
</div>
)

View File

@@ -4,20 +4,40 @@ import React, { useState, useMemo } from 'react'
import SearchCard from './SearchCard'
import { SearchCardProps } from '@/app/types'
import SearchFilters from './SearchFilters'
import { fetchRoutes } from '@/lib/search/fetchRoutes'
import { usePathname, useSearchParams } from 'next/navigation'
import Pagination from './Pagination'
interface ClientResultsProps {
initialResults: SearchCardProps[]
initialCount?: number
initialNext?: string | null
initialPrevious?: string | null
}
export default function ClientResults({ initialResults }: ClientResultsProps) {
export default function ClientResults({
initialResults,
initialCount = 0,
initialNext = null,
initialPrevious = null,
}: ClientResultsProps) {
const [selectedTransport, setSelectedTransport] = useState<number[]>([])
const [selectedPackageTypes, setSelectedPackageTypes] = useState<number[]>([])
const [results, setResults] = useState(initialResults)
const [isLoading, setIsLoading] = useState(false)
const [currentPage, setCurrentPage] = useState(1)
const [hasNext, setHasNext] = useState(!!initialNext)
const [hasPrevious, setHasPrevious] = useState(!!initialPrevious)
const [totalCount, setTotalCount] = useState(initialCount)
const pathname = usePathname()
const searchParams = useSearchParams()
const filteredResults = useMemo(() => {
let results = [...initialResults]
let filtered = [...results]
if (selectedTransport.length > 0) {
results = results.filter(item =>
filtered = filtered.filter(item =>
selectedTransport.some(id => {
switch (id) {
case 1:
@@ -34,7 +54,7 @@ export default function ClientResults({ initialResults }: ClientResultsProps) {
}
if (selectedPackageTypes.length > 0) {
results = results.filter(item => {
filtered = filtered.filter(item => {
return selectedPackageTypes.some(id => {
const match = (() => {
switch (id) {
@@ -58,8 +78,8 @@ export default function ClientResults({ initialResults }: ClientResultsProps) {
})
}
return results
}, [initialResults, selectedTransport, selectedPackageTypes])
return filtered
}, [results, selectedTransport, selectedPackageTypes])
const handleFiltersChange = ({
transport,
@@ -72,19 +92,61 @@ export default function ClientResults({ initialResults }: ClientResultsProps) {
setSelectedPackageTypes(packageTypes)
}
const handlePageChange = async (newPage: number) => {
setIsLoading(true)
try {
const category = pathname.split('/')[2] // получаем категорию из URL
const params = new URLSearchParams(searchParams.toString())
const {
results: newResults,
next,
previous,
count,
} = await fetchRoutes(category, params.toString(), newPage)
setResults(newResults)
setCurrentPage(newPage)
setHasNext(!!next)
setHasPrevious(!!previous)
setTotalCount(count)
} catch (error) {
console.error('Error fetching page:', error)
} finally {
setIsLoading(false)
}
}
const totalPages = Math.ceil(totalCount / 10) // 10 - размер страницы
return (
<>
<div className="space-y-6">
<SearchFilters onFiltersChange={handleFiltersChange} />
<div className="space-y-4">
{filteredResults.length > 0 ? (
filteredResults.map((item: SearchCardProps) => <SearchCard key={item.id} {...item} />)
) : (
<div className="rounded-lg bg-orange-50 p-4 text-center text-orange-800">
По выбранным фильтрам ничего не найдено. Попробуйте изменить параметры поиска.
{isLoading ? (
<div className="flex justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-gray-900"></div>
</div>
) : (
<>
<div className="grid gap-4">
{filteredResults.map(result => (
<SearchCard key={result.id} {...result} />
))}
</div>
)}
</div>
</>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
hasNext={hasNext}
hasPrevious={hasPrevious}
isLoading={isLoading}
onPageChange={handlePageChange}
/>
{filteredResults.length === 0 && (
<div className="text-center text-gray-500">Результаты не найдены</div>
)}
</>
)}
</div>
)
}

View File

@@ -0,0 +1,91 @@
'use client'
import React from 'react'
interface PaginationProps {
currentPage: number
totalPages: number
hasNext: boolean
hasPrevious: boolean
isLoading: boolean
onPageChange: (page: number) => void
}
export default function Pagination({
currentPage,
totalPages,
isLoading,
onPageChange,
}: PaginationProps) {
if (totalPages <= 1) return null
const renderPageNumbers = () => {
const pages = []
const maxVisiblePages = 5 // максимальное количество видимых страниц
// создаем кнопки страницы
const createPageButton = (pageNum: number) => (
<button
key={pageNum}
onClick={() => onPageChange(pageNum)}
disabled={isLoading || pageNum === currentPage}
className={`h-8 w-8 rounded-full ${
pageNum === currentPage
? 'bg-orange/80 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100'
} flex items-center justify-center border text-sm font-medium transition-colors`}
>
{pageNum}
</button>
)
// всегда показываем первую страницу
pages.push(createPageButton(1))
if (totalPages <= maxVisiblePages) {
// если страниц мало, показываем все
for (let i = 2; i <= totalPages; i++) {
pages.push(createPageButton(i))
}
} else {
// если страниц много, показываем с многоточием
if (currentPage > 3) {
pages.push(
<span key="start-ellipsis" className="px-2">
...
</span>
)
}
// показываем страницы вокруг текущей
for (
let i = Math.max(2, currentPage - 1);
i <= Math.min(totalPages - 1, currentPage + 1);
i++
) {
pages.push(createPageButton(i))
}
if (currentPage < totalPages - 2) {
pages.push(
<span key="end-ellipsis" className="px-2">
...
</span>
)
}
// всегда показываем последнюю страницу
if (totalPages > 1) {
pages.push(createPageButton(totalPages))
}
}
return pages
}
return (
<div className="mt-6 flex items-center justify-center">
<div className="flex space-x-2">{renderPageNumbers()}</div>
</div>
)
}

View File

@@ -235,6 +235,8 @@ export interface PricingCardProps {
export interface SearchResponse {
count: number
results: SearchCardProps[]
next: string | null
previous: string | null
}
export interface SearchPageProps {

View File

@@ -80,7 +80,7 @@ const RouteForm: React.FC<RouteFormProps> = ({
arrival: '',
phone_number: user?.phone_number || '',
comment: '',
email_notification: false,
email_notification: true,
}
const cargoOptions: SelectOption[] = cargo_types.map((type, index) => ({

View File

@@ -1,10 +1,15 @@
import { SearchResponse } from '@/app/types'
// получаем все предложения по выбранному owner_type
export async function fetchRoutes(category: string, query: string = ''): Promise<SearchResponse> {
export async function fetchRoutes(
category: string,
query: string = '',
page: number = 1
): Promise<SearchResponse> {
try {
const pageParam = page > 1 ? `${query ? '&' : '?'}page=${page}` : ''
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/search/${category}/${query ? `?${query}` : ''}`,
`${process.env.NEXT_PUBLIC_API_URL}/search/${category}/${query}${pageParam}`,
{
cache: 'no-store',
}
@@ -15,9 +20,14 @@ export async function fetchRoutes(category: string, query: string = ''): Promise
}
const data = await response.json()
return data
return {
results: data.results,
count: data.count,
next: data.next,
previous: data.previous,
}
} catch (error) {
console.error('Error fetching search results:', error)
return { results: [], count: 0 }
return { results: [], count: 0, next: null, previous: null }
}
}