implement pagination
This commit is contained in:
@@ -233,7 +233,7 @@ class LeadViewSet(ViewSet):
|
|||||||
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
try:
|
try:
|
||||||
lead = serializer.save()
|
lead: Leads = serializer.save()
|
||||||
|
|
||||||
# собираем ответ с данными о заявке для фронта
|
# собираем ответ с данными о заявке для фронта
|
||||||
response_data = {
|
response_data = {
|
||||||
@@ -247,16 +247,31 @@ class LeadViewSet(ViewSet):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Тестовая отправка email
|
# отправляем емаил владельцу маршрута
|
||||||
try:
|
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(
|
email_result = send_email(
|
||||||
to=["timofey.syr1704@yandex.by"],
|
to=[route_owner_email],
|
||||||
subject="Тестовое письмо от TripWB",
|
subject=email_subject,
|
||||||
html_content="<h1>Тест отправки письма</h1><p>Если вы видите это сообщение, значит отправка работает!</p>"
|
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:
|
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)
|
return Response(response_data, status=status.HTTP_201_CREATED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
|
|
||||||
export default async function SearchPage(props: SearchPageProps) {
|
export default async function SearchPage(props: SearchPageProps) {
|
||||||
const params = await props.params
|
const params = await props.params
|
||||||
const { results, count } = await fetchRoutes(params.category || '')
|
const { results, count, next, previous } = await fetchRoutes(params.category || '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
@@ -63,7 +63,12 @@ export default async function SearchPage(props: SearchPageProps) {
|
|||||||
<AddressSelector is_search={true} />
|
<AddressSelector is_search={true} />
|
||||||
|
|
||||||
<Suspense fallback={<div>Загрузка результатов...</div>}>
|
<Suspense fallback={<div>Загрузка результатов...</div>}>
|
||||||
<ClientResults initialResults={results} />
|
<ClientResults
|
||||||
|
initialResults={results}
|
||||||
|
initialCount={count}
|
||||||
|
initialNext={next}
|
||||||
|
initialPrevious={previous}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,20 +4,40 @@ import React, { useState, useMemo } from 'react'
|
|||||||
import SearchCard from './SearchCard'
|
import SearchCard from './SearchCard'
|
||||||
import { SearchCardProps } from '@/app/types'
|
import { SearchCardProps } from '@/app/types'
|
||||||
import SearchFilters from './SearchFilters'
|
import SearchFilters from './SearchFilters'
|
||||||
|
import { fetchRoutes } from '@/lib/search/fetchRoutes'
|
||||||
|
import { usePathname, useSearchParams } from 'next/navigation'
|
||||||
|
import Pagination from './Pagination'
|
||||||
|
|
||||||
interface ClientResultsProps {
|
interface ClientResultsProps {
|
||||||
initialResults: SearchCardProps[]
|
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 [selectedTransport, setSelectedTransport] = useState<number[]>([])
|
||||||
const [selectedPackageTypes, setSelectedPackageTypes] = 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(() => {
|
const filteredResults = useMemo(() => {
|
||||||
let results = [...initialResults]
|
let filtered = [...results]
|
||||||
|
|
||||||
if (selectedTransport.length > 0) {
|
if (selectedTransport.length > 0) {
|
||||||
results = results.filter(item =>
|
filtered = filtered.filter(item =>
|
||||||
selectedTransport.some(id => {
|
selectedTransport.some(id => {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -34,7 +54,7 @@ export default function ClientResults({ initialResults }: ClientResultsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPackageTypes.length > 0) {
|
if (selectedPackageTypes.length > 0) {
|
||||||
results = results.filter(item => {
|
filtered = filtered.filter(item => {
|
||||||
return selectedPackageTypes.some(id => {
|
return selectedPackageTypes.some(id => {
|
||||||
const match = (() => {
|
const match = (() => {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
@@ -58,8 +78,8 @@ export default function ClientResults({ initialResults }: ClientResultsProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return filtered
|
||||||
}, [initialResults, selectedTransport, selectedPackageTypes])
|
}, [results, selectedTransport, selectedPackageTypes])
|
||||||
|
|
||||||
const handleFiltersChange = ({
|
const handleFiltersChange = ({
|
||||||
transport,
|
transport,
|
||||||
@@ -72,19 +92,61 @@ export default function ClientResults({ initialResults }: ClientResultsProps) {
|
|||||||
setSelectedPackageTypes(packageTypes)
|
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 (
|
return (
|
||||||
<>
|
<div className="space-y-6">
|
||||||
<SearchFilters onFiltersChange={handleFiltersChange} />
|
<SearchFilters onFiltersChange={handleFiltersChange} />
|
||||||
|
|
||||||
<div className="space-y-4">
|
{isLoading ? (
|
||||||
{filteredResults.length > 0 ? (
|
<div className="flex justify-center">
|
||||||
filteredResults.map((item: SearchCardProps) => <SearchCard key={item.id} {...item} />)
|
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-gray-900"></div>
|
||||||
) : (
|
</div>
|
||||||
<div className="rounded-lg bg-orange-50 p-4 text-center text-orange-800">
|
) : (
|
||||||
По выбранным фильтрам ничего не найдено. Попробуйте изменить параметры поиска.
|
<>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{filteredResults.map(result => (
|
||||||
|
<SearchCard key={result.id} {...result} />
|
||||||
|
))}
|
||||||
</div>
|
</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 {
|
export interface SearchResponse {
|
||||||
count: number
|
count: number
|
||||||
results: SearchCardProps[]
|
results: SearchCardProps[]
|
||||||
|
next: string | null
|
||||||
|
previous: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchPageProps {
|
export interface SearchPageProps {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const RouteForm: React.FC<RouteFormProps> = ({
|
|||||||
arrival: '',
|
arrival: '',
|
||||||
phone_number: user?.phone_number || '',
|
phone_number: user?.phone_number || '',
|
||||||
comment: '',
|
comment: '',
|
||||||
email_notification: false,
|
email_notification: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const cargoOptions: SelectOption[] = cargo_types.map((type, index) => ({
|
const cargoOptions: SelectOption[] = cargo_types.map((type, index) => ({
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { SearchResponse } from '@/app/types'
|
import { SearchResponse } from '@/app/types'
|
||||||
|
|
||||||
// получаем все предложения по выбранному owner_type
|
// получаем все предложения по выбранному 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 {
|
try {
|
||||||
|
const pageParam = page > 1 ? `${query ? '&' : '?'}page=${page}` : ''
|
||||||
const response = await fetch(
|
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',
|
cache: 'no-store',
|
||||||
}
|
}
|
||||||
@@ -15,9 +20,14 @@ export async function fetchRoutes(category: string, query: string = ''): Promise
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return data
|
return {
|
||||||
|
results: data.results,
|
||||||
|
count: data.count,
|
||||||
|
next: data.next,
|
||||||
|
previous: data.previous,
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching search results:', 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