search filter component

This commit is contained in:
2025-05-27 14:02:50 +03:00
parent 7915221fc4
commit c897d25a7d
5 changed files with 126 additions and 131 deletions

View File

@@ -1,127 +0,0 @@
name: tripwb
networks:
tripwb:
name: tripwb
driver: bridge
services:
caddy:
image: caddy:alpine
container_name: tripwb-caddy-server
depends_on:
tripwb-backend-app:
condition: service_healthy
ports:
- 80:80
- 443:443
volumes:
- caddy-config:/config
- caddy-data:/data
- ./Caddyfile:/etc/caddy/Caddyfile:ro
networks:
- tripwb
tripwb-frontend-app:
build:
context: frontend/
# image: tripwb-frontend
container_name: tripwb-frontend-app
environment:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
NEXTAUTH_URL: ${NEXTAUTH_URL}
BACKEND_URL: ${BACKEND_URL}
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
tripwb-backend-app:
condition: service_healthy
caddy:
condition: service_started
# ports:
# - 9000:3000
networks:
- tripwb
tripwb-backend-app:
build:
context: backend/
# image:tripwb-backend
container_name: tripwb-backend-app
environment:
SECRET_KEY: ${SECRET_KEY}
DEBUG_MODE: ${DEBUG_MODE}
API_KEY: ${API_KEY}
DB_USER: ${DB_USER}
DB_HOST: ${DB_HOST}
DB_NAME: ${DB_NAME}
DB_PASSWORD: ${DB_PASSWORD}
DB_PORT: ${DB_PORT}
healthcheck:
test: ['CMD', 'curl', '-s', '-o', '-f', 'http://tripwb-backend-app:8000']
# interval: 5s
timeout: 5s
retries: 5
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- tripwb-backend-app-uploads:/root/tripwb/uploads
# ports:
# - 8000:8000
networks:
- tripwb
# pgadmin-app:
# image: dpage/pgadmin4
# container_name: pgadmin-app
# environment:
# DB_PORT: ${DB_PORT}
# DB_HOST: ${DB_HOST}
# DB_NAME: ${DB_NAME}
# DB_PASSWORD: ${DB_PASSWORD}
# DB_USER: ${DB_USER}
# BOT_TOKEN: ${BOT_TOKEN}
# CHAT_ID: ${BOT_TOKEN}
# JWT_SECRET: ${BOT_TOKEN}
# NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
# PGADMIN_DEFAULT_EMAIL: timofey.syr17@gmail.com
# PGADMIN_DEFAULT_PASSWORD: passWoRd
# depends_on:
# postgres:
# condition: service_healthy
# ports:
# - 81:80
# networks:
# - tripwb
postgres:
image: postgres:alpine
restart: always
container_name: tripwb-db
env_file: backend/.env
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_NAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: [CMD-SHELL, "sh -c 'pg_isready -U ${DB_USER} -d ${DB_NAME}'"]
interval: 10s
timeout: 5s
retries: 5
# ports:
# - 5432:5432
volumes:
- pg-data:/var/lib/postgresql/data
networks:
- tripwb
volumes:
pg-data:
caddy-config:
caddy-data:
tripwb-backend-app-uploads:

View File

@@ -2,6 +2,7 @@ import React, { Suspense } from 'react'
import type { Metadata } from 'next'
import SearchCard from '../../components/SearchCard'
import { SearchCardProps, RouteSearchPageProps } from '@/app/types'
import SearchFilters from '../../components/SearchFilters'
async function fetchSearch(category: string, from: string, to: string) {
const response = await fetch(
@@ -66,7 +67,7 @@ export default async function SearchPage(props: RouteSearchPageProps) {
const { results, count } = await fetchSearch(category, fromCity, toCity)
return (
<div className="container mx-auto p-4">
<div className="container mx-auto space-y-10 p-4">
<h1 className="mb-4 text-2xl font-bold">
{results.length > 0
? category === 'mover'
@@ -75,6 +76,8 @@ export default async function SearchPage(props: RouteSearchPageProps) {
: 'Результаты не найдены'}
</h1>
<SearchFilters />
<Suspense fallback={<div>Загрузка результатов...</div>}>
<div className="space-y-4">
{results.length > 0 ? (

View File

@@ -3,6 +3,7 @@ import type { Metadata } from 'next'
import SearchCard from '../components/SearchCard'
import { SearchCardProps, SearchPageProps } from '@/app/types'
import { fetchRoutes } from '@/lib/search/fetchRoutes'
import SearchFilters from '../components/SearchFilters'
export async function generateMetadata(): Promise<Metadata> {
return {
@@ -57,7 +58,7 @@ export default async function SearchPage(props: SearchPageProps) {
<h1 className="mb-4 text-2xl font-bold">
{params.category === 'mover' ? 'Поиск перевозчика' : 'Поиск посылки'}
</h1>
<SearchFilters />
<Suspense fallback={<div>Загрузка результатов...</div>}>
<div className="space-y-4">
{results.length > 0 ? (

View File

@@ -0,0 +1,110 @@
'use client'
import React, { useState } from 'react'
import MultiSelect from '@/components/ui/Selector'
const transportOptions = [
{ id: 1, value: 'road', label: 'Авто' },
{ id: 2, value: 'avia', label: 'Авиа' },
{ id: 3, value: 'both', label: 'Любой' },
]
const packageTypeOptions = [
{ id: 1, value: 'letter', label: 'Письмо или Документы' },
{ id: 2, value: 'package', label: 'Посылка (до 30кг)' },
{ id: 3, value: 'passenger', label: 'Попутчик' },
{ id: 4, value: 'parcel', label: 'Бандероль (до 5кг)' },
{ id: 5, value: 'cargo', label: 'Груз (свыше 30 кг)' },
]
interface SearchFiltersProps {
onFiltersChange?: (filters: { transport: number[]; packageTypes: number[] }) => void
}
const SearchFilters: React.FC<SearchFiltersProps> = ({ onFiltersChange }) => {
const [selectedTransport, setSelectedTransport] = useState<number[]>([])
const [selectedPackageTypes, setSelectedPackageTypes] = useState<number[]>([])
const handleTransportChange = (e: { target: { value: number[] } }) => {
setSelectedTransport(e.target.value)
onFiltersChange?.({
transport: e.target.value,
packageTypes: selectedPackageTypes,
})
}
const handlePackageTypesChange = (e: { target: { value: number[] } }) => {
setSelectedPackageTypes(e.target.value)
onFiltersChange?.({
transport: selectedTransport,
packageTypes: e.target.value,
})
}
const handleReset = () => {
setSelectedTransport([])
setSelectedPackageTypes([])
onFiltersChange?.({
transport: [],
packageTypes: [],
})
}
const hasActiveFilters = selectedTransport.length > 0 || selectedPackageTypes.length > 0
return (
<div className="w-full rounded-2xl bg-white p-6 shadow-sm">
<div className="mb-6 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Ищете что-то конкретное?</h2>
{hasActiveFilters && (
<button
onClick={handleReset}
className="hover:text-orange/80 flex items-center text-sm font-medium text-blue-600"
>
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Сбросить фильтры
</button>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<MultiSelect
value={selectedTransport}
handleChange={handleTransportChange}
label="Способ перевозки"
placeholder="Выберите способ перевозки"
name="transport"
options={transportOptions}
className="w-full"
/>
</div>
<div className="space-y-2">
<MultiSelect
value={selectedPackageTypes}
handleChange={handlePackageTypesChange}
label="Тип посылки"
placeholder="Выберите тип посылки"
name="packageType"
options={packageTypeOptions}
className="w-full"
/>
</div>
</div>
</div>
)
}
export default SearchFilters

View File

@@ -1,4 +1,6 @@
import React from 'react'
'use client'
import React, { useState, useEffect } from 'react'
import Select from 'react-select'
import { MultiSelectProps } from '@/app/types'
@@ -12,6 +14,12 @@ const MultiSelect = ({
className = '',
noOptionsMessage = 'Нет доступных опций',
}: MultiSelectProps) => {
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null)
useEffect(() => {
setPortalTarget(document.body)
}, [])
return (
<div className={className}>
{label && (
@@ -103,7 +111,7 @@ const MultiSelect = ({
},
}),
}}
menuPortalTarget={document.body}
menuPortalTarget={portalTarget}
menuPosition="fixed"
/>
</div>