Каталог, интеграция
This commit is contained in:
parent
7181718b0d
commit
7f6c6d1107
|
|
@ -15,4 +15,9 @@ interface CatalogItemDto {
|
||||||
interface MainPageCatalogResponseDto {
|
interface MainPageCatalogResponseDto {
|
||||||
featuredYacht: CatalogItemDto;
|
featuredYacht: CatalogItemDto;
|
||||||
restYachts: CatalogItemDto[];
|
restYachts: CatalogItemDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CatalogFilteredResponseDto {
|
||||||
|
items: CatalogItemDto[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
Применить
|
Применить
|
||||||
|
|
|
||||||
|
|
@ -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,84 +376,78 @@ 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 && (<>
|
||||||
<Link
|
{yachtCatalog.items.map((yacht) => (
|
||||||
key={idx}
|
<Link
|
||||||
href={`/catalog/${idx + 1}`}
|
key={yacht.id}
|
||||||
className="block"
|
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">
|
<Card className="overflow-hidden bg-white text-gray-900 border border-gray-200 cursor-pointer transition-all duration-200 hover:shadow-lg">
|
||||||
<div className="relative">
|
<CardHeader className="p-0 relative">
|
||||||
{/* Quick Booking Badge */}
|
<div className="relative">
|
||||||
{yacht.quickBooking && (
|
<Image
|
||||||
<div className="absolute top-3 left-3 z-10">
|
src={getImageUrl(yacht.mainImageUrl)}
|
||||||
<div className="bg-[#008299] text-white px-3 py-1 rounded-lg text-sm font-medium">
|
alt={yacht.name}
|
||||||
Быстрая бронь
|
width={400}
|
||||||
|
height={250}
|
||||||
|
className="w-full h-48 object-cover"
|
||||||
|
/>
|
||||||
|
{/* Badge Overlay */}
|
||||||
|
{!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
|
||||||
|
size={16}
|
||||||
|
name="restart"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
По запросу
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
<Image
|
</CardHeader>
|
||||||
src={yacht.img}
|
<CardContent className="p-4">
|
||||||
alt={yacht.name}
|
<div className="flex justify-between gap-4">
|
||||||
width={400}
|
{/* Левая колонка - название и длина */}
|
||||||
height={250}
|
<div className="space-y-2">
|
||||||
className="w-full h-48 object-cover"
|
<h3 className="font-bold text-lg">
|
||||||
/>
|
{yacht.name}
|
||||||
{/* Badge Overlay */}
|
</h3>
|
||||||
{yacht.badge && (
|
<div className="flex items-center gap-1 text-sm text-gray-600">
|
||||||
<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
|
<Icon
|
||||||
size={16}
|
size={16}
|
||||||
name="restart"
|
name="width"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{yacht.badge}
|
{formatWidth(yacht.length)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex justify-between gap-4">
|
|
||||||
{/* Левая колонка - название и длина */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="font-bold text-lg">
|
|
||||||
{yacht.name}
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-1 text-sm text-gray-600">
|
|
||||||
<Icon
|
|
||||||
size={16}
|
|
||||||
name="width"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
{yacht.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Правая колонка - цена и футы */}
|
{/* Правая колонка - цена и футы */}
|
||||||
<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
|
||||||
size={16}
|
size={16}
|
||||||
name="anchor"
|
name="anchor"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{yacht.feet}
|
{formatSpeed(yacht.speed)}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
</Link>
|
||||||
</Link>
|
))}
|
||||||
))}
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
{/* Кнопка для мобильных устройств */}
|
{/* Кнопка для мобильных устройств */}
|
||||||
<div className="md:hidden flex justify-center">
|
<Link href="/catalog">
|
||||||
<Button variant="gradient" className="font-bold text-white h-[64px] border-0 px-8 text-lg w-full">
|
<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>
|
</Button>
|
||||||
|
</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">
|
||||||
|
|
@ -65,7 +68,7 @@ export default function Hero() {
|
||||||
|
|
||||||
{/* Количество гостей */}
|
{/* Количество гостей */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<GuestPicker
|
<GuestPicker
|
||||||
adults={adults}
|
adults={adults}
|
||||||
childrenCount={children}
|
childrenCount={children}
|
||||||
onChange={(adults, children) => {
|
onChange={(adults, children) => {
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue