Интеграция с бэком на главной странице
This commit is contained in:
parent
85c23f136e
commit
7181718b0d
|
|
@ -1,6 +1,16 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "89.169.188.2",
|
||||
pathname: '/**'
|
||||
},
|
||||
],
|
||||
unoptimized: false,
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
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";
|
||||
|
||||
import { useState } from "react";
|
||||
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";
|
||||
|
|
@ -15,6 +16,10 @@ interface CatalogSidebarProps {
|
|||
}
|
||||
|
||||
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]);
|
||||
|
|
@ -24,6 +29,138 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
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) => {
|
||||
return new Intl.NumberFormat("ru-RU").format(value) + " Р";
|
||||
|
|
@ -39,6 +176,11 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
setQuickBooking(false);
|
||||
setHasToilet(false);
|
||||
setVesselType("");
|
||||
setDate(null);
|
||||
setDepartureTime("12:00");
|
||||
setArrivalTime("13:00");
|
||||
// Очищаем URL параметры без прокрутки страницы
|
||||
router.replace(pathname, { scroll: false });
|
||||
};
|
||||
|
||||
const activeFiltersCount = [
|
||||
|
|
@ -50,6 +192,9 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
quickBooking,
|
||||
hasToilet,
|
||||
vesselType !== "",
|
||||
date !== null,
|
||||
departureTime !== "12:00",
|
||||
arrivalTime !== "13:00",
|
||||
].filter(Boolean).length;
|
||||
|
||||
return (
|
||||
|
|
@ -111,7 +256,16 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
|
||||
{/* Дата */}
|
||||
<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>
|
||||
|
||||
{/* Гостей */}
|
||||
|
|
@ -259,7 +413,7 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
|
|||
|
||||
{/* Кнопка Применить */}
|
||||
<Button
|
||||
onClick={onApply}
|
||||
onClick={handleApplyFilters}
|
||||
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 { useState } from "react";
|
||||
import { GuestDatePicker } from "@/components/form/guest-date-picker";
|
||||
import { formatMinCost, formatWidth, getImageUrl } from "@/lib/utils";
|
||||
|
||||
const yacht = {
|
||||
name: "Яхта",
|
||||
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);
|
||||
export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
|
||||
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
|
||||
|
||||
const handleThumbnailClick = (imageSrc: string) => {
|
||||
setSelectedImage(imageSrc);
|
||||
|
|
@ -75,7 +53,7 @@ export default function FeaturedYacht() {
|
|||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Icon size={16} name="width" />
|
||||
<span className="text-lg">
|
||||
{yacht.length}
|
||||
{formatWidth(yacht.length)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -83,11 +61,12 @@ export default function FeaturedYacht() {
|
|||
{/* Main yacht image */}
|
||||
<div className="relative mb-6">
|
||||
<Image
|
||||
src={selectedImage}
|
||||
src={getImageUrl(selectedImage)}
|
||||
alt={yacht.name}
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-80 object-cover rounded-[24px]"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -102,30 +81,29 @@ export default function FeaturedYacht() {
|
|||
className="w-full"
|
||||
>
|
||||
<CarouselContent className="-ml-2 md:-ml-4">
|
||||
{yacht.thumbnails.map((thumb, idx) => (
|
||||
{yacht.galleryUrls.map((thumb, idx) => (
|
||||
<CarouselItem
|
||||
key={idx}
|
||||
className="pl-2 md:pl-4 basis-auto"
|
||||
>
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={thumb}
|
||||
alt={`${
|
||||
yacht.name
|
||||
} view ${idx + 1}`}
|
||||
src={getImageUrl(thumb)}
|
||||
alt={`${yacht.name
|
||||
} view ${idx + 1}`}
|
||||
width={80}
|
||||
height={60}
|
||||
className={`w-20 h-16 object-cover rounded-[8px] cursor-pointer border-2 transition-all ${
|
||||
selectedImage ===
|
||||
className={`w-20 h-16 object-cover rounded-[8px] cursor-pointer border-2 transition-all ${selectedImage ===
|
||||
thumb
|
||||
? "border-[#008299]"
|
||||
: "border-gray-200 hover:border-gray-400"
|
||||
}`}
|
||||
? "border-[#008299]"
|
||||
: "border-gray-200 hover:border-gray-400"
|
||||
}`}
|
||||
onClick={() =>
|
||||
handleThumbnailClick(
|
||||
thumb
|
||||
)
|
||||
}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
|
|
@ -137,7 +115,7 @@ export default function FeaturedYacht() {
|
|||
</div>
|
||||
|
||||
{/* Promoted badge */}
|
||||
{yacht.isPromoted && (
|
||||
{yacht.isFeatured && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Icon
|
||||
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">
|
||||
{/* Price */}
|
||||
<div className="mb-6">
|
||||
<p className="text-3xl font-bold">
|
||||
{yacht.price}
|
||||
<p className="text-3xl font-bold whitespace-nowrap">
|
||||
{formatMinCost(yacht.minCost)}
|
||||
<span className="text-sm font-normal text-gray-500">
|
||||
{yacht.perTime}
|
||||
/ час
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -202,7 +180,7 @@ export default function FeaturedYacht() {
|
|||
<span className="font-normal">
|
||||
Итого:
|
||||
</span>
|
||||
<span>{yacht.totalPrice}</span>
|
||||
<span>0 ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,63 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import Icon from "@/components/ui/icon";
|
||||
import Link from "next/link";
|
||||
import FeaturedYacht from "./FeaturedYacht";
|
||||
|
||||
const yachts = [
|
||||
{
|
||||
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: "По запросу",
|
||||
},
|
||||
];
|
||||
import useApiClient from "@/hooks/useApiClient";
|
||||
import { useEffect, useState } from "react";
|
||||
import { formatMinCost, formatSpeed, formatWidth, getImageUrl } from "@/lib/utils";
|
||||
|
||||
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 (
|
||||
<section className="text-white">
|
||||
<div className="container max-w-6xl mx-auto px-4 mt-6 md:mt-12">
|
||||
|
|
@ -79,103 +45,107 @@ export default function YachtGrid() {
|
|||
</div>
|
||||
|
||||
{/* Featured Yacht Block */}
|
||||
<FeaturedYacht />
|
||||
{featuredYacht && (
|
||||
<FeaturedYacht yacht={featuredYacht} />
|
||||
)}
|
||||
|
||||
{/* Yacht Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
|
||||
{yachts.map((yacht, idx) => (
|
||||
<Link
|
||||
key={idx}
|
||||
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">
|
||||
<div className="relative">
|
||||
{/* Best Offer Badge - над карточкой */}
|
||||
{yacht.bestOfferText && (
|
||||
<div className="w-full flex justify-center">
|
||||
<div
|
||||
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"
|
||||
style={{
|
||||
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"
|
||||
/>
|
||||
{yachtCatalog && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
|
||||
{yachtCatalog.map((yacht, idx) => (
|
||||
<Link
|
||||
key={idx}
|
||||
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">
|
||||
<div className="relative">
|
||||
{/* Best Offer Badge - над карточкой */}
|
||||
{yacht.topText && (
|
||||
<div className="w-full flex justify-center">
|
||||
<div
|
||||
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"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('/images/best-yacht-bg.jpg')",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{yacht.badge}
|
||||
{yacht.topText}
|
||||
</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-l">
|
||||
{yacht.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Icon size={16} name="width" />
|
||||
<span>{yacht.length}</span>
|
||||
</div>
|
||||
)}
|
||||
<Image
|
||||
src={getImageUrl(yacht.mainImageUrl)}
|
||||
alt={yacht.name}
|
||||
width={400}
|
||||
height={250}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
{/* Badge Overlay */}
|
||||
{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">
|
||||
<Icon
|
||||
size={16}
|
||||
name="restart"
|
||||
/>
|
||||
<span>
|
||||
По запросу
|
||||
</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-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="w-fit">
|
||||
{yacht.colorPrice ? (
|
||||
<p
|
||||
style={{
|
||||
background:
|
||||
"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]"
|
||||
>
|
||||
{yacht.price}
|
||||
</p>
|
||||
) : (
|
||||
<p className="w-fit text-l">
|
||||
{yacht.price}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Icon size={16} name="anchor" />
|
||||
<span>{yacht.feet}</span>
|
||||
{/* Правая колонка - цена и футы */}
|
||||
<div className="flex flex-col justify-between">
|
||||
<div className="w-fit">
|
||||
{yacht.isBestOffer ? (
|
||||
<p
|
||||
style={{
|
||||
background:
|
||||
"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] whitespace-nowrap"
|
||||
>
|
||||
{formatMinCost(yacht.minCost)} / час
|
||||
</p>
|
||||
) : (
|
||||
<p className="w-fit text-l whitespace-nowrap">
|
||||
{formatMinCost(yacht.minCost)} / час
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Icon size={16} name="anchor" />
|
||||
<span>{formatSpeed(yacht.speed)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Call to Action Button */}
|
||||
<div className="text-center">
|
||||
|
|
|
|||
|
|
@ -5,11 +5,88 @@ import Image from "next/image";
|
|||
import Link from "next/link";
|
||||
import { User, ArrowUpRight, Map, ArrowLeft, Heart } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export default function ConfirmPage() {
|
||||
const [promocode, setPromocode] = useState("");
|
||||
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 (
|
||||
<main className="bg-[#f4f4f4] grow">
|
||||
|
|
@ -36,8 +113,8 @@ export default function ConfirmPage() {
|
|||
Яхта Сеньорита
|
||||
</h2>
|
||||
<div className="flex justify-center gap-10 text-xs text-[#666666]">
|
||||
<span>09 авг.</span>
|
||||
<span>Гостей: 1</span>
|
||||
<span>{departureDateFormatted || "Не выбрано"}</span>
|
||||
<span>Гостей: {guestCount || "Не выбрано"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -69,7 +146,7 @@ export default function ConfirmPage() {
|
|||
</label>
|
||||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
||||
<div className="text-[#333333]">
|
||||
9 Авг 00:00
|
||||
{departureDisplay}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -79,7 +156,7 @@ export default function ConfirmPage() {
|
|||
</label>
|
||||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
||||
<div className="text-[#333333]">
|
||||
9 Авг 02:00
|
||||
{arrivalDisplay}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -99,7 +176,7 @@ export default function ConfirmPage() {
|
|||
</label>
|
||||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF] flex items-center justify-between">
|
||||
<span className="text-[#333333]">
|
||||
1 гость
|
||||
{guestsDisplay}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -316,8 +393,7 @@ export default function ConfirmPage() {
|
|||
Даты
|
||||
</div>
|
||||
<div className="text-base text-[#999999]">
|
||||
9 августа в 00:00 — 9 августа в
|
||||
02:00
|
||||
{datesDisplay}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -328,7 +404,7 @@ export default function ConfirmPage() {
|
|||
Гости
|
||||
</div>
|
||||
<div className="text-base text-[#999999]">
|
||||
1 гость
|
||||
{guestsDisplay}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,14 +18,59 @@ interface DatePickerProps {
|
|||
showIcon?: boolean;
|
||||
variant?: "default" | "small";
|
||||
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) {
|
||||
const [date, setDate] = React.useState<Date>();
|
||||
const [departureTime, setDepartureTime] = React.useState("12:00");
|
||||
const [arrivalTime, setArrivalTime] = React.useState("13:00");
|
||||
export function DatePicker({
|
||||
showIcon = true,
|
||||
variant = "default",
|
||||
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 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 = () => {
|
||||
// Закрываем popover после применения
|
||||
setOpen(false);
|
||||
|
|
@ -71,7 +116,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
|
|||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
onSelect={handleDateChange}
|
||||
className="mb-4 "
|
||||
locale={ru}
|
||||
disabled={(date) =>
|
||||
|
|
@ -112,7 +157,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
|
|||
type="time"
|
||||
value={departureTime}
|
||||
onChange={(e) =>
|
||||
setDepartureTime(e.target.value)
|
||||
handleDepartureTimeChange(e.target.value)
|
||||
}
|
||||
className="w-full focus:outline-none focus:border-transparent"
|
||||
/>
|
||||
|
|
@ -129,7 +174,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
|
|||
type="time"
|
||||
value={arrivalTime}
|
||||
onChange={(e) =>
|
||||
setArrivalTime(e.target.value)
|
||||
handleArrivalTimeChange(e.target.value)
|
||||
}
|
||||
className="w-full focus:outline-none focus:border-transparent"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const useApiClient = () => {
|
|||
const authPopup = useAuthPopup();
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: "/api",
|
||||
baseURL: "http://192.168.1.5:4000/",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,3 +4,28 @@ import { twMerge } from "tailwind-merge"
|
|||
export function cn(...inputs: ClassValue[]) {
|
||||
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