implement pagination
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
91
frontend/app/(urls)/search/components/Pagination.tsx
Normal file
91
frontend/app/(urls)/search/components/Pagination.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -235,6 +235,8 @@ export interface PricingCardProps {
|
||||
export interface SearchResponse {
|
||||
count: number
|
||||
results: SearchCardProps[]
|
||||
next: string | null
|
||||
previous: string | null
|
||||
}
|
||||
|
||||
export interface SearchPageProps {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user