Add all possible bullshit

This commit is contained in:
Иван 2025-12-15 00:59:03 +03:00
parent d177eee970
commit 745d58ab3a
14 changed files with 2241 additions and 2097 deletions

View File

@ -1,4 +1,4 @@
interface CatalogItemDto { interface CatalogItemShortDto {
id?: number; id?: number;
name: string; name: string;
length: number; length: number;
@ -13,11 +13,65 @@ interface CatalogItemDto {
} }
interface MainPageCatalogResponseDto { interface MainPageCatalogResponseDto {
featuredYacht: CatalogItemDto; featuredYacht: CatalogItemShortDto;
restYachts: CatalogItemDto[]; restYachts: CatalogItemShortDto[];
} }
interface CatalogFilteredResponseDto { interface CatalogFilteredResponseDto {
items: CatalogItemDto[]; items: CatalogItemShortDto[];
total: number; total: number;
} }
interface Reservation {
id: number;
yachtId: number;
reservatorId: number;
startUtc: number;
endUtc: number;
}
interface Review {
id: number;
reviewerId: number;
yachtId: number;
starsCount: number;
description: string;
}
interface User {
userId?: number;
firstName?: string;
lastName?: string;
phone?: string;
email?: string;
password?: string;
yachts?: Yacht[];
companyName?: string;
inn?: number;
ogrn?: number;
}
interface CatalogItemLongDto extends CatalogItemShortDto {
year: number;
comfortCapacity: number;
maxCapacity: number;
width: number;
cabinsCount: number;
matherial: string;
power: number;
description: string;
owner: User;
reviews: Review[];
reservations: Reservation[];
}
interface Yacht {
yachtId: number;
name: string;
model: string;
year: number;
length: number;
userId: number;
createdAt: Date;
updatedAt: Date;
}

View File

@ -5,15 +5,17 @@ import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { DatePicker } from "@/components/ui/date-picker"; import { DatePicker } from "@/components/ui/date-picker";
import { GuestPicker } from "@/components/form/guest-picker"; import { GuestPicker } from "@/components/form/guest-picker";
import { format } from "date-fns";
interface BookingWidgetProps { interface BookingWidgetProps {
price: string; price: string;
yacht: CatalogItemLongDto;
} }
export function BookingWidget({ price }: BookingWidgetProps) { export function BookingWidget({ price, yacht }: BookingWidgetProps) {
const router = useRouter(); const router = useRouter();
const [departureDate] = useState<Date | undefined>(); const [departureDate, setDepartureDate] = useState<Date | undefined>();
const [arrivalDate] = useState<Date | undefined>(); const [arrivalDate, setArrivalDate] = useState<Date | undefined>();
const [guests, setGuests] = useState({ adults: 1, children: 0 }); const [guests, setGuests] = useState({ adults: 1, children: 0 });
const [total] = useState(0); const [total] = useState(0);
@ -22,13 +24,18 @@ export function BookingWidget({ price }: BookingWidgetProps) {
}; };
const handleBook = () => { const handleBook = () => {
// Логика бронирования if (!departureDate || !arrivalDate || !yacht || !yacht.id) return;
console.log("Booking:", {
departureDate, const params = new URLSearchParams({
arrivalDate, yachtId: yacht.id.toString(),
guests, departureDate: format(departureDate, "yyyy-MM-dd"),
departureTime: format(departureDate, "HH:mm"),
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
arrivalTime: format(arrivalDate, "HH:mm"),
guests: (guests.adults + guests.children).toString(),
}); });
router.push("/confirm");
router.push(`/confirm?${params.toString()}`);
}; };
return ( return (
@ -36,9 +43,7 @@ export function BookingWidget({ price }: BookingWidgetProps) {
<div className="mb-6"> <div className="mb-6">
<p className="text-2xl font-bold text-[#333333] mb-2"> <p className="text-2xl font-bold text-[#333333] mb-2">
от {price} {" "} от {price} {" "}
<span className="text-base font-normal text-[#999999]"> <span className="text-base font-normal text-[#999999]">/час</span>
/час
</span>
</p> </p>
</div> </div>
@ -51,6 +56,9 @@ export function BookingWidget({ price }: BookingWidgetProps) {
variant="small" variant="small"
placeholder="Выберите дату и время" placeholder="Выберите дату и время"
showIcon={false} showIcon={false}
onDateChange={setDepartureDate}
value={departureDate}
onlyDeparture
/> />
</div> </div>
@ -62,6 +70,9 @@ export function BookingWidget({ price }: BookingWidgetProps) {
variant="small" variant="small"
placeholder="Выберите дату и время" placeholder="Выберите дату и время"
showIcon={false} showIcon={false}
onDateChange={setArrivalDate}
value={arrivalDate}
onlyArrival
/> />
</div> </div>
@ -84,6 +95,7 @@ export function BookingWidget({ price }: BookingWidgetProps) {
onClick={handleBook} onClick={handleBook}
variant="gradient" variant="gradient"
className="w-full h-12 font-bold text-white mb-4" className="w-full h-12 font-bold text-white mb-4"
disabled={!departureDate || !arrivalDate}
> >
Забронировать Забронировать
</Button> </Button>
@ -91,9 +103,7 @@ export function BookingWidget({ price }: BookingWidgetProps) {
<div className="pt-4 border-t border-gray-200"> <div className="pt-4 border-t border-gray-200">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-base text-[#333333]">Итого:</span> <span className="text-base text-[#333333]">Итого:</span>
<span className="text-base font-bold text-[#333333]"> <span className="text-base font-bold text-[#333333]">{total} </span>
{total}
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,19 +2,7 @@
import Image from "next/image"; import Image from "next/image";
interface ContactInfoProps { export function ContactInfo({ firstName, companyName, inn, ogrn }: User) {
contactPerson: {
name: string;
avatar: string;
};
requisites: {
ip: string;
inn: string;
ogrn: string;
};
}
export function ContactInfo({ contactPerson, requisites }: ContactInfoProps) {
return ( return (
<div className="flex flex-col sm:flex-row gap-5"> <div className="flex flex-col sm:flex-row gap-5">
<div className="flex-1 rounded-[24px] px-6 py-5 bg-[#f4f4f4]"> <div className="flex-1 rounded-[24px] px-6 py-5 bg-[#f4f4f4]">
@ -22,46 +10,32 @@ export function ContactInfo({ contactPerson, requisites }: ContactInfoProps) {
<div className="relative rounded-full overflow-hidden bg-gray-200 flex items-center justify-center"> <div className="relative rounded-full overflow-hidden bg-gray-200 flex items-center justify-center">
<Image <Image
src="/images/avatar.png" src="/images/avatar.png"
alt={contactPerson.name} alt={firstName || "avatar"}
width={124} width={124}
height={124} height={124}
/> />
</div> </div>
<div className="flex flex-col justify-between h-full"> <div className="flex flex-col justify-between h-full">
<h3 className="text-base font-bold text-[#333333]"> <h3 className="text-base font-bold text-[#333333]">{firstName}</h3>
{contactPerson.name} <p className="text-base text-[#333333]">Контактное лицо</p>
</h3>
<p className="text-base text-[#333333]">
Контактное лицо
</p>
</div> </div>
</div> </div>
</div> </div>
<div className="flex-1 rounded-[24px] px-6 py-5 bg-[#f4f4f4]"> <div className="flex-1 rounded-[24px] px-6 py-5 bg-[#f4f4f4]">
<h3 className="text-base font-bold text-[#333333] mb-3"> <h3 className="text-base font-bold text-[#333333] mb-3">Реквизиты</h3>
Реквизиты
</h3>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-col sm:flex-row sm:justify-between gap-1"> <div className="flex flex-col sm:flex-row sm:justify-between gap-1">
<span className="text-base text-[#333333]">ИП</span> <span className="text-base text-[#333333]">ИП</span>
<span className="text-base text-[#999999]"> <span className="text-base text-[#999999]">{companyName}</span>
{requisites.ip}
</span>
</div> </div>
<div className="flex flex-col sm:flex-row sm:justify-between gap-1"> <div className="flex flex-col sm:flex-row sm:justify-between gap-1">
<span className="text-base text-[#333333]">ИНН</span> <span className="text-base text-[#333333]">ИНН</span>
<span className="text-base text-[#999999]"> <span className="text-base text-[#999999]">{inn}</span>
{requisites.inn}
</span>
</div> </div>
<div className="flex flex-col sm:flex-row sm:justify-between gap-1"> <div className="flex flex-col sm:flex-row sm:justify-between gap-1">
<span className="text-base text-[#333333]"> <span className="text-base text-[#333333]">ОГРН/ОГРНИП</span>
ОГРН/ОГРНИП <span className="text-base text-[#999999]">{ogrn}</span>
</span>
<span className="text-base text-[#999999]">
{requisites.ogrn}
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useMemo } from "react";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import { import {
isSameMonth, isSameMonth,
@ -14,14 +14,24 @@ import {
import { ru } from "date-fns/locale"; import { ru } from "date-fns/locale";
import { ChevronLeftIcon, ChevronRightIcon, Clock } from "lucide-react"; import { ChevronLeftIcon, ChevronRightIcon, Clock } from "lucide-react";
interface Reservation {
id: number;
reservatorId: number;
yachtId: number;
startUtc: number;
endUtc: number;
}
interface YachtAvailabilityProps { interface YachtAvailabilityProps {
price: string; price: string;
mobile?: boolean; mobile?: boolean;
reservations?: Reservation[];
} }
export function YachtAvailability({ export function YachtAvailability({
price, price,
mobile = false, mobile = false,
reservations = [],
}: YachtAvailabilityProps) { }: YachtAvailabilityProps) {
const today = startOfDay(new Date()); const today = startOfDay(new Date());
const [currentMonth, setCurrentMonth] = useState( const [currentMonth, setCurrentMonth] = useState(
@ -34,6 +44,62 @@ export function YachtAvailability({
return new Date(2025, 3, i + 1); return new Date(2025, 3, i + 1);
}); });
// Format time from Unix timestamp to HH:mm in UTC
const formatTimeFromUnix = (unixTimestamp: number) => {
const date = new Date(unixTimestamp * 1000);
// Format in UTC to avoid timezone conversion
return format(date, "HH:mm");
};
// Get time portion of a UTC timestamp
const getUTCTime = (unixTimestamp: number) => {
const date = new Date(unixTimestamp * 1000);
return date.getUTCHours() * 60 + date.getUTCMinutes(); // minutes since midnight UTC
};
// Get reservations for a specific date with proper time splitting
const getReservationsForDate = (date: Date) => {
const dayStart = Math.floor(startOfDay(date).getTime() / 1000);
const dayEnd = dayStart + 24 * 60 * 60;
const dayReservations: Array<{
id: number;
startTime: string;
endTime: string;
}> = [];
reservations.forEach((reservation) => {
// Check if reservation overlaps with this day
if (reservation.startUtc < dayEnd && reservation.endUtc > dayStart) {
// Calculate the actual time range for this specific day
const dayReservationStart = Math.max(reservation.startUtc, dayStart);
const dayReservationEnd = Math.min(reservation.endUtc, dayEnd);
// Format times in UTC to avoid timezone issues
const startTime = formatTimeFromUnix(dayReservationStart);
const endTime = formatTimeFromUnix(dayReservationEnd);
dayReservations.push({
id: reservation.id,
startTime,
endTime,
});
}
});
return dayReservations;
};
// Check if a date has any reservations
const hasReservationsOnDate = (date: Date) => {
const dayStart = Math.floor(startOfDay(date).getTime() / 1000);
const dayEnd = dayStart + 24 * 60 * 60;
return reservations.some((reservation) => {
return reservation.startUtc < dayEnd && reservation.endUtc > dayStart;
});
};
const isDateUnavailable = (date: Date) => { const isDateUnavailable = (date: Date) => {
return unavailableDates.some( return unavailableDates.some(
(d) => (d) =>
@ -48,7 +114,6 @@ export function YachtAvailability({
}; };
const shouldBeCrossedOut = (date: Date) => { const shouldBeCrossedOut = (date: Date) => {
// Перечеркиваем если день занят или находится до текущего дня
return isDateUnavailable(date) || isDateInPast(date); return isDateUnavailable(date) || isDateInPast(date);
}; };
@ -88,6 +153,36 @@ export function YachtAvailability({
return { value: timeString, label: timeString }; return { value: timeString, label: timeString };
}); });
// Helper function to render time slots for desktop view
const renderTimeSlots = (date: Date) => {
const dateReservations = getReservationsForDate(date);
if (dateReservations.length === 0) {
// No reservations, show free time slot
return (
<div className="flex flex-col gap-1.5 w-full mt-1">
<div className="w-fit bg-[#F6BD4D] text-white text-[10px] font-medium px-1 py-0 rounded-full inline-block">
08:0020:00
</div>
</div>
);
}
// Show all reservations for this day
return (
<div className="flex flex-col gap-1 w-full mt-1">
{dateReservations.map((res) => (
<div
key={`${res.id}-${res.startTime}`}
className="w-fit bg-[#2F5CD0] text-white text-[10px] font-medium px-1 py-0 rounded-full inline-block"
>
{res.startTime}{res.endTime}
</div>
))}
</div>
);
};
if (mobile) { if (mobile) {
return ( return (
<div className="w-full"> <div className="w-full">
@ -126,15 +221,7 @@ export function YachtAvailability({
locale={ru} locale={ru}
formatters={{ formatters={{
formatWeekdayName: (date) => { formatWeekdayName: (date) => {
const weekdays = [ const weekdays = ["ВС", "ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ"];
"ВС",
"ПН",
"ВТ",
"СР",
"ЧТ",
"ПТ",
"СБ",
];
return weekdays[date.getDay()]; return weekdays[date.getDay()];
}, },
}} }}
@ -159,9 +246,8 @@ export function YachtAvailability({
return <div className="hidden" />; return <div className="hidden" />;
} }
const isCrossedOut = shouldBeCrossedOut( const isCrossedOut = shouldBeCrossedOut(day.date);
day.date const hasRes = hasReservationsOnDate(day.date);
);
return ( return (
<button <button
@ -180,6 +266,9 @@ export function YachtAvailability({
disabled={isCrossedOut} disabled={isCrossedOut}
> >
{day.date.getDate()} {day.date.getDate()}
{hasRes && !isCrossedOut && (
<div className="absolute bottom-1 right-1 w-1.5 h-1.5 bg-[#2F5CD0] rounded-full"></div>
)}
</button> </button>
); );
}, },
@ -231,9 +320,7 @@ export function YachtAvailability({
return ( return (
<div className="space-y-4 w-full"> <div className="space-y-4 w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-base font-bold text-[#333333]"> <h2 className="text-base font-bold text-[#333333]">Доступность яхты</h2>
Доступность яхты
</h2>
</div> </div>
<div className="bg-white w-full"> <div className="bg-white w-full">
@ -273,8 +360,7 @@ export function YachtAvailability({
button_next: "hidden", button_next: "hidden",
table: "w-full border-collapse", table: "w-full border-collapse",
weekdays: "hidden", weekdays: "hidden",
weekday: weekday: "flex-1 text-gray-500 text-xs font-normal p-2 text-center",
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
week: "flex w-full", week: "flex w-full",
day: "relative flex-1", day: "relative flex-1",
}} }}
@ -311,17 +397,16 @@ export function YachtAvailability({
{/* Дата и "Доступно:" в одной строке */} {/* Дата и "Доступно:" в одной строке */}
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<span className="text-xs text-gray-400"> <span className="text-xs text-gray-400">
Доступно: {hasReservationsOnDate(day.date)
? "Бронь:"
: "Доступно:"}
</span> </span>
<span className="text-sm font-medium text-[#333333]"> <span className="text-sm font-medium text-[#333333]">
{day.date.getDate()} {day.date.getDate()}
</span> </span>
</div> </div>
<div className="flex flex-col gap-1.5 w-full mt-1"> {/* Time slots - reservations first */}
<div className="w-fit bg-[#F6BD4D] text-white text-[10px] font-medium px-1 py-0 rounded-full inline-block"> {renderTimeSlots(day.date)}
08:0020:00
</div>
</div>
{/* Цена в нижнем правом углу */} {/* Цена в нижнем правом углу */}
<span className="absolute bottom-[2px] right-[4px] text-xs text-[#333333] font-medium"> <span className="absolute bottom-[2px] right-[4px] text-xs text-[#333333] font-medium">
{price} / час {price} / час

View File

@ -1,16 +1,7 @@
"use client"; "use client";
interface YachtCharacteristicsProps { interface YachtCharacteristicsProps {
yacht: { yacht: CatalogItemLongDto;
year: number;
maxCapacity: number;
comfortableCapacity: number;
length: number;
width: number;
cabins: number;
material: string;
power: number;
};
} }
export function YachtCharacteristics({ yacht }: YachtCharacteristicsProps) { export function YachtCharacteristics({ yacht }: YachtCharacteristicsProps) {
@ -22,12 +13,12 @@ export function YachtCharacteristics({ yacht }: YachtCharacteristicsProps) {
}, },
{ {
label: "Комфортная вместимость", label: "Комфортная вместимость",
value: `${yacht.comfortableCapacity} человек`, value: `${yacht.comfortCapacity} человек`,
}, },
{ label: "Длина", value: `${yacht.length} м` }, { label: "Длина", value: `${yacht.length} м` },
{ label: "Ширина", value: `${yacht.width} м` }, { label: "Ширина", value: `${yacht.width} м` },
{ label: "Каюты", value: yacht.cabins }, { label: "Каюты", value: yacht.cabinsCount },
{ label: "Материал", value: yacht.material }, { label: "Материал", value: yacht.matherial },
{ label: "Мощность", value: `${yacht.power} л/с` }, { label: "Мощность", value: `${yacht.power} л/с` },
]; ];
@ -42,9 +33,7 @@ export function YachtCharacteristics({ yacht }: YachtCharacteristicsProps) {
key={index} key={index}
className="flex justify-between items-center py-4 border-b border-gray-200" className="flex justify-between items-center py-4 border-b border-gray-200"
> >
<span className="text-base text-[#999999]"> <span className="text-base text-[#999999]">{char.label}</span>
{char.label}
</span>
<span className="text-base font-regular text-[#333333]"> <span className="text-base font-regular text-[#333333]">
{char.value} {char.value}
</span> </span>

View File

@ -11,6 +11,7 @@ import {
CarouselPrevious, CarouselPrevious,
type CarouselApi, type CarouselApi,
} from "@/components/ui/carousel"; } from "@/components/ui/carousel";
import { getImageUrl } from "@/lib/utils";
interface YachtGalleryProps { interface YachtGalleryProps {
images: string[]; images: string[];
@ -54,7 +55,7 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) {
<CarouselItem key={index}> <CarouselItem key={index}>
<div className="relative w-full h-[60vh] lg:h-[592px] rounded-0 lg:rounded-[24px] overflow-hidden"> <div className="relative w-full h-[60vh] lg:h-[592px] rounded-0 lg:rounded-[24px] overflow-hidden">
<Image <Image
src={img} src={getImageUrl(img)}
alt={`Yacht image ${index + 1}`} alt={`Yacht image ${index + 1}`}
fill fill
className="object-cover" className="object-cover"
@ -97,7 +98,7 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) {
}`} }`}
> >
<Image <Image
src={img} src={getImageUrl(img)}
alt={`Thumbnail ${index + 1}`} alt={`Thumbnail ${index + 1}`}
fill fill
className="object-cover" className="object-cover"

View File

@ -1,37 +0,0 @@
export const YACHT = {
id: 1,
name: "Яхта Название",
location: "7 Футов",
price: "18 000",
images: [
"/images/yachts/yacht1.jpg",
"/images/yachts/yacht2.jpg",
"/images/yachts/yacht3.jpg",
"/images/yachts/yacht4.jpg",
"/images/yachts/yacht5.jpg",
"/images/yachts/yacht6.jpg",
],
badge: "По запросу",
year: 2000,
maxCapacity: 11,
comfortableCapacity: 11,
length: 13,
width: 4,
cabins: 2,
material: "Стеклопластик",
power: 740,
description: `Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта
Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта
Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта
Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта
Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта Яхта`,
contactPerson: {
name: "Денис",
avatar: "/images/logo.svg",
},
requisites: {
ip: "Иванов Иван Иванович",
inn: "23000000000",
ogrn: "310000000000001",
},
};

View File

@ -1,8 +1,8 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useRouter, useParams } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState, useEffect } from "react";
import { ArrowLeft, Heart } from "lucide-react"; import { ArrowLeft, Heart } from "lucide-react";
import Icon from "@/components/ui/icon"; import Icon from "@/components/ui/icon";
import { YachtGallery } from "./components/YachtGallery"; import { YachtGallery } from "./components/YachtGallery";
@ -10,9 +10,23 @@ import { YachtAvailability } from "./components/YachtAvailability";
import { BookingWidget } from "./components/BookingWidget"; import { BookingWidget } from "./components/BookingWidget";
import { YachtCharacteristics } from "./components/YachtCharacteristics"; import { YachtCharacteristics } from "./components/YachtCharacteristics";
import { ContactInfo } from "./components/ContactInfo"; import { ContactInfo } from "./components/ContactInfo";
import { YACHT } from "./const"; import useApiClient from "@/hooks/useApiClient";
import { formatSpeed } from "@/lib/utils";
export default function YachtDetailPage() { export default function YachtDetailPage() {
const { id } = useParams();
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
const client = useApiClient();
useEffect(() => {
(async () => {
const response = await client.get<CatalogItemLongDto>(`/catalog/${id}/`);
setYacht(response.data);
})();
}, [id]);
// const params = useParams(); // const params = useParams();
const router = useRouter(); const router = useRouter();
const [activeTab, setActiveTab] = useState< const [activeTab, setActiveTab] = useState<
@ -24,6 +38,10 @@ export default function YachtDetailPage() {
| "reviews" | "reviews"
>("availability"); >("availability");
if (!yacht) {
return <div />;
}
return ( return (
<main className="bg-[#f4f4f4] min-h-screen "> <main className="bg-[#f4f4f4] min-h-screen ">
{/* Мобильная фиксированная верхняя панель навигации */} {/* Мобильная фиксированная верхняя панель навигации */}
@ -35,9 +53,7 @@ export default function YachtDetailPage() {
> >
<ArrowLeft size={24} className="text-[#333333]" /> <ArrowLeft size={24} className="text-[#333333]" />
</button> </button>
<h2 className="text-base font-medium text-[#333333]"> <h2 className="text-base font-medium text-[#333333]">Яхта</h2>
Яхта
</h2>
<button className="flex items-center justify-center"> <button className="flex items-center justify-center">
<Heart size={24} className="text-[#333333]" /> <Heart size={24} className="text-[#333333]" />
</button> </button>
@ -59,7 +75,7 @@ export default function YachtDetailPage() {
</span> </span>
</Link> </Link>
<span>&gt;</span> <span>&gt;</span>
<span className="text-[#333333]">{YACHT.name}</span> <span className="text-[#333333]">{yacht.name}</span>
</div> </div>
</div> </div>
@ -70,14 +86,14 @@ export default function YachtDetailPage() {
<div className="lg:hidden pt-[50px]"> <div className="lg:hidden pt-[50px]">
{/* Gallery */} {/* Gallery */}
<YachtGallery <YachtGallery
images={YACHT.images} images={yacht.galleryUrls || []}
badge={YACHT.badge} badge={!yacht.hasQuickRent ? "По запросу" : ""}
/> />
{/* Yacht Title */} {/* Yacht Title */}
<div className="px-4 pt-4"> <div className="px-4 pt-4">
<h1 className="text-xl font-bold text-[#333333] mb-4"> <h1 className="text-xl font-bold text-[#333333] mb-4">
{YACHT.name} {yacht.name}
</h1> </h1>
</div> </div>
@ -105,9 +121,7 @@ export default function YachtDetailPage() {
Описание Описание
</button> </button>
<button <button
onClick={() => onClick={() => setActiveTab("characteristics")}
setActiveTab("characteristics")
}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${ className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "characteristics" activeTab === "characteristics"
? "text-[#008299] border-b-2 border-[#008299]" ? "text-[#008299] border-b-2 border-[#008299]"
@ -143,26 +157,22 @@ export default function YachtDetailPage() {
<div className="px-4 py-6"> <div className="px-4 py-6">
{activeTab === "availability" && ( {activeTab === "availability" && (
<YachtAvailability <YachtAvailability
price={YACHT.price} price={String(yacht.minCost)}
mobile={true} mobile={true}
reservations={yacht.reservations}
/> />
)} )}
{activeTab === "description" && ( {activeTab === "description" && (
<div> <div>
<p className="text-base text-[#666666] leading-relaxed"> <p className="text-base text-[#666666] leading-relaxed">
{YACHT.description} {yacht.description}
</p> </p>
</div> </div>
)} )}
{activeTab === "characteristics" && ( {activeTab === "characteristics" && (
<YachtCharacteristics yacht={YACHT} /> <YachtCharacteristics yacht={yacht} />
)}
{activeTab === "contact" && (
<ContactInfo
contactPerson={YACHT.contactPerson}
requisites={YACHT.requisites}
/>
)} )}
{activeTab === "contact" && <ContactInfo {...yacht.owner} />}
{activeTab === "reviews" && ( {activeTab === "reviews" && (
<div> <div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
@ -186,20 +196,16 @@ export default function YachtDetailPage() {
{/* Yacht Title and Actions */} {/* Yacht Title and Actions */}
<div className="mb-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> <div className="mb-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h1 className="text-xl md:text-2xl font-bold text-[#333333]"> <h1 className="text-xl md:text-2xl font-bold text-[#333333]">
{YACHT.name} {yacht.name}
</h1> </h1>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="flex items-center gap-2 text-[#333333]"> <div className="flex items-center gap-2 text-[#333333]">
<Icon name="pin" size={32} /> <Icon name="pin" size={32} />
<span className="text-base"> <span className="text-base">{formatSpeed(yacht.speed)}</span>
{YACHT.location}
</span>
</div> </div>
<button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors"> <button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors">
<Icon name="share" size={32} /> <Icon name="share" size={32} />
<span className="text-base"> <span className="text-base">Поделиться</span>
Поделиться
</span>
</button> </button>
<button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors"> <button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors">
<Icon name="heart" size={32} /> <Icon name="heart" size={32} />
@ -212,8 +218,8 @@ export default function YachtDetailPage() {
<div className="space-y-6"> <div className="space-y-6">
{/* Gallery */} {/* Gallery */}
<YachtGallery <YachtGallery
images={YACHT.images} images={yacht.galleryUrls || []}
badge={YACHT.badge} badge={!yacht.hasQuickRent ? "По запросу" : ""}
/> />
{/* Content with Booking Widget on the right */} {/* Content with Booking Widget on the right */}
@ -221,10 +227,13 @@ export default function YachtDetailPage() {
{/* Left column - all content below gallery */} {/* Left column - all content below gallery */}
<div className="flex-1 space-y-6"> <div className="flex-1 space-y-6">
{/* Availability */} {/* Availability */}
<YachtAvailability price={YACHT.price} /> <YachtAvailability
price={String(yacht.minCost)}
reservations={yacht.reservations}
/>
{/* Characteristics */} {/* Characteristics */}
<YachtCharacteristics yacht={YACHT} /> <YachtCharacteristics yacht={yacht} />
{/* Description */} {/* Description */}
<div> <div>
@ -232,15 +241,12 @@ export default function YachtDetailPage() {
Описание Описание
</h2> </h2>
<p className="text-base text-[#666666] leading-relaxed"> <p className="text-base text-[#666666] leading-relaxed">
{YACHT.description} {yacht.description}
</p> </p>
</div> </div>
{/* Contact and Requisites */} {/* Contact and Requisites */}
<ContactInfo <ContactInfo {...yacht.owner} />
contactPerson={YACHT.contactPerson}
requisites={YACHT.requisites}
/>
{/* Reviews */} {/* Reviews */}
<div> <div>
@ -260,7 +266,7 @@ export default function YachtDetailPage() {
{/* Right column - Booking Widget (sticky) */} {/* Right column - Booking Widget (sticky) */}
<div className="lg:w-74 flex-shrink-0 lg:sticky lg:top-24 self-start"> <div className="lg:w-74 flex-shrink-0 lg:sticky lg:top-24 self-start">
<BookingWidget price={YACHT.price} /> <BookingWidget price={String(yacht.minCost)} yacht={yacht} />
</div> </div>
</div> </div>
</div> </div>
@ -273,14 +279,12 @@ export default function YachtDetailPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<span className="text-lg font-bold text-[#333333]"> <span className="text-lg font-bold text-[#333333]">
{YACHT.price} {yacht.minCost}
</span>
<span className="text-sm text-[#999999] ml-1">
/ час
</span> </span>
<span className="text-sm text-[#999999] ml-1">/ час</span>
</div> </div>
<button <button
onClick={() => router.push("/confirm")} onClick={() => router.push(`/confirm?yachtId=${yacht.id}`)}
className="bg-[#008299] text-white px-6 py-3 rounded-lg font-bold text-base hover:bg-[#006d7a] transition-colors" className="bg-[#008299] text-white px-6 py-3 rounded-lg font-bold text-base hover:bg-[#006d7a] transition-colors"
> >
Забронировать Забронировать

View File

@ -15,7 +15,11 @@ 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"; import { formatMinCost, formatWidth, getImageUrl } from "@/lib/utils";
export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) { export default function FeaturedYacht({
yacht,
}: {
yacht: CatalogItemShortDto;
}) {
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl); const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
const handleThumbnailClick = (imageSrc: string) => { const handleThumbnailClick = (imageSrc: string) => {
@ -33,8 +37,7 @@ export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
<div <div
className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative lg:hidden" className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative lg:hidden"
style={{ style={{
backgroundImage: backgroundImage: "url(/images/badge-bg.jpg)",
"url(/images/badge-bg.jpg)",
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
@ -47,14 +50,10 @@ export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
{/* Header with yacht name and length */} {/* Header with yacht name and length */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold"> <h2 className="text-3xl font-bold">{yacht.name}</h2>
{yacht.name}
</h2>
<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">{formatWidth(yacht.length)}</span>
{formatWidth(yacht.length)}
</span>
</div> </div>
</div> </div>
@ -89,20 +88,15 @@ export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
<div className="relative"> <div className="relative">
<Image <Image
src={getImageUrl(thumb)} src={getImageUrl(thumb)}
alt={`${yacht.name alt={`${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 ${selectedImage === className={`w-20 h-16 object-cover rounded-[8px] cursor-pointer border-2 transition-all ${
thumb selectedImage === thumb
? "border-[#008299]" ? "border-[#008299]"
: "border-gray-200 hover:border-gray-400" : "border-gray-200 hover:border-gray-400"
}`} }`}
onClick={() => onClick={() => handleThumbnailClick(thumb)}
handleThumbnailClick(
thumb
)
}
unoptimized unoptimized
/> />
</div> </div>
@ -139,8 +133,7 @@ export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
<div <div
className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative hidden lg:flex" className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative hidden lg:flex"
style={{ style={{
backgroundImage: backgroundImage: "url(/images/badge-bg.jpg)",
"url(/images/badge-bg.jpg)",
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
@ -177,9 +170,7 @@ export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
{/* Total price */} {/* Total price */}
<div className="flex justify-between items-center text-l mt-6 font-bold text-gray-800"> <div className="flex justify-between items-center text-l mt-6 font-bold text-gray-800">
<span className="font-normal"> <span className="font-normal">Итого:</span>
Итого:
</span>
<span>0 </span> <span>0 </span>
</div> </div>
</div> </div>

View File

@ -8,21 +8,31 @@ import Link from "next/link";
import FeaturedYacht from "./FeaturedYacht"; import FeaturedYacht from "./FeaturedYacht";
import useApiClient from "@/hooks/useApiClient"; import useApiClient from "@/hooks/useApiClient";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { formatMinCost, formatSpeed, formatWidth, getImageUrl } from "@/lib/utils"; import {
formatMinCost,
formatSpeed,
formatWidth,
getImageUrl,
} from "@/lib/utils";
export default function YachtGrid() { export default function YachtGrid() {
const client = useApiClient(); const client = useApiClient();
const [featuredYacht, setFeaturedYacht] = useState<CatalogItemDto | null>(null); const [featuredYacht, setFeaturedYacht] =
const [yachtCatalog, setYachtCatalog] = useState<CatalogItemDto[] | null>(null); useState<CatalogItemShortDto | null>(null);
const [yachtCatalog, setYachtCatalog] = useState<
CatalogItemShortDto[] | null
>(null);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const response = await client.get<MainPageCatalogResponseDto>("/catalog/main-page/"); const response = await client.get<MainPageCatalogResponseDto>(
"/catalog/main-page/"
);
setFeaturedYacht(response.data.featuredYacht); setFeaturedYacht(response.data.featuredYacht);
setYachtCatalog(response.data.restYachts); setYachtCatalog(response.data.restYachts);
})(); })();
}, []) }, []);
return ( return (
<section className="text-white"> <section className="text-white">
@ -39,15 +49,13 @@ export default function YachtGrid() {
Каталог лучших яхт Балаклавы разных ценовых сегментах. Каталог лучших яхт Балаклавы разных ценовых сегментах.
</p> </p>
<p className="text-gray-700 leading-relaxed"> <p className="text-gray-700 leading-relaxed">
Проверенные лодки с лицензией на перевозки, опытные Проверенные лодки с лицензией на перевозки, опытные капитаны.
капитаны. Выбирайте удобную дату, время и бронируйте. Выбирайте удобную дату, время и бронируйте.
</p> </p>
</div> </div>
{/* Featured Yacht Block */} {/* Featured Yacht Block */}
{featuredYacht && ( {featuredYacht && <FeaturedYacht yacht={featuredYacht} />}
<FeaturedYacht yacht={featuredYacht} />
)}
{/* Yacht Grid */} {/* Yacht Grid */}
{yachtCatalog && ( {yachtCatalog && (
@ -55,7 +63,7 @@ export default function YachtGrid() {
{yachtCatalog.map((yacht) => ( {yachtCatalog.map((yacht) => (
<Link <Link
key={yacht.id} key={yacht.id}
href={`/catalog/${yacht.id ?? 0}}`} href={`/catalog/${yacht.id ?? 0}`}
className="block" className="block"
> >
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg"> <Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg">
@ -71,9 +79,7 @@ export default function YachtGrid() {
"url('/images/best-yacht-bg.jpg')", "url('/images/best-yacht-bg.jpg')",
}} }}
> >
<span> <span>{yacht.topText}</span>
{yacht.topText}
</span>
</div> </div>
</div> </div>
)} )}
@ -89,13 +95,8 @@ export default function YachtGrid() {
<> <>
<div className="absolute top-3 left-3"> <div className="absolute top-3 left-3">
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm flex items-center gap-1"> <div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm flex items-center gap-1">
<Icon <Icon size={16} name="restart" />
size={16} <span>По запросу</span>
name="restart"
/>
<span>
По запросу
</span>
</div> </div>
</div> </div>
</> </>
@ -106,9 +107,7 @@ export default function YachtGrid() {
<div className="flex justify-between gap-4"> <div className="flex justify-between gap-4">
{/* Левая колонка - название и длина */} {/* Левая колонка - название и длина */}
<div className="space-y-2"> <div className="space-y-2">
<h3 className="font-bold text-l"> <h3 className="font-bold text-l">{yacht.name}</h3>
{yacht.name}
</h3>
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
<Icon size={16} name="width" /> <Icon size={16} name="width" />
<span>{formatWidth(yacht.length)}</span> <span>{formatWidth(yacht.length)}</span>

View File

@ -4,30 +4,99 @@ import { Button } from "@/components/ui/button";
import Image from "next/image"; 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 { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import useApiClient from "@/hooks/useApiClient";
import { getImageUrl } from "@/lib/utils";
import { differenceInHours, parseISO } from "date-fns";
export default function ConfirmPage() { export default function ConfirmPage() {
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
const [totalHours, setTotalHours] = useState<number>(0);
const [totalPrice, setTotalPrice] = useState<number>(0);
const client = useApiClient();
const [promocode, setPromocode] = useState(""); const [promocode, setPromocode] = useState("");
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
// Извлекаем параметры из URL // Извлекаем параметры из URL
const yachtId = searchParams.get("yachtId"); const yachtId = searchParams.get("yachtId");
const guestCount = searchParams.get("guestCount"); const guestCount = searchParams.get("guests");
const departureDate = searchParams.get("departureDate"); const departureDate = searchParams.get("departureDate");
const departureTime = searchParams.get("departureTime"); const departureTime = searchParams.get("departureTime");
const arrivalDate = searchParams.get("arrivalDate"); const arrivalDate = searchParams.get("arrivalDate");
const arrivalTime = searchParams.get("arrivalTime"); const arrivalTime = searchParams.get("arrivalTime");
useEffect(() => {
(async () => {
const response = await client.get<CatalogItemLongDto>(
`/catalog/${yachtId}/`
);
setYacht(response.data);
})();
}, [yachtId]);
// Расчет стоимости при изменении дат
useEffect(() => {
if (
departureDate &&
departureTime &&
arrivalDate &&
arrivalTime &&
yacht?.minCost
) {
try {
// Создаем полные даты
const departureDateTime = parseISO(`${departureDate}T${departureTime}`);
const arrivalDateTime = parseISO(`${arrivalDate}T${arrivalTime}`);
// Рассчитываем разницу в часах (с округлением до 0.5 часа)
let hoursDiff = differenceInHours(arrivalDateTime, departureDateTime);
// Добавляем разницу в минутах
const minutesDiff =
(arrivalDateTime.getMinutes() - departureDateTime.getMinutes()) / 60;
hoursDiff += minutesDiff;
// Округляем до ближайших 0.5 часа
const roundedHours = Math.ceil(hoursDiff * 2) / 2;
// Рассчитываем стоимость
const pricePerHour = yacht.minCost;
const total = pricePerHour * roundedHours;
setTotalHours(roundedHours);
setTotalPrice(total);
} catch (error) {
console.error("Error calculating price:", error);
setTotalHours(0);
setTotalPrice(0);
}
} else {
setTotalHours(0);
setTotalPrice(0);
}
}, [departureDate, departureTime, arrivalDate, arrivalTime, yacht]);
// Функция для форматирования даты (краткий формат) // Функция для форматирования даты (краткий формат)
const formatDate = (dateString: string | null) => { const formatDate = (dateString: string | null) => {
if (!dateString) return null; if (!dateString) return null;
try { try {
const date = new Date(dateString); const date = parseISO(dateString);
const months = [ const months = [
"янв", "фев", "мар", "апр", "май", "июн", "янв",
"июл", "авг", "сен", "окт", "ноя", "дек" "фев",
"мар",
"апр",
"май",
"июн",
"июл",
"авг",
"сен",
"окт",
"ноя",
"дек",
]; ];
const day = date.getDate(); const day = date.getDate();
const month = months[date.getMonth()]; const month = months[date.getMonth()];
@ -41,10 +110,20 @@ export default function ConfirmPage() {
const formatDateFull = (dateString: string | null) => { const formatDateFull = (dateString: string | null) => {
if (!dateString) return null; if (!dateString) return null;
try { try {
const date = new Date(dateString); const date = parseISO(dateString);
const months = [ const months = [
"января", "февраля", "марта", "апреля", "мая", "июня", "января",
"июля", "августа", "сентября", "октября", "ноября", "декабря" "февраля",
"марта",
"апреля",
"мая",
"июня",
"июля",
"августа",
"сентября",
"октября",
"ноября",
"декабря",
]; ];
const day = date.getDate(); const day = date.getDate();
const month = months[date.getMonth()]; const month = months[date.getMonth()];
@ -57,7 +136,6 @@ export default function ConfirmPage() {
// Функция для форматирования времени // Функция для форматирования времени
const formatTime = (timeString: string | null) => { const formatTime = (timeString: string | null) => {
if (!timeString) return null; if (!timeString) return null;
// Предполагаем формат HH:mm или HH:mm:ss
return timeString.split(":").slice(0, 2).join(":"); return timeString.split(":").slice(0, 2).join(":");
}; };
@ -72,22 +150,39 @@ export default function ConfirmPage() {
const arrivalDateFormattedFull = formatDateFull(arrivalDate); const arrivalDateFormattedFull = formatDateFull(arrivalDate);
// Формируем строки для отображения // Формируем строки для отображения
const departureDisplay = departureDateFormatted && departureTimeFormatted const departureDisplay =
departureDateFormatted && departureTimeFormatted
? `${departureDateFormatted} ${departureTimeFormatted}` ? `${departureDateFormatted} ${departureTimeFormatted}`
: "Не выбрано"; : "Не выбрано";
const arrivalDisplay = arrivalDateFormatted && arrivalTimeFormatted const arrivalDisplay =
arrivalDateFormatted && arrivalTimeFormatted
? `${arrivalDateFormatted} ${arrivalTimeFormatted}` ? `${arrivalDateFormatted} ${arrivalTimeFormatted}`
: "Не выбрано"; : "Не выбрано";
const datesDisplay = departureDateFormattedFull && departureTimeFormatted && arrivalDateFormattedFull && arrivalTimeFormatted const datesDisplay =
departureDateFormattedFull &&
departureTimeFormatted &&
arrivalDateFormattedFull &&
arrivalTimeFormatted
? `${departureDateFormattedFull} в ${departureTimeFormatted}${arrivalDateFormattedFull} в ${arrivalTimeFormatted}` ? `${departureDateFormattedFull} в ${departureTimeFormatted}${arrivalDateFormattedFull} в ${arrivalTimeFormatted}`
: "Не выбрано"; : "Не выбрано";
const guestsDisplay = guestCount const guestsDisplay = guestCount
? guestCount === "1" ? "1 гость" : `${guestCount} гостей` ? guestCount === "1"
? "1 гость"
: `${guestCount} гостей`
: "Не выбрано"; : "Не выбрано";
// Форматирование цены с разделителями тысяч
const formatPrice = (price: number) => {
return new Intl.NumberFormat("ru-RU").format(price);
};
if (!yacht) {
return <div />;
}
return ( return (
<main className="bg-[#f4f4f4] grow"> <main className="bg-[#f4f4f4] grow">
{/* Мобильная версия */} {/* Мобильная версия */}
@ -101,16 +196,13 @@ export default function ConfirmPage() {
onClick={() => router.back()} onClick={() => router.back()}
className="flex-shrink-0 w-10 h-10 rounded-full border border-[#DFDFDF] flex items-center justify-center hover:bg-[#f4f4f4] transition-colors" className="flex-shrink-0 w-10 h-10 rounded-full border border-[#DFDFDF] flex items-center justify-center hover:bg-[#f4f4f4] transition-colors"
> >
<ArrowLeft <ArrowLeft size={20} className="text-[#333333]" />
size={20}
className="text-[#333333]"
/>
</button> </button>
{/* Центральный блок с информацией */} {/* Центральный блок с информацией */}
<div className="flex-1 min-w-0 text-center"> <div className="flex-1 min-w-0 text-center">
<h2 className="text-base font-bold text-[#333333] mb-1"> <h2 className="text-base font-bold text-[#333333] mb-1">
Яхта Сеньорита Яхта {yacht.name}
</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>{departureDateFormatted || "Не выбрано"}</span> <span>{departureDateFormatted || "Не выбрано"}</span>
@ -120,10 +212,7 @@ export default function ConfirmPage() {
{/* Кнопка избранного */} {/* Кнопка избранного */}
<button className="flex-shrink-0 w-10 h-10 flex items-center justify-center hover:opacity-70 transition-opacity"> <button className="flex-shrink-0 w-10 h-10 flex items-center justify-center hover:opacity-70 transition-opacity">
<Heart <Heart size={20} className="text-[#333333] stroke-2" />
size={20}
className="text-[#333333] stroke-2"
/>
</button> </button>
</div> </div>
</div> </div>
@ -133,9 +222,7 @@ export default function ConfirmPage() {
<div className="bg-white p-4"> <div className="bg-white p-4">
{/* Заголовок с иконкой */} {/* Заголовок с иконкой */}
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">
<h1 className="text-xl text-[#333333]"> <h1 className="text-xl text-[#333333]">Ваше бронирование 🛥</h1>
Ваше бронирование 🛥
</h1>
</div> </div>
{/* Поля Выход и Заход */} {/* Поля Выход и Заход */}
@ -145,9 +232,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]">{departureDisplay}</div>
{departureDisplay}
</div>
</div> </div>
</div> </div>
<div className="relative"> <div className="relative">
@ -155,9 +240,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]">{arrivalDisplay}</div>
{arrivalDisplay}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -175,9 +258,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]">{guestsDisplay}</span>
{guestsDisplay}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -188,8 +269,7 @@ export default function ConfirmPage() {
Правила отмены Правила отмены
</h3> </h3>
<p className="text-base text-[#333333]"> <p className="text-base text-[#333333]">
При отмене до 10 мая вы получите частичный При отмене до 10 мая вы получите частичный возврат.{" "}
возврат.{" "}
<Link <Link
href="#" href="#"
className="text-sm text-[#2D908D] hover:text-[#007088] font-bold transition-colors" className="text-sm text-[#2D908D] hover:text-[#007088] font-bold transition-colors"
@ -205,28 +285,32 @@ export default function ConfirmPage() {
Детализация цены Детализация цены
</h3> </h3>
<div> <div>
{totalHours > 0 && yacht.minCost ? (
<>
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<span className="text-[#333333]"> <span className="text-[#333333]">
26 400 x 2ч {formatPrice(yacht.minCost)} × {totalHours}ч
</span> </span>
<span className="text-[#333333]"> <span className="text-[#333333]">
52 800 {formatPrice(totalPrice)}
</span> </span>
</div> </div>
<div className="flex justify-between items-center mb-4 pb-4 border-b border-[#DFDFDF]"> <div className="flex justify-between items-center mb-4 pb-4 border-b border-[#DFDFDF]">
<span className="text-[#333333]"> <span className="text-[#333333]">Услуги</span>
Услуги
</span>
<span className="text-[#333333]">0 Р</span> <span className="text-[#333333]">0 Р</span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-[#333333]"> <span className="text-[#333333]">Итого:</span>
Итого:
</span>
<span className="font-bold text-[#333333]"> <span className="font-bold text-[#333333]">
52 800 Р {formatPrice(totalPrice)} Р
</span> </span>
</div> </div>
</>
) : (
<div className="text-[#999999] text-center py-4">
Укажите даты для расчета стоимости
</div>
)}
</div> </div>
</div> </div>
@ -237,9 +321,7 @@ export default function ConfirmPage() {
type="text" type="text"
placeholder="Промокод" placeholder="Промокод"
value={promocode} value={promocode}
onChange={(e) => onChange={(e) => setPromocode(e.target.value)}
setPromocode(e.target.value)
}
className="flex-1 min-w-0 px-4 py-3 h-[64px] border border-[#DFDFDF] rounded-full text-sm text-[#757575] focus:outline-none focus:ring-2 focus:ring-[#008299] focus:border-transparent" className="flex-1 min-w-0 px-4 py-3 h-[64px] border border-[#DFDFDF] rounded-full text-sm text-[#757575] focus:outline-none focus:ring-2 focus:ring-[#008299] focus:border-transparent"
/> />
<Button <Button
@ -255,6 +337,7 @@ export default function ConfirmPage() {
<Button <Button
variant="default" variant="default"
className="w-full h-[56px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full transition-colors duration-200" className="w-full h-[56px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full transition-colors duration-200"
disabled={totalHours === 0}
> >
Отправить заявку Отправить заявку
</Button> </Button>
@ -273,9 +356,7 @@ export default function ConfirmPage() {
</span> </span>
</Link> </Link>
<span>&gt;</span> <span>&gt;</span>
<span className="text-[#333333]"> <span className="text-[#333333]">Ваше бронирование</span>
Ваше бронирование
</span>
</div> </div>
<div className="flex flex-col lg:flex-row gap-6"> <div className="flex flex-col lg:flex-row gap-6">
@ -286,7 +367,7 @@ export default function ConfirmPage() {
{/* Изображение яхты */} {/* Изображение яхты */}
<div className="relative mb-5"> <div className="relative mb-5">
<Image <Image
src="/images/yachts/yacht1.jpg" src={getImageUrl(yacht.mainImageUrl)}
alt="Яхта" alt="Яхта"
width={400} width={400}
height={250} height={250}
@ -295,16 +376,11 @@ export default function ConfirmPage() {
{/* Плашка владельца */} {/* Плашка владельца */}
<div className="absolute top-2 left-2"> <div className="absolute top-2 left-2">
<div className="bg-white backdrop-blur-sm px-4 py-2 rounded-[8px] flex items-center gap-2"> <div className="bg-white backdrop-blur-sm px-4 py-2 rounded-[8px] flex items-center gap-2">
<User <User size={22} className="text-[#999999]" />
size={22}
className="text-[#999999]"
/>
<div className="flex flex-col gap-[4px]"> <div className="flex flex-col gap-[4px]">
<span className="text-[#999999]"> <span className="text-[#999999]">Владелец</span>
Владелец
</span>
<span className="text-[#333333] font-bold"> <span className="text-[#333333] font-bold">
Денис {yacht.owner.firstName}
</span> </span>
</div> </div>
</div> </div>
@ -312,7 +388,7 @@ export default function ConfirmPage() {
</div> </div>
{/* Название яхты */} {/* Название яхты */}
<h3 className="text-base text-[#333333] pb-3 border-b border-[#DFDFDF] mb-4"> <h3 className="text-base text-[#333333] pb-3 border-b border-[#DFDFDF] mb-4">
Яхта Яхта {yacht.name}
</h3> </h3>
{/* Детализация цены */} {/* Детализация цены */}
@ -321,30 +397,32 @@ export default function ConfirmPage() {
Детализация цены Детализация цены
</h4> </h4>
<div> <div>
{totalHours > 0 && yacht.minCost ? (
<>
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<span className="text-[#333333]"> <span className="text-[#333333]">
26 400 x 2ч {formatPrice(yacht.minCost)} × {totalHours}ч
</span> </span>
<span className="text-[#333333]"> <span className="text-[#333333]">
52 800 {formatPrice(totalPrice)}
</span> </span>
</div> </div>
<div className="flex justify-between items-center border-b border-[#DFDFDF] pb-4 mb-4"> <div className="flex justify-between items-center border-b border-[#DFDFDF] pb-4 mb-4">
<span className="text-[#333333]"> <span className="text-[#333333]">Услуги</span>
Услуги <span className="text-[#333333]">0 Р</span>
</span>
<span className="text-[#333333]">
0 Р
</span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-[#333333]"> <span className="text-[#333333]">Итого:</span>
Итого:
</span>
<span className="text-[#333333] font-bold"> <span className="text-[#333333] font-bold">
52 800 Р {formatPrice(totalPrice)} Р
</span> </span>
</div> </div>
</>
) : (
<div className="text-[#999999] text-center py-4">
Укажите даты для расчета стоимости
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -358,9 +436,7 @@ export default function ConfirmPage() {
type="text" type="text"
placeholder="Промокод" placeholder="Промокод"
value={promocode} value={promocode}
onChange={(e) => onChange={(e) => setPromocode(e.target.value)}
setPromocode(e.target.value)
}
className="flex-1 min-w-0 px-4 sm:px-8 py-5 h-[64px] border border-[#DFDFDF] rounded-full text-base text-[#757575] focus:outline-none focus:ring-2 focus:ring-[#008299] focus:border-transparent" className="flex-1 min-w-0 px-4 sm:px-8 py-5 h-[64px] border border-[#DFDFDF] rounded-full text-base text-[#757575] focus:outline-none focus:ring-2 focus:ring-[#008299] focus:border-transparent"
/> />
<Button <Button
@ -385,13 +461,11 @@ export default function ConfirmPage() {
Ваше бронирование Ваше бронирование
</h2> </h2>
{/* Сведения о бронировании */} {/* Сведения о бронирования */}
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-4 mb-4">
{/* Даты */} {/* Даты */}
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4"> <div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
<div className="text-[#333333] mb-1"> <div className="text-[#333333] mb-1">Даты</div>
Даты
</div>
<div className="text-base text-[#999999]"> <div className="text-base text-[#999999]">
{datesDisplay} {datesDisplay}
</div> </div>
@ -400,9 +474,7 @@ export default function ConfirmPage() {
{/* Гости */} {/* Гости */}
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4"> <div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
<div> <div>
<div className="text-[#333333] mb-1"> <div className="text-[#333333] mb-1">Гости</div>
Гости
</div>
<div className="text-base text-[#999999]"> <div className="text-base text-[#999999]">
{guestsDisplay} {guestsDisplay}
</div> </div>
@ -423,8 +495,7 @@ export default function ConfirmPage() {
</h3> </h3>
<p className="text-[#333333]"> <p className="text-[#333333]">
При отмене до 10 мая вы получите частичный При отмене до 10 мая вы получите частичный возврат.
возврат.
</p> </p>
<Link <Link
href="#" href="#"
@ -445,6 +516,7 @@ export default function ConfirmPage() {
variant="default" variant="default"
size="lg" size="lg"
className="flex-shrink-0 h-[64px] w-[270px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full p-0 transition-colors duration-200 hover:shadow-lg" className="flex-shrink-0 h-[64px] w-[270px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full p-0 transition-colors duration-200 hover:shadow-lg"
disabled={totalHours === 0}
> >
Отправить заявку Отправить заявку
</Button> </Button>

View File

@ -62,9 +62,7 @@ const CommonPopoverContent: React.FC<CommonPopoverContentProps> = ({
onSelect={setDate} onSelect={setDate}
className="mb-[24px]" className="mb-[24px]"
locale={ru} locale={ru}
disabled={(date) => disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
date < new Date(new Date().setHours(0, 0, 0, 0))
}
classNames={{ classNames={{
root: "w-full", root: "w-full",
month: "flex w-full flex-col gap-4", month: "flex w-full flex-col gap-4",
@ -76,8 +74,7 @@ const CommonPopoverContent: React.FC<CommonPopoverContentProps> = ({
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold", "flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
table: "w-full border-collapse", table: "w-full border-collapse",
weekdays: "flex", weekdays: "flex",
weekday: weekday: "flex-1 text-gray-500 text-xs font-normal p-2 text-center",
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
day_button: "font-bold ring-0 focus:ring-0", day_button: "font-bold ring-0 focus:ring-0",
week: "mt-2 flex w-full", week: "mt-2 flex w-full",
today: "bg-gray-100 text-gray-900 rounded-full", today: "bg-gray-100 text-gray-900 rounded-full",
@ -88,7 +85,6 @@ const CommonPopoverContent: React.FC<CommonPopoverContentProps> = ({
}} }}
/> />
{/* Счетчики гостей */} {/* Счетчики гостей */}
<div className="mb-[24px] flex gap-3"> <div className="mb-[24px] flex gap-3">
<Counter <Counter
@ -217,10 +213,7 @@ export const GuestDatePicker: React.FC<GuestDatePickerProps> = ({
<div className={className}> <div className={className}>
<div className="space-y-5"> <div className="space-y-5">
{/* Кнопка Выход */} {/* Кнопка Выход */}
<Popover <Popover open={isDepartureOpen} onOpenChange={setIsDepartureOpen}>
open={isDepartureOpen}
onOpenChange={setIsDepartureOpen}
>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"

View File

@ -24,6 +24,8 @@ interface DatePickerProps {
onDateChange?: (date: Date | undefined) => void; onDateChange?: (date: Date | undefined) => void;
onDepartureTimeChange?: (time: string) => void; onDepartureTimeChange?: (time: string) => void;
onArrivalTimeChange?: (time: string) => void; onArrivalTimeChange?: (time: string) => void;
onlyDeparture?: boolean;
onlyArrival?: boolean;
} }
export function DatePicker({ export function DatePicker({
@ -36,19 +38,31 @@ export function DatePicker({
onDateChange, onDateChange,
onDepartureTimeChange, onDepartureTimeChange,
onArrivalTimeChange, onArrivalTimeChange,
onlyDeparture,
onlyArrival,
}: DatePickerProps) { }: DatePickerProps) {
const [internalDate, setInternalDate] = React.useState<Date>(); const [internalDate, setInternalDate] = React.useState<Date>();
const [internalDepartureTime, setInternalDepartureTime] = React.useState("12:00"); const [internalDepartureTime, setInternalDepartureTime] =
React.useState("12:00");
const [internalArrivalTime, setInternalArrivalTime] = React.useState("13:00"); const [internalArrivalTime, setInternalArrivalTime] = React.useState("13:00");
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
// Определяем, является ли компонент контролируемым // Определяем, является ли компонент контролируемым
const isControlled = value !== undefined || externalDepartureTime !== undefined || externalArrivalTime !== undefined; const isControlled =
value !== undefined ||
externalDepartureTime !== undefined ||
externalArrivalTime !== undefined;
// Используем внешние значения, если они предоставлены, иначе внутренние // Используем внешние значения, если они предоставлены, иначе внутренние
const date = value !== undefined ? (value || undefined) : internalDate; const date = value !== undefined ? value || undefined : internalDate;
const departureTime = externalDepartureTime !== undefined ? externalDepartureTime : internalDepartureTime; const departureTime =
const arrivalTime = externalArrivalTime !== undefined ? externalArrivalTime : internalArrivalTime; externalDepartureTime !== undefined
? externalDepartureTime
: internalDepartureTime;
const arrivalTime =
externalArrivalTime !== undefined
? externalArrivalTime
: internalArrivalTime;
const handleDateChange = (newDate: Date | undefined) => { const handleDateChange = (newDate: Date | undefined) => {
if (onDateChange) { if (onDateChange) {
@ -91,17 +105,12 @@ export function DatePicker({
> >
<div className="flex items-center"> <div className="flex items-center">
{showIcon && ( {showIcon && (
<Icon <Icon name="calendar" className="w-4 h-4 text-brand mr-2" />
name="calendar"
className="w-4 h-4 text-brand mr-2"
/>
)} )}
{date ? ( {date ? (
format( format(date, `d MMMM, ${departureTime} - ${arrivalTime}`, {
date, locale: ru,
`d MMMM, ${departureTime} - ${arrivalTime}`, })
{ locale: ru }
)
) : ( ) : (
<span>{placeholder}</span> <span>{placeholder}</span>
)} )}
@ -150,6 +159,7 @@ export function DatePicker({
{/* Поля времени */} {/* Поля времени */}
<div className="flex gap-3 mb-4"> <div className="flex gap-3 mb-4">
{!onlyDeparture ? (
<div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center"> <div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center">
<label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1"> <label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1">
Выход Выход
@ -159,14 +169,14 @@ export function DatePicker({
<input <input
type="time" type="time"
value={departureTime} value={departureTime}
onChange={(e) => onChange={(e) => handleDepartureTimeChange(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"
/> />
</div> </div>
</div> </div>
) : null}
{!onlyArrival ? (
<div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center"> <div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center">
<label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1"> <label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1">
Заход Заход
@ -176,13 +186,12 @@ export function DatePicker({
<input <input
type="time" type="time"
value={arrivalTime} value={arrivalTime}
onChange={(e) => onChange={(e) => handleArrivalTimeChange(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"
/> />
</div> </div>
</div> </div>
) : null}
</div> </div>
{/* Кнопка Применить */} {/* Кнопка Применить */}

View File

@ -7,7 +7,7 @@ const useApiClient = () => {
const authPopup = useAuthPopup(); const authPopup = useAuthPopup();
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: "http://192.168.1.5:4000/", baseURL: "/api",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },