Каталог, интеграция
This commit is contained in:
parent
7181718b0d
commit
7f6c6d1107
|
|
@ -16,3 +16,8 @@ interface MainPageCatalogResponseDto {
|
|||
featuredYacht: CatalogItemDto;
|
||||
restYachts: CatalogItemDto[];
|
||||
}
|
||||
|
||||
interface CatalogFilteredResponseDto {
|
||||
items: CatalogItemDto[];
|
||||
total: number;
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
|
|
@ -10,191 +8,37 @@ import { Button } from "@/components/ui/button";
|
|||
import { DatePicker } from "@/components/ui/date-picker";
|
||||
import { GuestPicker } from "@/components/form/guest-picker";
|
||||
import Icon from "@/components/ui/icon";
|
||||
import { CatalogFilters, defaultFilters } from "@/app/catalog/page";
|
||||
|
||||
interface CatalogSidebarProps {
|
||||
filters: CatalogFilters;
|
||||
setFilters: (filters: CatalogFilters | ((prev: CatalogFilters) => CatalogFilters)) => void;
|
||||
onApply?: () => void;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [lengthRange, setLengthRange] = useState([7, 50]);
|
||||
const [priceRange, setPriceRange] = useState([3000, 200000]);
|
||||
const [yearRange, setYearRange] = useState([1991, 2025]);
|
||||
const [adults, setAdults] = useState<number>(0);
|
||||
const [children, setChildren] = useState<number>(0);
|
||||
const [paymentType, setPaymentType] = useState("all");
|
||||
const [quickBooking, setQuickBooking] = useState(false);
|
||||
const [hasToilet, setHasToilet] = useState(false);
|
||||
const [vesselType, setVesselType] = useState("");
|
||||
const [date, setDate] = useState<Date | null>(null);
|
||||
const [departureTime, setDepartureTime] = useState("12:00");
|
||||
const [arrivalTime, setArrivalTime] = useState("13:00");
|
||||
|
||||
// Загрузка фильтров из searchParams при монтировании и изменении URL
|
||||
useEffect(() => {
|
||||
const lengthMin = searchParams.get("lengthMin");
|
||||
const lengthMax = searchParams.get("lengthMax");
|
||||
if (lengthMin && lengthMax) {
|
||||
setLengthRange([parseInt(lengthMin), parseInt(lengthMax)]);
|
||||
} else {
|
||||
setLengthRange([7, 50]);
|
||||
}
|
||||
|
||||
const priceMin = searchParams.get("priceMin");
|
||||
const priceMax = searchParams.get("priceMax");
|
||||
if (priceMin && priceMax) {
|
||||
setPriceRange([parseInt(priceMin), parseInt(priceMax)]);
|
||||
} else {
|
||||
setPriceRange([3000, 200000]);
|
||||
}
|
||||
|
||||
const yearMin = searchParams.get("yearMin");
|
||||
const yearMax = searchParams.get("yearMax");
|
||||
if (yearMin && yearMax) {
|
||||
setYearRange([parseInt(yearMin), parseInt(yearMax)]);
|
||||
} else {
|
||||
setYearRange([1991, 2025]);
|
||||
}
|
||||
|
||||
const adultsParam = searchParams.get("adults");
|
||||
setAdults(adultsParam ? parseInt(adultsParam) : 0);
|
||||
|
||||
const childrenParam = searchParams.get("children");
|
||||
setChildren(childrenParam ? parseInt(childrenParam) : 0);
|
||||
|
||||
const paymentTypeParam = searchParams.get("paymentType");
|
||||
setPaymentType(paymentTypeParam || "all");
|
||||
|
||||
const quickBookingParam = searchParams.get("quickBooking");
|
||||
setQuickBooking(quickBookingParam === "true");
|
||||
|
||||
const hasToiletParam = searchParams.get("hasToilet");
|
||||
setHasToilet(hasToiletParam === "true");
|
||||
|
||||
const vesselTypeParam = searchParams.get("vesselType");
|
||||
setVesselType(vesselTypeParam || "");
|
||||
|
||||
const dateParam = searchParams.get("date");
|
||||
if (dateParam) {
|
||||
const parsedDate = new Date(dateParam);
|
||||
if (!isNaN(parsedDate.getTime())) {
|
||||
setDate(parsedDate);
|
||||
} else {
|
||||
setDate(null);
|
||||
}
|
||||
} else {
|
||||
setDate(null);
|
||||
}
|
||||
|
||||
const departureTimeParam = searchParams.get("departureTime");
|
||||
setDepartureTime(departureTimeParam || "12:00");
|
||||
|
||||
const arrivalTimeParam = searchParams.get("arrivalTime");
|
||||
setArrivalTime(arrivalTimeParam || "13:00");
|
||||
}, [searchParams]);
|
||||
|
||||
// Функция для сохранения фильтров в searchParams
|
||||
const handleApplyFilters = () => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Сохраняем только нестандартные значения
|
||||
if (lengthRange[0] !== 7 || lengthRange[1] !== 50) {
|
||||
params.set("lengthMin", lengthRange[0].toString());
|
||||
params.set("lengthMax", lengthRange[1].toString());
|
||||
}
|
||||
|
||||
if (priceRange[0] !== 3000 || priceRange[1] !== 200000) {
|
||||
params.set("priceMin", priceRange[0].toString());
|
||||
params.set("priceMax", priceRange[1].toString());
|
||||
}
|
||||
|
||||
if (yearRange[0] !== 1991 || yearRange[1] !== 2025) {
|
||||
params.set("yearMin", yearRange[0].toString());
|
||||
params.set("yearMax", yearRange[1].toString());
|
||||
}
|
||||
|
||||
if (adults > 0) {
|
||||
params.set("adults", adults.toString());
|
||||
}
|
||||
|
||||
if (children > 0) {
|
||||
params.set("children", children.toString());
|
||||
}
|
||||
|
||||
if (paymentType !== "all") {
|
||||
params.set("paymentType", paymentType);
|
||||
}
|
||||
|
||||
if (quickBooking) {
|
||||
params.set("quickBooking", "true");
|
||||
}
|
||||
|
||||
if (hasToilet) {
|
||||
params.set("hasToilet", "true");
|
||||
}
|
||||
|
||||
if (vesselType) {
|
||||
params.set("vesselType", vesselType);
|
||||
}
|
||||
|
||||
if (date) {
|
||||
params.set("date", date.toISOString());
|
||||
}
|
||||
|
||||
if (departureTime !== "12:00") {
|
||||
params.set("departureTime", departureTime);
|
||||
}
|
||||
|
||||
if (arrivalTime !== "13:00") {
|
||||
params.set("arrivalTime", arrivalTime);
|
||||
}
|
||||
|
||||
// Обновляем URL без прокрутки страницы
|
||||
const newUrl = params.toString()
|
||||
? `${pathname}?${params.toString()}`
|
||||
: pathname;
|
||||
router.replace(newUrl, { scroll: false });
|
||||
|
||||
// Вызываем callback, если он есть
|
||||
onApply?.();
|
||||
};
|
||||
|
||||
export default function CatalogSidebar({
|
||||
filters,
|
||||
setFilters,
|
||||
onApply,
|
||||
onReset,
|
||||
}: CatalogSidebarProps) {
|
||||
const formatPrice = (value: number) => {
|
||||
return new Intl.NumberFormat("ru-RU").format(value) + " Р";
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setLengthRange([7, 50]);
|
||||
setPriceRange([3000, 200000]);
|
||||
setYearRange([1991, 2025]);
|
||||
setAdults(0);
|
||||
setChildren(0);
|
||||
setPaymentType("all");
|
||||
setQuickBooking(false);
|
||||
setHasToilet(false);
|
||||
setVesselType("");
|
||||
setDate(null);
|
||||
setDepartureTime("12:00");
|
||||
setArrivalTime("13:00");
|
||||
// Очищаем URL параметры без прокрутки страницы
|
||||
router.replace(pathname, { scroll: false });
|
||||
};
|
||||
|
||||
const activeFiltersCount = [
|
||||
lengthRange[0] !== 7 || lengthRange[1] !== 50,
|
||||
priceRange[0] !== 3000 || priceRange[1] !== 200000,
|
||||
yearRange[0] !== 1991 || yearRange[1] !== 2025,
|
||||
adults !== 0 || children !== 0,
|
||||
paymentType !== "all",
|
||||
quickBooking,
|
||||
hasToilet,
|
||||
vesselType !== "",
|
||||
date !== null,
|
||||
departureTime !== "12:00",
|
||||
arrivalTime !== "13:00",
|
||||
filters.lengthRange[0] !== defaultFilters.lengthRange[0] || filters.lengthRange[1] !== defaultFilters.lengthRange[1],
|
||||
filters.priceRange[0] !== defaultFilters.priceRange[0] || filters.priceRange[1] !== defaultFilters.priceRange[1],
|
||||
filters.yearRange[0] !== defaultFilters.yearRange[0] || filters.yearRange[1] !== defaultFilters.yearRange[1],
|
||||
filters.adults !== 0 || filters.children !== 0,
|
||||
filters.paymentType !== defaultFilters.paymentType,
|
||||
filters.quickBooking,
|
||||
filters.hasToilet,
|
||||
filters.search !== "",
|
||||
filters.date !== null,
|
||||
filters.departureTime !== defaultFilters.departureTime,
|
||||
filters.arrivalTime !== defaultFilters.arrivalTime,
|
||||
].filter(Boolean).length;
|
||||
|
||||
return (
|
||||
|
|
@ -202,7 +46,7 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-md font-bold text-[#333333]">Искать</h2>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
onClick={onReset}
|
||||
className="text-xs text-[#333333] hover:text-gray-900"
|
||||
>
|
||||
Сбросить все{" "}
|
||||
|
|
@ -221,8 +65,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<input
|
||||
type="text"
|
||||
placeholder="Искать"
|
||||
value={vesselType}
|
||||
onChange={(e) => setVesselType(e.target.value)}
|
||||
value={filters.search}
|
||||
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
||||
className="w-full h-12 px-3 border border-gray-300 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-[#008299]"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -234,8 +78,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
</Label>
|
||||
<div className="px-0">
|
||||
<Slider
|
||||
value={lengthRange}
|
||||
onValueChange={setLengthRange}
|
||||
value={filters.lengthRange}
|
||||
onValueChange={(value) => setFilters({ ...filters, lengthRange: value as [number, number] })}
|
||||
min={7}
|
||||
max={50}
|
||||
step={1}
|
||||
|
|
@ -244,10 +88,10 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<div className="flex justify-between text-xs text-[#333333]">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name="lengthMin" size={20} />
|
||||
{lengthRange[0]} m
|
||||
{filters.lengthRange[0]} m
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{lengthRange[1]} m
|
||||
{filters.lengthRange[1]} m
|
||||
<Icon name="lengthMax" size={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -259,12 +103,12 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<DatePicker
|
||||
variant="small"
|
||||
placeholder="Дата"
|
||||
value={date}
|
||||
departureTime={departureTime}
|
||||
arrivalTime={arrivalTime}
|
||||
onDateChange={(newDate) => setDate(newDate || null)}
|
||||
onDepartureTimeChange={setDepartureTime}
|
||||
onArrivalTimeChange={setArrivalTime}
|
||||
value={filters.date}
|
||||
departureTime={filters.departureTime}
|
||||
arrivalTime={filters.arrivalTime}
|
||||
onDateChange={(newDate) => setFilters({ ...filters, date: newDate || null })}
|
||||
onDepartureTimeChange={(time) => setFilters({ ...filters, departureTime: time })}
|
||||
onArrivalTimeChange={(time) => setFilters({ ...filters, arrivalTime: time })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -272,11 +116,10 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<div>
|
||||
<GuestPicker
|
||||
variant="small"
|
||||
adults={adults}
|
||||
childrenCount={children}
|
||||
adults={filters.adults}
|
||||
childrenCount={filters.children}
|
||||
onChange={(adults, children) => {
|
||||
setAdults(adults);
|
||||
setChildren(children);
|
||||
setFilters({ ...filters, adults, children });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -286,9 +129,9 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<Checkbox
|
||||
id="quick-booking"
|
||||
variant="large"
|
||||
checked={quickBooking}
|
||||
checked={filters.quickBooking}
|
||||
onCheckedChange={(checked) =>
|
||||
setQuickBooking(checked === true)
|
||||
setFilters({ ...filters, quickBooking: checked === true })
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
|
|
@ -306,8 +149,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
</Label>
|
||||
<div>
|
||||
<Slider
|
||||
value={priceRange}
|
||||
onValueChange={setPriceRange}
|
||||
value={filters.priceRange}
|
||||
onValueChange={(value) => setFilters({ ...filters, priceRange: value as [number, number] })}
|
||||
min={3000}
|
||||
max={200000}
|
||||
step={1000}
|
||||
|
|
@ -316,10 +159,10 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<div className="flex justify-between text-xs text-[#333333]">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name="priceMin" size={20} />
|
||||
{formatPrice(priceRange[0])}
|
||||
{formatPrice(filters.priceRange[0])}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{formatPrice(priceRange[1])}
|
||||
{formatPrice(filters.priceRange[1])}
|
||||
<Icon name="priceMax" size={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -332,8 +175,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
Тип оплаты
|
||||
</Label>
|
||||
<RadioGroup
|
||||
value={paymentType}
|
||||
onValueChange={setPaymentType}
|
||||
value={filters.paymentType}
|
||||
onValueChange={(value) => setFilters({ ...filters, paymentType: value })}
|
||||
className="space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -373,8 +216,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
</Label>
|
||||
<div>
|
||||
<Slider
|
||||
value={yearRange}
|
||||
onValueChange={setYearRange}
|
||||
value={filters.yearRange}
|
||||
onValueChange={(value) => setFilters({ ...filters, yearRange: value as [number, number] })}
|
||||
min={1991}
|
||||
max={2025}
|
||||
step={1}
|
||||
|
|
@ -383,10 +226,10 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<div className="flex justify-between text-xs text-[#333333]">
|
||||
<div className="text-s flex items-center gap-1">
|
||||
<Icon name="boatYearMin" size={20} />
|
||||
{yearRange[0]}
|
||||
{filters.yearRange[0]}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{yearRange[1]}
|
||||
{filters.yearRange[1]}
|
||||
<Icon name="boatYearMax" size={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -398,9 +241,9 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
<Checkbox
|
||||
id="toilet"
|
||||
variant="large"
|
||||
checked={hasToilet}
|
||||
checked={filters.hasToilet}
|
||||
onCheckedChange={(checked) =>
|
||||
setHasToilet(checked === true)
|
||||
setFilters({ ...filters, hasToilet: checked === true })
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
|
|
@ -413,7 +256,7 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
|
||||
{/* Кнопка Применить */}
|
||||
<Button
|
||||
onClick={handleApplyFilters}
|
||||
onClick={onApply}
|
||||
className="w-full bg-[#008299] hover:bg-[#006d7f] text-white font-bold h-12 mt-2"
|
||||
>
|
||||
Применить
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import Image from "next/image";
|
||||
import Icon from "@/components/ui/icon";
|
||||
|
|
@ -15,111 +15,224 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { Sliders, X, ArrowLeft } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useApiClient from "@/hooks/useApiClient";
|
||||
import { formatMinCost, formatSpeed, formatWidth, getImageUrl } from "@/lib/utils";
|
||||
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
||||
|
||||
const yachts = [
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "12 метров",
|
||||
price: "от 12 500 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht1.jpg",
|
||||
quickBooking: true,
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "12 метров",
|
||||
price: "от 13 200 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht2.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "8 метров",
|
||||
price: "от 7 000 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht3.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "13 метров",
|
||||
price: "от 12 000 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht4.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "13 метров",
|
||||
price: "от 30 000 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht5.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "10 метров",
|
||||
price: "от 8 500 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht6.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "8 метров",
|
||||
price: "от 6 000 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht1.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "10 метров",
|
||||
price: "от 6 960 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht2.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "10 метров",
|
||||
price: "от 6 600 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht3.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "13 метров",
|
||||
price: "от 18 000 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht4.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "10 метров",
|
||||
price: "от 7 200 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht5.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
{
|
||||
name: "Яхта",
|
||||
length: "11 метров",
|
||||
price: "от 7 200 ₽ / час",
|
||||
feet: "7 Футов",
|
||||
img: "/images/yachts/yacht6.jpg",
|
||||
badge: "По запросу",
|
||||
},
|
||||
];
|
||||
export interface CatalogFilters {
|
||||
search: string;
|
||||
lengthRange: [number, number];
|
||||
priceRange: [number, number];
|
||||
yearRange: [number, number];
|
||||
adults: number;
|
||||
children: number;
|
||||
paymentType: string;
|
||||
quickBooking: boolean;
|
||||
hasToilet: boolean;
|
||||
date: Date | null;
|
||||
departureTime: string;
|
||||
arrivalTime: string;
|
||||
}
|
||||
|
||||
export const defaultFilters: CatalogFilters = {
|
||||
lengthRange: [7, 50],
|
||||
priceRange: [3000, 200000],
|
||||
yearRange: [1991, 2025],
|
||||
adults: 0,
|
||||
children: 0,
|
||||
paymentType: "all",
|
||||
quickBooking: false,
|
||||
hasToilet: false,
|
||||
search: "",
|
||||
date: null,
|
||||
departureTime: "12:00",
|
||||
arrivalTime: "13:00",
|
||||
};
|
||||
|
||||
export default function CatalogPage() {
|
||||
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const client = useApiClient();
|
||||
|
||||
const [yachtCatalog, setYachtCatalog] = useState<CatalogFilteredResponseDto | null>(null);
|
||||
|
||||
// Объединенное состояние фильтров
|
||||
const [filters, setFilters] = useState<CatalogFilters>(defaultFilters);
|
||||
|
||||
// Загрузка фильтров из searchParams при монтировании и изменении URL
|
||||
useEffect(() => {
|
||||
const newFilters: CatalogFilters = { ...defaultFilters };
|
||||
|
||||
const lengthMin = searchParams.get("lengthMin");
|
||||
const lengthMax = searchParams.get("lengthMax");
|
||||
if (lengthMin && lengthMax) {
|
||||
const min = parseInt(lengthMin);
|
||||
const max = parseInt(lengthMax);
|
||||
if (!isNaN(min) && !isNaN(max)) {
|
||||
newFilters.lengthRange = [min, max];
|
||||
}
|
||||
}
|
||||
|
||||
const priceMin = searchParams.get("priceMin");
|
||||
const priceMax = searchParams.get("priceMax");
|
||||
if (priceMin && priceMax) {
|
||||
const min = parseInt(priceMin);
|
||||
const max = parseInt(priceMax);
|
||||
if (!isNaN(min) && !isNaN(max)) {
|
||||
newFilters.priceRange = [min, max];
|
||||
}
|
||||
}
|
||||
|
||||
const yearMin = searchParams.get("yearMin");
|
||||
const yearMax = searchParams.get("yearMax");
|
||||
if (yearMin && yearMax) {
|
||||
const min = parseInt(yearMin);
|
||||
const max = parseInt(yearMax);
|
||||
if (!isNaN(min) && !isNaN(max)) {
|
||||
newFilters.yearRange = [min, max];
|
||||
}
|
||||
}
|
||||
|
||||
const adultsParam = searchParams.get("adults");
|
||||
if (adultsParam) {
|
||||
newFilters.adults = parseInt(adultsParam) || 0;
|
||||
}
|
||||
|
||||
const childrenParam = searchParams.get("children");
|
||||
if (childrenParam) {
|
||||
newFilters.children = parseInt(childrenParam) || 0;
|
||||
}
|
||||
|
||||
const paymentTypeParam = searchParams.get("paymentType");
|
||||
if (paymentTypeParam) {
|
||||
newFilters.paymentType = paymentTypeParam;
|
||||
}
|
||||
|
||||
const quickBookingParam = searchParams.get("quickBooking");
|
||||
if (quickBookingParam) {
|
||||
newFilters.quickBooking = quickBookingParam === "true";
|
||||
}
|
||||
|
||||
const hasToiletParam = searchParams.get("hasToilet");
|
||||
if (hasToiletParam) {
|
||||
newFilters.hasToilet = hasToiletParam === "true";
|
||||
}
|
||||
|
||||
const searchParam = searchParams.get("search");
|
||||
if (searchParam) {
|
||||
newFilters.search = searchParam;
|
||||
}
|
||||
|
||||
const dateParam = searchParams.get("date");
|
||||
if (dateParam) {
|
||||
const parsedDate = new Date(dateParam);
|
||||
if (!isNaN(parsedDate.getTime())) {
|
||||
newFilters.date = parsedDate;
|
||||
}
|
||||
}
|
||||
|
||||
const departureTimeParam = searchParams.get("departureTime");
|
||||
if (departureTimeParam) {
|
||||
newFilters.departureTime = departureTimeParam;
|
||||
}
|
||||
|
||||
const arrivalTimeParam = searchParams.get("arrivalTime");
|
||||
if (arrivalTimeParam) {
|
||||
newFilters.arrivalTime = arrivalTimeParam;
|
||||
}
|
||||
|
||||
setFilters(newFilters);
|
||||
}, [searchParams]);
|
||||
|
||||
// Функция для сохранения фильтров в searchParams
|
||||
const handleApplyFilters = () => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Сохраняем только нестандартные значения
|
||||
if (filters.lengthRange[0] !== defaultFilters.lengthRange[0] || filters.lengthRange[1] !== defaultFilters.lengthRange[1]) {
|
||||
params.set("lengthMin", filters.lengthRange[0].toString());
|
||||
params.set("lengthMax", filters.lengthRange[1].toString());
|
||||
}
|
||||
|
||||
if (filters.priceRange[0] !== defaultFilters.priceRange[0] || filters.priceRange[1] !== defaultFilters.priceRange[1]) {
|
||||
params.set("priceMin", filters.priceRange[0].toString());
|
||||
params.set("priceMax", filters.priceRange[1].toString());
|
||||
}
|
||||
|
||||
if (filters.yearRange[0] !== defaultFilters.yearRange[0] || filters.yearRange[1] !== defaultFilters.yearRange[1]) {
|
||||
params.set("yearMin", filters.yearRange[0].toString());
|
||||
params.set("yearMax", filters.yearRange[1].toString());
|
||||
}
|
||||
|
||||
if (filters.adults > 0) {
|
||||
params.set("adults", filters.adults.toString());
|
||||
}
|
||||
|
||||
if (filters.children > 0) {
|
||||
params.set("children", filters.children.toString());
|
||||
}
|
||||
|
||||
if (filters.paymentType !== defaultFilters.paymentType) {
|
||||
params.set("paymentType", filters.paymentType);
|
||||
}
|
||||
|
||||
if (filters.quickBooking) {
|
||||
params.set("quickBooking", "true");
|
||||
}
|
||||
|
||||
if (filters.hasToilet) {
|
||||
params.set("hasToilet", "true");
|
||||
}
|
||||
|
||||
if (filters.search) {
|
||||
params.set("search", filters.search);
|
||||
}
|
||||
|
||||
if (filters.date) {
|
||||
params.set("date", filters.date.toISOString());
|
||||
}
|
||||
|
||||
if (filters.departureTime !== defaultFilters.departureTime) {
|
||||
params.set("departureTime", filters.departureTime);
|
||||
}
|
||||
|
||||
if (filters.arrivalTime !== defaultFilters.arrivalTime) {
|
||||
params.set("arrivalTime", filters.arrivalTime);
|
||||
}
|
||||
|
||||
// Обновляем URL без прокрутки страницы
|
||||
const newUrl = params.toString()
|
||||
? `${pathname}?${params.toString()}`
|
||||
: pathname;
|
||||
router.replace(newUrl, { scroll: false });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setFilters(defaultFilters);
|
||||
// Очищаем URL параметры без прокрутки страницы
|
||||
router.replace(pathname, { scroll: false });
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const params: Record<string, string> = {
|
||||
search: searchParams.get("search") ?? "",
|
||||
};
|
||||
|
||||
const response = await client.get<CatalogFilteredResponseDto>("/catalog/filtered/", {
|
||||
params,
|
||||
});
|
||||
|
||||
setYachtCatalog(response.data);
|
||||
})();
|
||||
}, [searchParams])
|
||||
|
||||
return (
|
||||
<main className="bg-[#f4f4f4] min-h-screen">
|
||||
<main className="bg-[#f4f4f4]">
|
||||
<div className="container max-w-6xl mx-auto px-4 py-6">
|
||||
{/* Breadcrumbs - скрыты на мобильных */}
|
||||
<div className="hidden lg:flex mb-4 text-sm text-[#999999] items-center gap-[16px]">
|
||||
|
|
@ -168,7 +281,12 @@ export default function CatalogPage() {
|
|||
<div className="flex flex-col lg:flex-row gap-[74px]">
|
||||
{/* Sidebar - скрыт на мобильных, виден на десктопе */}
|
||||
<div className="hidden lg:block w-full lg:w-[260px] flex-shrink-0">
|
||||
<CatalogSidebar />
|
||||
<CatalogSidebar
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
onApply={handleApplyFilters}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Мобильное модальное окно фильтров */}
|
||||
|
|
@ -196,7 +314,13 @@ export default function CatalogPage() {
|
|||
</div>
|
||||
<div className="p-4">
|
||||
<CatalogSidebar
|
||||
onApply={() => setIsFiltersOpen(false)}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
onApply={() => {
|
||||
handleApplyFilters();
|
||||
setIsFiltersOpen(false);
|
||||
}}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -214,7 +338,7 @@ export default function CatalogPage() {
|
|||
<p className="hidden lg:block text-lg text-[#333333]">
|
||||
Доступно яхт:{" "}
|
||||
<span className="text-[#b2b2b2]">
|
||||
{yachts.length}
|
||||
{yachtCatalog?.total}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -252,32 +376,25 @@ export default function CatalogPage() {
|
|||
|
||||
{/* Yacht Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{yachts.map((yacht, idx) => (
|
||||
{yachtCatalog && (<>
|
||||
{yachtCatalog.items.map((yacht) => (
|
||||
<Link
|
||||
key={idx}
|
||||
href={`/catalog/${idx + 1}`}
|
||||
key={yacht.id}
|
||||
href={`/catalog/${yacht.id ?? 0}`}
|
||||
className="block"
|
||||
>
|
||||
<Card className="overflow-hidden bg-white text-gray-900 border border-gray-200 cursor-pointer transition-all duration-200 hover:shadow-lg">
|
||||
<CardHeader className="p-0 relative">
|
||||
<div className="relative">
|
||||
{/* Quick Booking Badge */}
|
||||
{yacht.quickBooking && (
|
||||
<div className="absolute top-3 left-3 z-10">
|
||||
<div className="bg-[#008299] text-white px-3 py-1 rounded-lg text-sm font-medium">
|
||||
Быстрая бронь
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Image
|
||||
src={yacht.img}
|
||||
src={getImageUrl(yacht.mainImageUrl)}
|
||||
alt={yacht.name}
|
||||
width={400}
|
||||
height={250}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
{/* Badge Overlay */}
|
||||
{yacht.badge && (
|
||||
{!yacht.hasQuickRent && (
|
||||
<div className="absolute top-3 left-3">
|
||||
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm gap-1">
|
||||
<Icon
|
||||
|
|
@ -285,7 +402,7 @@ export default function CatalogPage() {
|
|||
name="restart"
|
||||
/>
|
||||
<span>
|
||||
{yacht.badge}
|
||||
По запросу
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -305,7 +422,7 @@ export default function CatalogPage() {
|
|||
name="width"
|
||||
/>
|
||||
<span>
|
||||
{yacht.length}
|
||||
{formatWidth(yacht.length)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -313,7 +430,7 @@ export default function CatalogPage() {
|
|||
{/* Правая колонка - цена и футы */}
|
||||
<div className="space-y-2 text-right">
|
||||
<p className="text-lg font-bold text-black">
|
||||
{yacht.price}
|
||||
{formatMinCost(yacht.minCost)} / час
|
||||
</p>
|
||||
<div className="flex items-center gap-1 text-sm text-gray-600 justify-end">
|
||||
<Icon
|
||||
|
|
@ -321,7 +438,7 @@ export default function CatalogPage() {
|
|||
name="anchor"
|
||||
/>
|
||||
<span>
|
||||
{yacht.feet}
|
||||
{formatSpeed(yacht.speed)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -330,6 +447,7 @@ export default function CatalogPage() {
|
|||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { DatePicker } from "@/components/ui/date-picker";
|
||||
import Icon from "@/components/ui/icon";
|
||||
import { GuestPicker } from "@/components/form/guest-picker";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Hero() {
|
||||
const [adults, setAdults] = useState<number>(0);
|
||||
|
|
@ -34,11 +35,13 @@ export default function Hero() {
|
|||
</div>
|
||||
|
||||
{/* Кнопка для мобильных устройств */}
|
||||
<Link href="/catalog">
|
||||
<div className="md:hidden flex justify-center">
|
||||
<Button variant="gradient" className="font-bold text-white h-[64px] border-0 px-8 text-lg w-full">
|
||||
Выберите яхту
|
||||
</Button>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Поисковая форма - скрыта на мобильных устройствах */}
|
||||
<Card className="bg-white shadow-lg s hidden md:block rounded-full">
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ export default function YachtGrid() {
|
|||
{/* Yacht Grid */}
|
||||
{yachtCatalog && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
|
||||
{yachtCatalog.map((yacht, idx) => (
|
||||
{yachtCatalog.map((yacht) => (
|
||||
<Link
|
||||
key={idx}
|
||||
href={`/catalog/${idx + 1}`}
|
||||
key={yacht.id}
|
||||
href={`/catalog/${yacht.id ?? 0}}`}
|
||||
className="block"
|
||||
>
|
||||
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg">
|
||||
|
|
@ -85,7 +85,7 @@ export default function YachtGrid() {
|
|||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
{/* Badge Overlay */}
|
||||
{yacht.hasQuickRent && !yacht.topText && (
|
||||
{!yacht.hasQuickRent && !yacht.topText && (
|
||||
<>
|
||||
<div className="absolute top-3 left-3">
|
||||
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm flex items-center gap-1">
|
||||
|
|
|
|||
Loading…
Reference in New Issue