Интеграция с бэком на главной странице

This commit is contained in:
Sergey Bolshakov 2025-12-14 20:41:54 +03:00
parent 85c23f136e
commit 7181718b0d
9 changed files with 477 additions and 201 deletions

View File

@ -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$/,

18
src/api/types.ts Normal file
View File

@ -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[];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 + " ₽";
};