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

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

@ -15,4 +15,9 @@ interface CatalogItemDto {
interface MainPageCatalogResponseDto {
featuredYacht: CatalogItemDto;
restYachts: CatalogItemDto[];
}
}
interface CatalogFilteredResponseDto {
items: CatalogItemDto[];
total: number;
}

View File

@ -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"
>
Применить

View File

@ -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,84 +376,78 @@ export default function CatalogPage() {
{/* Yacht Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{yachts.map((yacht, idx) => (
<Link
key={idx}
href={`/catalog/${idx + 1}`}
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">
Быстрая бронь
{yachtCatalog && (<>
{yachtCatalog.items.map((yacht) => (
<Link
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">
<Image
src={getImageUrl(yacht.mainImageUrl)}
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>
)}
<Image
src={yacht.img}
alt={yacht.name}
width={400}
height={250}
className="w-full h-48 object-cover"
/>
{/* Badge Overlay */}
{yacht.badge && (
<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>
</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="restart"
name="width"
/>
<span>
{yacht.badge}
{formatWidth(yacht.length)}
</span>
</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">
<p className="text-lg font-bold text-black">
{yacht.price}
</p>
<div className="flex items-center gap-1 text-sm text-gray-600 justify-end">
<Icon
size={16}
name="anchor"
/>
<span>
{yacht.feet}
</span>
{/* Правая колонка - цена и футы */}
<div className="space-y-2 text-right">
<p className="text-lg font-bold text-black">
{formatMinCost(yacht.minCost)} / час
</p>
<div className="flex items-center gap-1 text-sm text-gray-600 justify-end">
<Icon
size={16}
name="anchor"
/>
<span>
{formatSpeed(yacht.speed)}
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</Link>
))}
</CardContent>
</Card>
</Link>
))}
</>)}
</div>
</div>
</div>

View File

@ -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>
{/* Кнопка для мобильных устройств */}
<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 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">
@ -65,7 +68,7 @@ export default function Hero() {
{/* Количество гостей */}
<div className="flex-1">
<GuestPicker
<GuestPicker
adults={adults}
childrenCount={children}
onChange={(adults, children) => {

View File

@ -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">