search filter component
This commit is contained in:
@@ -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:
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
110
frontend/app/(urls)/search/components/SearchFilters.tsx
Normal file
110
frontend/app/(urls)/search/components/SearchFilters.tsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user