Интеграция с бэком на главной странице
This commit is contained in:
parent
85c23f136e
commit
7181718b0d
|
|
@ -1,6 +1,16 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "http",
|
||||||
|
hostname: "89.169.188.2",
|
||||||
|
pathname: '/**'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
unoptimized: false,
|
||||||
|
},
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
interface CatalogItemDto {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
length: number;
|
||||||
|
speed: number;
|
||||||
|
minCost: number;
|
||||||
|
mainImageUrl: string;
|
||||||
|
galleryUrls: string[];
|
||||||
|
hasQuickRent: boolean;
|
||||||
|
isFeatured: boolean;
|
||||||
|
topText?: string;
|
||||||
|
isBestOffer?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MainPageCatalogResponseDto {
|
||||||
|
featuredYacht: CatalogItemDto;
|
||||||
|
restYachts: CatalogItemDto[];
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
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";
|
||||||
|
|
@ -15,6 +16,10 @@ interface CatalogSidebarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [lengthRange, setLengthRange] = useState([7, 50]);
|
const [lengthRange, setLengthRange] = useState([7, 50]);
|
||||||
const [priceRange, setPriceRange] = useState([3000, 200000]);
|
const [priceRange, setPriceRange] = useState([3000, 200000]);
|
||||||
const [yearRange, setYearRange] = useState([1991, 2025]);
|
const [yearRange, setYearRange] = useState([1991, 2025]);
|
||||||
|
|
@ -24,6 +29,138 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
||||||
const [quickBooking, setQuickBooking] = useState(false);
|
const [quickBooking, setQuickBooking] = useState(false);
|
||||||
const [hasToilet, setHasToilet] = useState(false);
|
const [hasToilet, setHasToilet] = useState(false);
|
||||||
const [vesselType, setVesselType] = useState("");
|
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) + " Р";
|
||||||
|
|
@ -39,6 +176,11 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
||||||
setQuickBooking(false);
|
setQuickBooking(false);
|
||||||
setHasToilet(false);
|
setHasToilet(false);
|
||||||
setVesselType("");
|
setVesselType("");
|
||||||
|
setDate(null);
|
||||||
|
setDepartureTime("12:00");
|
||||||
|
setArrivalTime("13:00");
|
||||||
|
// Очищаем URL параметры без прокрутки страницы
|
||||||
|
router.replace(pathname, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeFiltersCount = [
|
const activeFiltersCount = [
|
||||||
|
|
@ -50,6 +192,9 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
||||||
quickBooking,
|
quickBooking,
|
||||||
hasToilet,
|
hasToilet,
|
||||||
vesselType !== "",
|
vesselType !== "",
|
||||||
|
date !== null,
|
||||||
|
departureTime !== "12:00",
|
||||||
|
arrivalTime !== "13:00",
|
||||||
].filter(Boolean).length;
|
].filter(Boolean).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -111,7 +256,16 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
||||||
|
|
||||||
{/* Дата */}
|
{/* Дата */}
|
||||||
<div>
|
<div>
|
||||||
<DatePicker variant="small" placeholder="Дата" />
|
<DatePicker
|
||||||
|
variant="small"
|
||||||
|
placeholder="Дата"
|
||||||
|
value={date}
|
||||||
|
departureTime={departureTime}
|
||||||
|
arrivalTime={arrivalTime}
|
||||||
|
onDateChange={(newDate) => setDate(newDate || null)}
|
||||||
|
onDepartureTimeChange={setDepartureTime}
|
||||||
|
onArrivalTimeChange={setArrivalTime}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Гостей */}
|
{/* Гостей */}
|
||||||
|
|
@ -259,7 +413,7 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
||||||
|
|
||||||
{/* Кнопка Применить */}
|
{/* Кнопка Применить */}
|
||||||
<Button
|
<Button
|
||||||
onClick={onApply}
|
onClick={handleApplyFilters}
|
||||||
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"
|
||||||
>
|
>
|
||||||
Применить
|
Применить
|
||||||
|
|
|
||||||
|
|
@ -13,32 +13,10 @@ import Image from "next/image";
|
||||||
import Icon from "@/components/ui/icon";
|
import Icon from "@/components/ui/icon";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GuestDatePicker } from "@/components/form/guest-date-picker";
|
import { GuestDatePicker } from "@/components/form/guest-date-picker";
|
||||||
|
import { formatMinCost, formatWidth, getImageUrl } from "@/lib/utils";
|
||||||
|
|
||||||
const yacht = {
|
export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
|
||||||
name: "Яхта",
|
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
|
||||||
length: "12 метров",
|
|
||||||
price: "от 18 000 ₽",
|
|
||||||
perTime: "/ час",
|
|
||||||
feet: "7 Футов",
|
|
||||||
mainImage: "/images/featured-yacht/featured1.png",
|
|
||||||
thumbnails: [
|
|
||||||
"/images/featured-yacht/featured1.png",
|
|
||||||
"/images/featured-yacht/featured2.png",
|
|
||||||
"/images/featured-yacht/featured3.png",
|
|
||||||
"/images/featured-yacht/featured4.png",
|
|
||||||
"/images/featured-yacht/featured5.png",
|
|
||||||
"/images/featured-yacht/featured6.png",
|
|
||||||
"/images/featured-yacht/featured7.png",
|
|
||||||
"/images/featured-yacht/featured8.png",
|
|
||||||
"/images/featured-yacht/featured9.png",
|
|
||||||
"/images/featured-yacht/featured10.png",
|
|
||||||
],
|
|
||||||
isPromoted: true,
|
|
||||||
totalPrice: "0 ₽",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function FeaturedYacht() {
|
|
||||||
const [selectedImage, setSelectedImage] = useState(yacht.mainImage);
|
|
||||||
|
|
||||||
const handleThumbnailClick = (imageSrc: string) => {
|
const handleThumbnailClick = (imageSrc: string) => {
|
||||||
setSelectedImage(imageSrc);
|
setSelectedImage(imageSrc);
|
||||||
|
|
@ -75,7 +53,7 @@ export default function FeaturedYacht() {
|
||||||
<div className="flex items-center gap-2 text-gray-600">
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
<Icon size={16} name="width" />
|
<Icon size={16} name="width" />
|
||||||
<span className="text-lg">
|
<span className="text-lg">
|
||||||
{yacht.length}
|
{formatWidth(yacht.length)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,11 +61,12 @@ export default function FeaturedYacht() {
|
||||||
{/* Main yacht image */}
|
{/* Main yacht image */}
|
||||||
<div className="relative mb-6">
|
<div className="relative mb-6">
|
||||||
<Image
|
<Image
|
||||||
src={selectedImage}
|
src={getImageUrl(selectedImage)}
|
||||||
alt={yacht.name}
|
alt={yacht.name}
|
||||||
width={600}
|
width={600}
|
||||||
height={400}
|
height={400}
|
||||||
className="w-full h-80 object-cover rounded-[24px]"
|
className="w-full h-80 object-cover rounded-[24px]"
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -102,30 +81,29 @@ export default function FeaturedYacht() {
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<CarouselContent className="-ml-2 md:-ml-4">
|
<CarouselContent className="-ml-2 md:-ml-4">
|
||||||
{yacht.thumbnails.map((thumb, idx) => (
|
{yacht.galleryUrls.map((thumb, idx) => (
|
||||||
<CarouselItem
|
<CarouselItem
|
||||||
key={idx}
|
key={idx}
|
||||||
className="pl-2 md:pl-4 basis-auto"
|
className="pl-2 md:pl-4 basis-auto"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Image
|
<Image
|
||||||
src={thumb}
|
src={getImageUrl(thumb)}
|
||||||
alt={`${
|
alt={`${yacht.name
|
||||||
yacht.name
|
} view ${idx + 1}`}
|
||||||
} view ${idx + 1}`}
|
|
||||||
width={80}
|
width={80}
|
||||||
height={60}
|
height={60}
|
||||||
className={`w-20 h-16 object-cover rounded-[8px] cursor-pointer border-2 transition-all ${
|
className={`w-20 h-16 object-cover rounded-[8px] cursor-pointer border-2 transition-all ${selectedImage ===
|
||||||
selectedImage ===
|
|
||||||
thumb
|
thumb
|
||||||
? "border-[#008299]"
|
? "border-[#008299]"
|
||||||
: "border-gray-200 hover:border-gray-400"
|
: "border-gray-200 hover:border-gray-400"
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleThumbnailClick(
|
handleThumbnailClick(
|
||||||
thumb
|
thumb
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CarouselItem>
|
</CarouselItem>
|
||||||
|
|
@ -137,7 +115,7 @@ export default function FeaturedYacht() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Promoted badge */}
|
{/* Promoted badge */}
|
||||||
{yacht.isPromoted && (
|
{yacht.isFeatured && (
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||||
<Icon
|
<Icon
|
||||||
className="min-w-[21px] min-h-[21px]"
|
className="min-w-[21px] min-h-[21px]"
|
||||||
|
|
@ -176,10 +154,10 @@ export default function FeaturedYacht() {
|
||||||
<div className="border rounded-[16px] p-6 pb-8 border-gray-200 pt-6">
|
<div className="border rounded-[16px] p-6 pb-8 border-gray-200 pt-6">
|
||||||
{/* Price */}
|
{/* Price */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="text-3xl font-bold">
|
<p className="text-3xl font-bold whitespace-nowrap">
|
||||||
{yacht.price}
|
{formatMinCost(yacht.minCost)}
|
||||||
<span className="text-sm font-normal text-gray-500">
|
<span className="text-sm font-normal text-gray-500">
|
||||||
{yacht.perTime}
|
/ час
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -202,7 +180,7 @@ export default function FeaturedYacht() {
|
||||||
<span className="font-normal">
|
<span className="font-normal">
|
||||||
Итого:
|
Итого:
|
||||||
</span>
|
</span>
|
||||||
<span>{yacht.totalPrice}</span>
|
<span>0 ₽</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,29 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Icon from "@/components/ui/icon";
|
import Icon from "@/components/ui/icon";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import FeaturedYacht from "./FeaturedYacht";
|
import FeaturedYacht from "./FeaturedYacht";
|
||||||
|
import useApiClient from "@/hooks/useApiClient";
|
||||||
const yachts = [
|
import { useEffect, useState } from "react";
|
||||||
{
|
import { formatMinCost, formatSpeed, formatWidth, getImageUrl } from "@/lib/utils";
|
||||||
name: "Яхта",
|
|
||||||
length: "12 метров",
|
|
||||||
price: "от 12 500 ₽ / час",
|
|
||||||
feet: "7 Футов",
|
|
||||||
img: "/images/yachts/yacht1.jpg",
|
|
||||||
bestOfferText: "🔥 Лучшее предложение",
|
|
||||||
colorPrice: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Яхта",
|
|
||||||
length: "14 метров",
|
|
||||||
price: "от 26 400 ₽ / час",
|
|
||||||
feet: "7 Футов",
|
|
||||||
img: "/images/yachts/yacht2.jpg",
|
|
||||||
bestOfferText: "🍷 Идеальна для заката с бокалом вина",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Яхта",
|
|
||||||
length: "22 метра",
|
|
||||||
price: "от 48 000 ₽ / час",
|
|
||||||
feet: "7 Футов",
|
|
||||||
img: "/images/yachts/yacht3.jpg",
|
|
||||||
bestOfferText: "⌛ Часто бронируется — успей",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Яхта",
|
|
||||||
length: "13.6 метров",
|
|
||||||
price: "от 17 400 ₽ / час",
|
|
||||||
feet: "7 Футов",
|
|
||||||
img: "/images/yachts/yacht4.jpg",
|
|
||||||
badge: "По запросу",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Яхта",
|
|
||||||
length: "13 метров",
|
|
||||||
price: "от 14 400 ₽ / час",
|
|
||||||
feet: "7 Футов",
|
|
||||||
img: "/images/yachts/yacht5.jpg",
|
|
||||||
badge: "По запросу",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Яхта",
|
|
||||||
length: "12 метров",
|
|
||||||
price: "от 12 480 ₽ / час",
|
|
||||||
feet: "7 Футов",
|
|
||||||
img: "/images/yachts/yacht6.jpg",
|
|
||||||
badge: "По запросу",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function YachtGrid() {
|
export default function YachtGrid() {
|
||||||
|
const client = useApiClient();
|
||||||
|
|
||||||
|
const [featuredYacht, setFeaturedYacht] = useState<CatalogItemDto | null>(null);
|
||||||
|
const [yachtCatalog, setYachtCatalog] = useState<CatalogItemDto[] | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const response = await client.get<MainPageCatalogResponseDto>("/catalog/main-page/");
|
||||||
|
setFeaturedYacht(response.data.featuredYacht);
|
||||||
|
setYachtCatalog(response.data.restYachts);
|
||||||
|
})();
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="text-white">
|
<section className="text-white">
|
||||||
<div className="container max-w-6xl mx-auto px-4 mt-6 md:mt-12">
|
<div className="container max-w-6xl mx-auto px-4 mt-6 md:mt-12">
|
||||||
|
|
@ -79,103 +45,107 @@ export default function YachtGrid() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Featured Yacht Block */}
|
{/* Featured Yacht Block */}
|
||||||
<FeaturedYacht />
|
{featuredYacht && (
|
||||||
|
<FeaturedYacht yacht={featuredYacht} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Yacht Grid */}
|
{/* Yacht Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
|
{yachtCatalog && (
|
||||||
{yachts.map((yacht, idx) => (
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
|
||||||
<Link
|
{yachtCatalog.map((yacht, idx) => (
|
||||||
key={idx}
|
<Link
|
||||||
href={`/catalog/${idx + 1}`}
|
key={idx}
|
||||||
className="block"
|
href={`/catalog/${idx + 1}`}
|
||||||
>
|
className="block"
|
||||||
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg">
|
>
|
||||||
<CardHeader className="p-0 relative">
|
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg">
|
||||||
<div className="relative">
|
<CardHeader className="p-0 relative">
|
||||||
{/* Best Offer Badge - над карточкой */}
|
<div className="relative">
|
||||||
{yacht.bestOfferText && (
|
{/* Best Offer Badge - над карточкой */}
|
||||||
<div className="w-full flex justify-center">
|
{yacht.topText && (
|
||||||
<div
|
<div className="w-full flex justify-center">
|
||||||
className="flex w-full items-center justify-center text-white px-4 py-2 text-sm font-medium bg-cover bg-center bg-no-repeat"
|
<div
|
||||||
style={{
|
className="flex w-full items-center justify-center text-white px-4 py-2 text-sm font-medium bg-cover bg-center bg-no-repeat"
|
||||||
backgroundImage:
|
style={{
|
||||||
"url('/images/best-yacht-bg.jpg')",
|
backgroundImage:
|
||||||
}}
|
"url('/images/best-yacht-bg.jpg')",
|
||||||
>
|
}}
|
||||||
<span>
|
>
|
||||||
{yacht.bestOfferText}
|
|
||||||
</span>
|
|
||||||
</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 flex items-center gap-1">
|
|
||||||
<Icon
|
|
||||||
size={16}
|
|
||||||
name="restart"
|
|
||||||
/>
|
|
||||||
<span>
|
<span>
|
||||||
{yacht.badge}
|
{yacht.topText}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
<Image
|
||||||
</div>
|
src={getImageUrl(yacht.mainImageUrl)}
|
||||||
</CardHeader>
|
alt={yacht.name}
|
||||||
<CardContent className="p-4">
|
width={400}
|
||||||
<div className="flex justify-between gap-4">
|
height={250}
|
||||||
{/* Левая колонка - название и длина */}
|
className="w-full h-48 object-cover"
|
||||||
<div className="space-y-2">
|
/>
|
||||||
<h3 className="font-bold text-l">
|
{/* Badge Overlay */}
|
||||||
{yacht.name}
|
{yacht.hasQuickRent && !yacht.topText && (
|
||||||
</h3>
|
<>
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="absolute top-3 left-3">
|
||||||
<Icon size={16} name="width" />
|
<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">
|
||||||
<span>{yacht.length}</span>
|
<Icon
|
||||||
</div>
|
size={16}
|
||||||
|
name="restart"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
По запросу
|
||||||
|
</span>
|
||||||
|
</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-l">
|
||||||
|
{yacht.name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-1 text-sm">
|
||||||
|
<Icon size={16} name="width" />
|
||||||
|
<span>{formatWidth(yacht.length)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Правая колонка - цена и футы */}
|
{/* Правая колонка - цена и футы */}
|
||||||
<div className="space-y-2">
|
<div className="flex flex-col justify-between">
|
||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
{yacht.colorPrice ? (
|
{yacht.isBestOffer ? (
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"linear-gradient(90deg, #008299 0%, #7E8FFF 100%)",
|
"linear-gradient(90deg, #008299 0%, #7E8FFF 100%)",
|
||||||
}}
|
}}
|
||||||
className="text-l font-bold text-white pl-2 pr-3 rounded-t-[5px] rounded-bl-[10px] rounded-br-[30px]"
|
className="text-l font-bold text-white pl-2 pr-3 rounded-t-[5px] rounded-bl-[10px] rounded-br-[30px] whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{yacht.price}
|
{formatMinCost(yacht.minCost)} / час
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="w-fit text-l">
|
<p className="w-fit text-l whitespace-nowrap">
|
||||||
{yacht.price}
|
{formatMinCost(yacht.minCost)} / час
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
<Icon size={16} name="anchor" />
|
<Icon size={16} name="anchor" />
|
||||||
<span>{yacht.feet}</span>
|
<span>{formatSpeed(yacht.speed)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
</Link>
|
||||||
</Link>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Call to Action Button */}
|
{/* Call to Action Button */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,88 @@ import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { User, ArrowUpRight, Map, ArrowLeft, Heart } from "lucide-react";
|
import { User, ArrowUpRight, Map, ArrowLeft, Heart } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
export default function ConfirmPage() {
|
export default function ConfirmPage() {
|
||||||
const [promocode, setPromocode] = useState("");
|
const [promocode, setPromocode] = useState("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Извлекаем параметры из URL
|
||||||
|
const yachtId = searchParams.get("yachtId");
|
||||||
|
const guestCount = searchParams.get("guestCount");
|
||||||
|
const departureDate = searchParams.get("departureDate");
|
||||||
|
const departureTime = searchParams.get("departureTime");
|
||||||
|
const arrivalDate = searchParams.get("arrivalDate");
|
||||||
|
const arrivalTime = searchParams.get("arrivalTime");
|
||||||
|
|
||||||
|
// Функция для форматирования даты (краткий формат)
|
||||||
|
const formatDate = (dateString: string | null) => {
|
||||||
|
if (!dateString) return null;
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const months = [
|
||||||
|
"янв", "фев", "мар", "апр", "май", "июн",
|
||||||
|
"июл", "авг", "сен", "окт", "ноя", "дек"
|
||||||
|
];
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = months[date.getMonth()];
|
||||||
|
return `${day} ${month}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для форматирования даты (полный формат для десктопа)
|
||||||
|
const formatDateFull = (dateString: string | null) => {
|
||||||
|
if (!dateString) return null;
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const months = [
|
||||||
|
"января", "февраля", "марта", "апреля", "мая", "июня",
|
||||||
|
"июля", "августа", "сентября", "октября", "ноября", "декабря"
|
||||||
|
];
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = months[date.getMonth()];
|
||||||
|
return `${day} ${month}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для форматирования времени
|
||||||
|
const formatTime = (timeString: string | null) => {
|
||||||
|
if (!timeString) return null;
|
||||||
|
// Предполагаем формат HH:mm или HH:mm:ss
|
||||||
|
return timeString.split(":").slice(0, 2).join(":");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Форматируем данные для отображения
|
||||||
|
const departureDateFormatted = formatDate(departureDate);
|
||||||
|
const departureTimeFormatted = formatTime(departureTime);
|
||||||
|
const arrivalDateFormatted = formatDate(arrivalDate);
|
||||||
|
const arrivalTimeFormatted = formatTime(arrivalTime);
|
||||||
|
|
||||||
|
// Полный формат для десктопной версии
|
||||||
|
const departureDateFormattedFull = formatDateFull(departureDate);
|
||||||
|
const arrivalDateFormattedFull = formatDateFull(arrivalDate);
|
||||||
|
|
||||||
|
// Формируем строки для отображения
|
||||||
|
const departureDisplay = departureDateFormatted && departureTimeFormatted
|
||||||
|
? `${departureDateFormatted} ${departureTimeFormatted}`
|
||||||
|
: "Не выбрано";
|
||||||
|
|
||||||
|
const arrivalDisplay = arrivalDateFormatted && arrivalTimeFormatted
|
||||||
|
? `${arrivalDateFormatted} ${arrivalTimeFormatted}`
|
||||||
|
: "Не выбрано";
|
||||||
|
|
||||||
|
const datesDisplay = departureDateFormattedFull && departureTimeFormatted && arrivalDateFormattedFull && arrivalTimeFormatted
|
||||||
|
? `${departureDateFormattedFull} в ${departureTimeFormatted} — ${arrivalDateFormattedFull} в ${arrivalTimeFormatted}`
|
||||||
|
: "Не выбрано";
|
||||||
|
|
||||||
|
const guestsDisplay = guestCount
|
||||||
|
? guestCount === "1" ? "1 гость" : `${guestCount} гостей`
|
||||||
|
: "Не выбрано";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="bg-[#f4f4f4] grow">
|
<main className="bg-[#f4f4f4] grow">
|
||||||
|
|
@ -36,8 +113,8 @@ export default function ConfirmPage() {
|
||||||
Яхта Сеньорита
|
Яхта Сеньорита
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex justify-center gap-10 text-xs text-[#666666]">
|
<div className="flex justify-center gap-10 text-xs text-[#666666]">
|
||||||
<span>09 авг.</span>
|
<span>{departureDateFormatted || "Не выбрано"}</span>
|
||||||
<span>Гостей: 1</span>
|
<span>Гостей: {guestCount || "Не выбрано"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -69,7 +146,7 @@ export default function ConfirmPage() {
|
||||||
</label>
|
</label>
|
||||||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
||||||
<div className="text-[#333333]">
|
<div className="text-[#333333]">
|
||||||
9 Авг 00:00
|
{departureDisplay}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -79,7 +156,7 @@ export default function ConfirmPage() {
|
||||||
</label>
|
</label>
|
||||||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
||||||
<div className="text-[#333333]">
|
<div className="text-[#333333]">
|
||||||
9 Авг 02:00
|
{arrivalDisplay}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -99,7 +176,7 @@ export default function ConfirmPage() {
|
||||||
</label>
|
</label>
|
||||||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF] flex items-center justify-between">
|
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF] flex items-center justify-between">
|
||||||
<span className="text-[#333333]">
|
<span className="text-[#333333]">
|
||||||
1 гость
|
{guestsDisplay}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -316,8 +393,7 @@ export default function ConfirmPage() {
|
||||||
Даты
|
Даты
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base text-[#999999]">
|
<div className="text-base text-[#999999]">
|
||||||
9 августа в 00:00 — 9 августа в
|
{datesDisplay}
|
||||||
02:00
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -328,7 +404,7 @@ export default function ConfirmPage() {
|
||||||
Гости
|
Гости
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base text-[#999999]">
|
<div className="text-base text-[#999999]">
|
||||||
1 гость
|
{guestsDisplay}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,59 @@ interface DatePickerProps {
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
variant?: "default" | "small";
|
variant?: "default" | "small";
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
value?: Date | null;
|
||||||
|
departureTime?: string;
|
||||||
|
arrivalTime?: string;
|
||||||
|
onDateChange?: (date: Date | undefined) => void;
|
||||||
|
onDepartureTimeChange?: (time: string) => void;
|
||||||
|
onArrivalTimeChange?: (time: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DatePicker({ showIcon = true, variant = "default", placeholder = "Выберите дату и время" }: DatePickerProps) {
|
export function DatePicker({
|
||||||
const [date, setDate] = React.useState<Date>();
|
showIcon = true,
|
||||||
const [departureTime, setDepartureTime] = React.useState("12:00");
|
variant = "default",
|
||||||
const [arrivalTime, setArrivalTime] = React.useState("13:00");
|
placeholder = "Выберите дату и время",
|
||||||
|
value,
|
||||||
|
departureTime: externalDepartureTime,
|
||||||
|
arrivalTime: externalArrivalTime,
|
||||||
|
onDateChange,
|
||||||
|
onDepartureTimeChange,
|
||||||
|
onArrivalTimeChange,
|
||||||
|
}: DatePickerProps) {
|
||||||
|
const [internalDate, setInternalDate] = React.useState<Date>();
|
||||||
|
const [internalDepartureTime, setInternalDepartureTime] = React.useState("12:00");
|
||||||
|
const [internalArrivalTime, setInternalArrivalTime] = React.useState("13:00");
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
// Используем внешние значения, если они предоставлены, иначе внутренние
|
||||||
|
const date = value !== undefined ? (value || undefined) : internalDate;
|
||||||
|
const departureTime = externalDepartureTime !== undefined ? externalDepartureTime : internalDepartureTime;
|
||||||
|
const arrivalTime = externalArrivalTime !== undefined ? externalArrivalTime : internalArrivalTime;
|
||||||
|
|
||||||
|
const handleDateChange = (newDate: Date | undefined) => {
|
||||||
|
if (onDateChange) {
|
||||||
|
onDateChange(newDate);
|
||||||
|
} else {
|
||||||
|
setInternalDate(newDate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDepartureTimeChange = (time: string) => {
|
||||||
|
if (onDepartureTimeChange) {
|
||||||
|
onDepartureTimeChange(time);
|
||||||
|
} else {
|
||||||
|
setInternalDepartureTime(time);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleArrivalTimeChange = (time: string) => {
|
||||||
|
if (onArrivalTimeChange) {
|
||||||
|
onArrivalTimeChange(time);
|
||||||
|
} else {
|
||||||
|
setInternalArrivalTime(time);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
// Закрываем popover после применения
|
// Закрываем popover после применения
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
@ -71,7 +116,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={date}
|
selected={date}
|
||||||
onSelect={setDate}
|
onSelect={handleDateChange}
|
||||||
className="mb-4 "
|
className="mb-4 "
|
||||||
locale={ru}
|
locale={ru}
|
||||||
disabled={(date) =>
|
disabled={(date) =>
|
||||||
|
|
@ -112,7 +157,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
|
||||||
type="time"
|
type="time"
|
||||||
value={departureTime}
|
value={departureTime}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setDepartureTime(e.target.value)
|
handleDepartureTimeChange(e.target.value)
|
||||||
}
|
}
|
||||||
className="w-full focus:outline-none focus:border-transparent"
|
className="w-full focus:outline-none focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
|
|
@ -129,7 +174,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
|
||||||
type="time"
|
type="time"
|
||||||
value={arrivalTime}
|
value={arrivalTime}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setArrivalTime(e.target.value)
|
handleArrivalTimeChange(e.target.value)
|
||||||
}
|
}
|
||||||
className="w-full focus:outline-none focus:border-transparent"
|
className="w-full focus:outline-none focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const useApiClient = () => {
|
||||||
const authPopup = useAuthPopup();
|
const authPopup = useAuthPopup();
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: "/api",
|
baseURL: "http://192.168.1.5:4000/",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,28 @@ import { twMerge } from "tailwind-merge"
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const API_BASE_URL = "http://89.169.188.2";
|
||||||
|
|
||||||
|
export const getImageUrl = (relativePath: string): string => {
|
||||||
|
if (!relativePath) return "";
|
||||||
|
// Если путь уже абсолютный, возвращаем как есть
|
||||||
|
if (relativePath.startsWith("http://") || relativePath.startsWith("https://")) {
|
||||||
|
return relativePath;
|
||||||
|
}
|
||||||
|
// Убираем начальный слеш, если есть, и формируем абсолютный URL
|
||||||
|
const cleanPath = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
|
||||||
|
return `${API_BASE_URL}/${cleanPath}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatWidth = (width: number): string => {
|
||||||
|
return width + " метров";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatSpeed = (speed: number): string => {
|
||||||
|
return speed + " футов";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatMinCost = (minCost: number): string => {
|
||||||
|
return "от " + minCost + " ₽";
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue