Каталог, интеграция

This commit is contained in:
Sergey Bolshakov 2025-12-14 21:50:43 +03:00
parent 7181718b0d
commit 7f6c6d1107
5 changed files with 359 additions and 390 deletions

View File

@ -16,3 +16,8 @@ interface MainPageCatalogResponseDto {
featuredYacht: CatalogItemDto; featuredYacht: CatalogItemDto;
restYachts: CatalogItemDto[]; restYachts: CatalogItemDto[];
} }
interface CatalogFilteredResponseDto {
items: CatalogItemDto[];
total: number;
}

View File

@ -1,7 +1,5 @@
"use client"; "use client";
import { useState, useEffect } from "react";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; 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 { DatePicker } from "@/components/ui/date-picker";
import { GuestPicker } from "@/components/form/guest-picker"; import { GuestPicker } from "@/components/form/guest-picker";
import Icon from "@/components/ui/icon"; import Icon from "@/components/ui/icon";
import { CatalogFilters, defaultFilters } from "@/app/catalog/page";
interface CatalogSidebarProps { interface CatalogSidebarProps {
filters: CatalogFilters;
setFilters: (filters: CatalogFilters | ((prev: CatalogFilters) => CatalogFilters)) => void;
onApply?: () => void; onApply?: () => void;
onReset?: () => void;
} }
export default function CatalogSidebar({ onApply }: CatalogSidebarProps) { export default function CatalogSidebar({
const searchParams = useSearchParams(); filters,
const router = useRouter(); setFilters,
const pathname = usePathname(); onApply,
onReset,
const [lengthRange, setLengthRange] = useState([7, 50]); }: CatalogSidebarProps) {
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?.();
};
const formatPrice = (value: number) => { const formatPrice = (value: number) => {
return new Intl.NumberFormat("ru-RU").format(value) + " Р"; 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 = [ const activeFiltersCount = [
lengthRange[0] !== 7 || lengthRange[1] !== 50, filters.lengthRange[0] !== defaultFilters.lengthRange[0] || filters.lengthRange[1] !== defaultFilters.lengthRange[1],
priceRange[0] !== 3000 || priceRange[1] !== 200000, filters.priceRange[0] !== defaultFilters.priceRange[0] || filters.priceRange[1] !== defaultFilters.priceRange[1],
yearRange[0] !== 1991 || yearRange[1] !== 2025, filters.yearRange[0] !== defaultFilters.yearRange[0] || filters.yearRange[1] !== defaultFilters.yearRange[1],
adults !== 0 || children !== 0, filters.adults !== 0 || filters.children !== 0,
paymentType !== "all", filters.paymentType !== defaultFilters.paymentType,
quickBooking, filters.quickBooking,
hasToilet, filters.hasToilet,
vesselType !== "", filters.search !== "",
date !== null, filters.date !== null,
departureTime !== "12:00", filters.departureTime !== defaultFilters.departureTime,
arrivalTime !== "13:00", filters.arrivalTime !== defaultFilters.arrivalTime,
].filter(Boolean).length; ].filter(Boolean).length;
return ( return (
@ -202,7 +46,7 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-md font-bold text-[#333333]">Искать</h2> <h2 className="text-md font-bold text-[#333333]">Искать</h2>
<button <button
onClick={handleReset} onClick={onReset}
className="text-xs text-[#333333] hover:text-gray-900" className="text-xs text-[#333333] hover:text-gray-900"
> >
Сбросить все{" "} Сбросить все{" "}
@ -221,8 +65,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<input <input
type="text" type="text"
placeholder="Искать" placeholder="Искать"
value={vesselType} value={filters.search}
onChange={(e) => setVesselType(e.target.value)} 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]" 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> </div>
@ -234,8 +78,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
</Label> </Label>
<div className="px-0"> <div className="px-0">
<Slider <Slider
value={lengthRange} value={filters.lengthRange}
onValueChange={setLengthRange} onValueChange={(value) => setFilters({ ...filters, lengthRange: value as [number, number] })}
min={7} min={7}
max={50} max={50}
step={1} step={1}
@ -244,10 +88,10 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<div className="flex justify-between text-xs text-[#333333]"> <div className="flex justify-between text-xs text-[#333333]">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Icon name="lengthMin" size={20} /> <Icon name="lengthMin" size={20} />
{lengthRange[0]} m {filters.lengthRange[0]} m
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{lengthRange[1]} m {filters.lengthRange[1]} m
<Icon name="lengthMax" size={20} /> <Icon name="lengthMax" size={20} />
</div> </div>
</div> </div>
@ -259,12 +103,12 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<DatePicker <DatePicker
variant="small" variant="small"
placeholder="Дата" placeholder="Дата"
value={date} value={filters.date}
departureTime={departureTime} departureTime={filters.departureTime}
arrivalTime={arrivalTime} arrivalTime={filters.arrivalTime}
onDateChange={(newDate) => setDate(newDate || null)} onDateChange={(newDate) => setFilters({ ...filters, date: newDate || null })}
onDepartureTimeChange={setDepartureTime} onDepartureTimeChange={(time) => setFilters({ ...filters, departureTime: time })}
onArrivalTimeChange={setArrivalTime} onArrivalTimeChange={(time) => setFilters({ ...filters, arrivalTime: time })}
/> />
</div> </div>
@ -272,11 +116,10 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<div> <div>
<GuestPicker <GuestPicker
variant="small" variant="small"
adults={adults} adults={filters.adults}
childrenCount={children} childrenCount={filters.children}
onChange={(adults, children) => { onChange={(adults, children) => {
setAdults(adults); setFilters({ ...filters, adults, children });
setChildren(children);
}} }}
/> />
</div> </div>
@ -286,9 +129,9 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<Checkbox <Checkbox
id="quick-booking" id="quick-booking"
variant="large" variant="large"
checked={quickBooking} checked={filters.quickBooking}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setQuickBooking(checked === true) setFilters({ ...filters, quickBooking: checked === true })
} }
/> />
<Label <Label
@ -306,8 +149,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
</Label> </Label>
<div> <div>
<Slider <Slider
value={priceRange} value={filters.priceRange}
onValueChange={setPriceRange} onValueChange={(value) => setFilters({ ...filters, priceRange: value as [number, number] })}
min={3000} min={3000}
max={200000} max={200000}
step={1000} step={1000}
@ -316,10 +159,10 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<div className="flex justify-between text-xs text-[#333333]"> <div className="flex justify-between text-xs text-[#333333]">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Icon name="priceMin" size={20} /> <Icon name="priceMin" size={20} />
{formatPrice(priceRange[0])} {formatPrice(filters.priceRange[0])}
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{formatPrice(priceRange[1])} {formatPrice(filters.priceRange[1])}
<Icon name="priceMax" size={20} /> <Icon name="priceMax" size={20} />
</div> </div>
</div> </div>
@ -332,8 +175,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
Тип оплаты Тип оплаты
</Label> </Label>
<RadioGroup <RadioGroup
value={paymentType} value={filters.paymentType}
onValueChange={setPaymentType} onValueChange={(value) => setFilters({ ...filters, paymentType: value })}
className="space-y-2" className="space-y-2"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -373,8 +216,8 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
</Label> </Label>
<div> <div>
<Slider <Slider
value={yearRange} value={filters.yearRange}
onValueChange={setYearRange} onValueChange={(value) => setFilters({ ...filters, yearRange: value as [number, number] })}
min={1991} min={1991}
max={2025} max={2025}
step={1} step={1}
@ -383,10 +226,10 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<div className="flex justify-between text-xs text-[#333333]"> <div className="flex justify-between text-xs text-[#333333]">
<div className="text-s flex items-center gap-1"> <div className="text-s flex items-center gap-1">
<Icon name="boatYearMin" size={20} /> <Icon name="boatYearMin" size={20} />
{yearRange[0]} {filters.yearRange[0]}
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{yearRange[1]} {filters.yearRange[1]}
<Icon name="boatYearMax" size={20} /> <Icon name="boatYearMax" size={20} />
</div> </div>
</div> </div>
@ -398,9 +241,9 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
<Checkbox <Checkbox
id="toilet" id="toilet"
variant="large" variant="large"
checked={hasToilet} checked={filters.hasToilet}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setHasToilet(checked === true) setFilters({ ...filters, hasToilet: checked === true })
} }
/> />
<Label <Label
@ -413,7 +256,7 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
{/* Кнопка Применить */} {/* Кнопка Применить */}
<Button <Button
onClick={handleApplyFilters} onClick={onApply}
className="w-full bg-[#008299] hover:bg-[#006d7f] text-white font-bold h-12 mt-2" className="w-full bg-[#008299] hover:bg-[#006d7f] text-white font-bold h-12 mt-2"
> >
Применить Применить

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState, useCallback, useRef } from "react";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/ui/card";
import Image from "next/image"; import Image from "next/image";
import Icon from "@/components/ui/icon"; import Icon from "@/components/ui/icon";
@ -15,111 +15,224 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Sliders, X, ArrowLeft } from "lucide-react"; import { Sliders, X, ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button"; 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 = [ export interface CatalogFilters {
{ search: string;
name: "Яхта", lengthRange: [number, number];
length: "12 метров", priceRange: [number, number];
price: "от 12 500 ₽ / час", yearRange: [number, number];
feet: "7 Футов", adults: number;
img: "/images/yachts/yacht1.jpg", children: number;
quickBooking: true, paymentType: string;
}, quickBooking: boolean;
{ hasToilet: boolean;
name: "Яхта", date: Date | null;
length: "12 метров", departureTime: string;
price: "от 13 200 ₽ / час", arrivalTime: string;
feet: "7 Футов", }
img: "/images/yachts/yacht2.jpg",
badge: "По запросу", export const defaultFilters: CatalogFilters = {
}, lengthRange: [7, 50],
{ priceRange: [3000, 200000],
name: "Яхта", yearRange: [1991, 2025],
length: "8 метров", adults: 0,
price: "от 7 000 ₽ / час", children: 0,
feet: "7 Футов", paymentType: "all",
img: "/images/yachts/yacht3.jpg", quickBooking: false,
badge: "По запросу", hasToilet: false,
}, search: "",
{ date: null,
name: "Яхта", departureTime: "12:00",
length: "13 метров", arrivalTime: "13:00",
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 default function CatalogPage() { export default function CatalogPage() {
const [isFiltersOpen, setIsFiltersOpen] = useState(false); 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 ( return (
<main className="bg-[#f4f4f4] min-h-screen"> <main className="bg-[#f4f4f4]">
<div className="container max-w-6xl mx-auto px-4 py-6"> <div className="container max-w-6xl mx-auto px-4 py-6">
{/* Breadcrumbs - скрыты на мобильных */} {/* Breadcrumbs - скрыты на мобильных */}
<div className="hidden lg:flex mb-4 text-sm text-[#999999] items-center gap-[16px]"> <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]"> <div className="flex flex-col lg:flex-row gap-[74px]">
{/* Sidebar - скрыт на мобильных, виден на десктопе */} {/* Sidebar - скрыт на мобильных, виден на десктопе */}
<div className="hidden lg:block w-full lg:w-[260px] flex-shrink-0"> <div className="hidden lg:block w-full lg:w-[260px] flex-shrink-0">
<CatalogSidebar /> <CatalogSidebar
filters={filters}
setFilters={setFilters}
onApply={handleApplyFilters}
onReset={handleReset}
/>
</div> </div>
{/* Мобильное модальное окно фильтров */} {/* Мобильное модальное окно фильтров */}
@ -196,7 +314,13 @@ export default function CatalogPage() {
</div> </div>
<div className="p-4"> <div className="p-4">
<CatalogSidebar <CatalogSidebar
onApply={() => setIsFiltersOpen(false)} filters={filters}
setFilters={setFilters}
onApply={() => {
handleApplyFilters();
setIsFiltersOpen(false);
}}
onReset={handleReset}
/> />
</div> </div>
</div> </div>
@ -214,7 +338,7 @@ export default function CatalogPage() {
<p className="hidden lg:block text-lg text-[#333333]"> <p className="hidden lg:block text-lg text-[#333333]">
Доступно яхт:{" "} Доступно яхт:{" "}
<span className="text-[#b2b2b2]"> <span className="text-[#b2b2b2]">
{yachts.length} {yachtCatalog?.total}
</span> </span>
</p> </p>
</div> </div>
@ -252,32 +376,25 @@ export default function CatalogPage() {
{/* Yacht Grid */} {/* Yacht Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{yachts.map((yacht, idx) => ( {yachtCatalog && (<>
{yachtCatalog.items.map((yacht) => (
<Link <Link
key={idx} key={yacht.id}
href={`/catalog/${idx + 1}`} href={`/catalog/${yacht.id ?? 0}`}
className="block" className="block"
> >
<Card className="overflow-hidden bg-white text-gray-900 border border-gray-200 cursor-pointer transition-all duration-200 hover:shadow-lg"> <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"> <CardHeader className="p-0 relative">
<div className="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 <Image
src={yacht.img} src={getImageUrl(yacht.mainImageUrl)}
alt={yacht.name} alt={yacht.name}
width={400} width={400}
height={250} height={250}
className="w-full h-48 object-cover" className="w-full h-48 object-cover"
/> />
{/* Badge Overlay */} {/* Badge Overlay */}
{yacht.badge && ( {!yacht.hasQuickRent && (
<div className="absolute top-3 left-3"> <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"> <div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm gap-1">
<Icon <Icon
@ -285,7 +402,7 @@ export default function CatalogPage() {
name="restart" name="restart"
/> />
<span> <span>
{yacht.badge} По запросу
</span> </span>
</div> </div>
</div> </div>
@ -305,7 +422,7 @@ export default function CatalogPage() {
name="width" name="width"
/> />
<span> <span>
{yacht.length} {formatWidth(yacht.length)}
</span> </span>
</div> </div>
</div> </div>
@ -313,7 +430,7 @@ export default function CatalogPage() {
{/* Правая колонка - цена и футы */} {/* Правая колонка - цена и футы */}
<div className="space-y-2 text-right"> <div className="space-y-2 text-right">
<p className="text-lg font-bold text-black"> <p className="text-lg font-bold text-black">
{yacht.price} {formatMinCost(yacht.minCost)} / час
</p> </p>
<div className="flex items-center gap-1 text-sm text-gray-600 justify-end"> <div className="flex items-center gap-1 text-sm text-gray-600 justify-end">
<Icon <Icon
@ -321,7 +438,7 @@ export default function CatalogPage() {
name="anchor" name="anchor"
/> />
<span> <span>
{yacht.feet} {formatSpeed(yacht.speed)}
</span> </span>
</div> </div>
</div> </div>
@ -330,6 +447,7 @@ export default function CatalogPage() {
</Card> </Card>
</Link> </Link>
))} ))}
</>)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { DatePicker } from "@/components/ui/date-picker"; import { DatePicker } from "@/components/ui/date-picker";
import Icon from "@/components/ui/icon"; import Icon from "@/components/ui/icon";
import { GuestPicker } from "@/components/form/guest-picker"; import { GuestPicker } from "@/components/form/guest-picker";
import Link from "next/link";
export default function Hero() { export default function Hero() {
const [adults, setAdults] = useState<number>(0); const [adults, setAdults] = useState<number>(0);
@ -34,11 +35,13 @@ export default function Hero() {
</div> </div>
{/* Кнопка для мобильных устройств */} {/* Кнопка для мобильных устройств */}
<Link href="/catalog">
<div className="md:hidden flex justify-center"> <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 variant="gradient" className="font-bold text-white h-[64px] border-0 px-8 text-lg w-full">
Выберите яхту Выберите яхту
</Button> </Button>
</div> </div>
</Link>
{/* Поисковая форма - скрыта на мобильных устройствах */} {/* Поисковая форма - скрыта на мобильных устройствах */}
<Card className="bg-white shadow-lg s hidden md:block rounded-full"> <Card className="bg-white shadow-lg s hidden md:block rounded-full">

View File

@ -52,10 +52,10 @@ export default function YachtGrid() {
{/* Yacht Grid */} {/* Yacht Grid */}
{yachtCatalog && ( {yachtCatalog && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"> <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 <Link
key={idx} key={yacht.id}
href={`/catalog/${idx + 1}`} href={`/catalog/${yacht.id ?? 0}}`}
className="block" className="block"
> >
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg"> <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" className="w-full h-48 object-cover"
/> />
{/* Badge Overlay */} {/* Badge Overlay */}
{yacht.hasQuickRent && !yacht.topText && ( {!yacht.hasQuickRent && !yacht.topText && (
<> <>
<div className="absolute top-3 left-3"> <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"> <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">