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 type { Metadata } from 'next'
|
||||||
import SearchCard from '../../components/SearchCard'
|
import SearchCard from '../../components/SearchCard'
|
||||||
import { SearchCardProps, RouteSearchPageProps } from '@/app/types'
|
import { SearchCardProps, RouteSearchPageProps } from '@/app/types'
|
||||||
|
import SearchFilters from '../../components/SearchFilters'
|
||||||
|
|
||||||
async function fetchSearch(category: string, from: string, to: string) {
|
async function fetchSearch(category: string, from: string, to: string) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -66,7 +67,7 @@ export default async function SearchPage(props: RouteSearchPageProps) {
|
|||||||
const { results, count } = await fetchSearch(category, fromCity, toCity)
|
const { results, count } = await fetchSearch(category, fromCity, toCity)
|
||||||
|
|
||||||
return (
|
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">
|
<h1 className="mb-4 text-2xl font-bold">
|
||||||
{results.length > 0
|
{results.length > 0
|
||||||
? category === 'mover'
|
? category === 'mover'
|
||||||
@@ -75,6 +76,8 @@ export default async function SearchPage(props: RouteSearchPageProps) {
|
|||||||
: 'Результаты не найдены'}
|
: 'Результаты не найдены'}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<SearchFilters />
|
||||||
|
|
||||||
<Suspense fallback={<div>Загрузка результатов...</div>}>
|
<Suspense fallback={<div>Загрузка результатов...</div>}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{results.length > 0 ? (
|
{results.length > 0 ? (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Metadata } from 'next'
|
|||||||
import SearchCard from '../components/SearchCard'
|
import SearchCard from '../components/SearchCard'
|
||||||
import { SearchCardProps, SearchPageProps } from '@/app/types'
|
import { SearchCardProps, SearchPageProps } from '@/app/types'
|
||||||
import { fetchRoutes } from '@/lib/search/fetchRoutes'
|
import { fetchRoutes } from '@/lib/search/fetchRoutes'
|
||||||
|
import SearchFilters from '../components/SearchFilters'
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
return {
|
return {
|
||||||
@@ -57,7 +58,7 @@ export default async function SearchPage(props: SearchPageProps) {
|
|||||||
<h1 className="mb-4 text-2xl font-bold">
|
<h1 className="mb-4 text-2xl font-bold">
|
||||||
{params.category === 'mover' ? 'Поиск перевозчика' : 'Поиск посылки'}
|
{params.category === 'mover' ? 'Поиск перевозчика' : 'Поиск посылки'}
|
||||||
</h1>
|
</h1>
|
||||||
|
<SearchFilters />
|
||||||
<Suspense fallback={<div>Загрузка результатов...</div>}>
|
<Suspense fallback={<div>Загрузка результатов...</div>}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{results.length > 0 ? (
|
{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 Select from 'react-select'
|
||||||
import { MultiSelectProps } from '@/app/types'
|
import { MultiSelectProps } from '@/app/types'
|
||||||
|
|
||||||
@@ -12,6 +14,12 @@ const MultiSelect = ({
|
|||||||
className = '',
|
className = '',
|
||||||
noOptionsMessage = 'Нет доступных опций',
|
noOptionsMessage = 'Нет доступных опций',
|
||||||
}: MultiSelectProps) => {
|
}: MultiSelectProps) => {
|
||||||
|
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPortalTarget(document.body)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{label && (
|
{label && (
|
||||||
@@ -103,7 +111,7 @@ const MultiSelect = ({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
menuPortalTarget={document.body}
|
menuPortalTarget={portalTarget}
|
||||||
menuPosition="fixed"
|
menuPosition="fixed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user