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,23 +1,77 @@
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 CatalogItemShortDto {
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[];
featuredYacht: CatalogItemShortDto;
restYachts: CatalogItemShortDto[];
}
interface CatalogFilteredResponseDto {
items: CatalogItemDto[];
total: number;
items: CatalogItemShortDto[];
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,97 +5,107 @@ import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { DatePicker } from "@/components/ui/date-picker";
import { GuestPicker } from "@/components/form/guest-picker";
import { format } from "date-fns";
interface BookingWidgetProps {
price: string;
price: string;
yacht: CatalogItemLongDto;
}
export function BookingWidget({ price }: BookingWidgetProps) {
const router = useRouter();
const [departureDate] = useState<Date | undefined>();
const [arrivalDate] = useState<Date | undefined>();
const [guests, setGuests] = useState({ adults: 1, children: 0 });
const [total] = useState(0);
export function BookingWidget({ price, yacht }: BookingWidgetProps) {
const router = useRouter();
const [departureDate, setDepartureDate] = useState<Date | undefined>();
const [arrivalDate, setArrivalDate] = useState<Date | undefined>();
const [guests, setGuests] = useState({ adults: 1, children: 0 });
const [total] = useState(0);
const handleGuestsChange = (adults: number, children: number) => {
setGuests({ adults, children });
};
const handleGuestsChange = (adults: number, children: number) => {
setGuests({ adults, children });
};
const handleBook = () => {
// Логика бронирования
console.log("Booking:", {
departureDate,
arrivalDate,
guests,
});
router.push("/confirm");
};
const handleBook = () => {
if (!departureDate || !arrivalDate || !yacht || !yacht.id) return;
return (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="mb-6">
<p className="text-2xl font-bold text-[#333333] mb-2">
от {price} {" "}
<span className="text-base font-normal text-[#999999]">
/час
</span>
</p>
</div>
const params = new URLSearchParams({
yachtId: yacht.id.toString(),
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(),
});
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium text-[#333333] mb-2">
Выход
</label>
<DatePicker
variant="small"
placeholder="Выберите дату и время"
showIcon={false}
/>
</div>
router.push(`/confirm?${params.toString()}`);
};
<div>
<label className="block text-sm font-medium text-[#333333] mb-2">
Заход
</label>
<DatePicker
variant="small"
placeholder="Выберите дату и время"
showIcon={false}
/>
</div>
return (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="mb-6">
<p className="text-2xl font-bold text-[#333333] mb-2">
от {price} {" "}
<span className="text-base font-normal text-[#999999]">/час</span>
</p>
</div>
<div>
<label className="block text-sm font-medium text-[#333333] mb-2">
Гостей
</label>
<GuestPicker
adults={guests.adults}
childrenCount={guests.children}
onChange={handleGuestsChange}
variant="small"
showIcon={false}
placeholder="1 гость"
/>
</div>
</div>
<Button
onClick={handleBook}
variant="gradient"
className="w-full h-12 font-bold text-white mb-4"
>
Забронировать
</Button>
<div className="pt-4 border-t border-gray-200">
<div className="flex justify-between items-center">
<span className="text-base text-[#333333]">Итого:</span>
<span className="text-base font-bold text-[#333333]">
{total}
</span>
</div>
</div>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium text-[#333333] mb-2">
Выход
</label>
<DatePicker
variant="small"
placeholder="Выберите дату и время"
showIcon={false}
onDateChange={setDepartureDate}
value={departureDate}
onlyDeparture
/>
</div>
);
<div>
<label className="block text-sm font-medium text-[#333333] mb-2">
Заход
</label>
<DatePicker
variant="small"
placeholder="Выберите дату и время"
showIcon={false}
onDateChange={setArrivalDate}
value={arrivalDate}
onlyArrival
/>
</div>
<div>
<label className="block text-sm font-medium text-[#333333] mb-2">
Гостей
</label>
<GuestPicker
adults={guests.adults}
childrenCount={guests.children}
onChange={handleGuestsChange}
variant="small"
showIcon={false}
placeholder="1 гость"
/>
</div>
</div>
<Button
onClick={handleBook}
variant="gradient"
className="w-full h-12 font-bold text-white mb-4"
disabled={!departureDate || !arrivalDate}
>
Забронировать
</Button>
<div className="pt-4 border-t border-gray-200">
<div className="flex justify-between items-center">
<span className="text-base text-[#333333]">Итого:</span>
<span className="text-base font-bold text-[#333333]">{total} </span>
</div>
</div>
</div>
);
}

View File

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

View File

@ -1,339 +1,424 @@
"use client";
import { useState } from "react";
import { useState, useMemo } from "react";
import { Calendar } from "@/components/ui/calendar";
import {
isSameMonth,
isBefore,
startOfDay,
format,
eachDayOfInterval,
startOfMonth,
endOfMonth,
isSameMonth,
isBefore,
startOfDay,
format,
eachDayOfInterval,
startOfMonth,
endOfMonth,
} from "date-fns";
import { ru } from "date-fns/locale";
import { ChevronLeftIcon, ChevronRightIcon, Clock } from "lucide-react";
interface Reservation {
id: number;
reservatorId: number;
yachtId: number;
startUtc: number;
endUtc: number;
}
interface YachtAvailabilityProps {
price: string;
mobile?: boolean;
price: string;
mobile?: boolean;
reservations?: Reservation[];
}
export function YachtAvailability({
price,
mobile = false,
price,
mobile = false,
reservations = [],
}: YachtAvailabilityProps) {
const today = startOfDay(new Date());
const [currentMonth, setCurrentMonth] = useState(
new Date(today.getFullYear(), today.getMonth(), 1)
);
const [startTime, setStartTime] = useState<string>("");
const [endTime, setEndTime] = useState<string>("");
const today = startOfDay(new Date());
const [currentMonth, setCurrentMonth] = useState(
new Date(today.getFullYear(), today.getMonth(), 1)
);
const [startTime, setStartTime] = useState<string>("");
const [endTime, setEndTime] = useState<string>("");
const unavailableDates = Array.from({ length: 26 }, (_, i) => {
return new Date(2025, 3, i + 1);
});
const unavailableDates = Array.from({ length: 26 }, (_, i) => {
return new Date(2025, 3, i + 1);
});
const isDateUnavailable = (date: Date) => {
return unavailableDates.some(
(d) =>
d.getDate() === date.getDate() &&
d.getMonth() === date.getMonth() &&
d.getFullYear() === date.getFullYear()
);
};
// 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");
};
const isDateInPast = (date: Date) => {
return isBefore(startOfDay(date), today);
};
// 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
};
const shouldBeCrossedOut = (date: Date) => {
// Перечеркиваем если день занят или находится до текущего дня
return isDateUnavailable(date) || isDateInPast(date);
};
// 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 isDateAvailable = (date: Date) => {
return !shouldBeCrossedOut(date) && isSameMonth(date, currentMonth);
};
const dayReservations: Array<{
id: number;
startTime: string;
endTime: string;
}> = [];
const getAvailableDaysCount = () => {
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(currentMonth);
const daysInMonth = eachDayOfInterval({
start: monthStart,
end: monthEnd,
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 daysInMonth.filter((day) => isDateAvailable(day)).length;
};
const goToPreviousMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)
);
};
const goToNextMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)
);
};
// Генерация времени для селекта
const timeOptions = Array.from({ length: 24 * 2 }, (_, i) => {
const hours = Math.floor(i / 2);
const minutes = (i % 2) * 30;
const timeString = `${String(hours).padStart(2, "0")}:${String(
minutes
).padStart(2, "0")}`;
return { value: timeString, label: timeString };
}
});
if (mobile) {
return (
<div className="w-full">
{/* Навигация по месяцам */}
<div className="flex items-center justify-between mb-4">
<button
onClick={goToPreviousMonth}
className="w-10 h-10 rounded-full border border-[#dfdfdf] flex items-center justify-center hover:bg-gray-50 transition-colors"
>
<ChevronLeftIcon className="size-4 text-[#333333]" />
</button>
<div className="flex flex-col items-center">
<span className="text-lg font-medium text-[#333333] capitalize">
{format(currentMonth, "LLLL", { locale: ru })}
</span>
<span className="text-sm text-[#999999]">
Свободных дней: {getAvailableDaysCount()}
</span>
</div>
<button
onClick={goToNextMonth}
className="w-10 h-10 rounded-full border border-[#dfdfdf] flex items-center justify-center hover:bg-gray-50 transition-colors"
>
<ChevronRightIcon className="size-4 text-[#333333]" />
</button>
</div>
return dayReservations;
};
{/* Календарь */}
<div style={{ flexShrink: 0 }}>
<Calendar
mode="single"
month={currentMonth}
onMonthChange={setCurrentMonth}
showOutsideDays={false}
className="w-full p-0"
locale={ru}
formatters={{
formatWeekdayName: (date) => {
const weekdays = [
"ВС",
"ПН",
"ВТ",
"СР",
"ЧТ",
"ПТ",
"СБ",
];
return weekdays[date.getDay()];
},
}}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-2",
nav: "hidden",
month_caption: "hidden",
caption_label: "hidden",
button_previous: "hidden",
button_next: "hidden",
table: "w-full border-collapse table-fixed",
weekdays: "flex w-full mb-2",
weekday:
"flex-1 text-[#999999] text-xs font-normal p-2 text-center",
week: "flex w-full min-h-[50px]",
day: "relative flex-1 min-w-0 flex-shrink-0",
}}
components={{
DayButton: ({ day, ...props }) => {
if (!isSameMonth(day.date, currentMonth)) {
return <div className="hidden" />;
}
// 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;
const isCrossedOut = shouldBeCrossedOut(
day.date
);
return reservations.some((reservation) => {
return reservation.startUtc < dayEnd && reservation.endUtc > dayStart;
});
};
return (
<button
{...props}
className={`relative w-full flex items-center justify-center text-sm font-medium transition-colors ${
isCrossedOut
? "text-[#CCCCCC] line-through"
: "text-[#333333] hover:bg-gray-100"
}`}
style={
{
aspectRatio: "1 / 1",
minHeight: "44px",
} as React.CSSProperties
}
disabled={isCrossedOut}
>
{day.date.getDate()}
</button>
);
},
}}
/>
</div>
const isDateUnavailable = (date: Date) => {
return unavailableDates.some(
(d) =>
d.getDate() === date.getDate() &&
d.getMonth() === date.getMonth() &&
d.getFullYear() === date.getFullYear()
);
};
{/* Выбор времени */}
<div className="space-y-4 mb-4" style={{ marginTop: "24px" }}>
<div className="flex gap-3">
<div className="flex-1">
<select
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
>
<option value="">--:--</option>
{timeOptions.map((time) => (
<option key={time.value} value={time.value}>
{time.label}
</option>
))}
</select>
</div>
<div className="flex-1">
<select
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
>
<option value="">--:--</option>
{timeOptions.map((time) => (
<option key={time.value} value={time.value}>
{time.label}
</option>
))}
</select>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-[#999999]">
<Clock size={16} />
<span>По местному времени яхты</span>
</div>
</div>
</div>
);
const isDateInPast = (date: Date) => {
return isBefore(startOfDay(date), today);
};
const shouldBeCrossedOut = (date: Date) => {
return isDateUnavailable(date) || isDateInPast(date);
};
const isDateAvailable = (date: Date) => {
return !shouldBeCrossedOut(date) && isSameMonth(date, currentMonth);
};
const getAvailableDaysCount = () => {
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(currentMonth);
const daysInMonth = eachDayOfInterval({
start: monthStart,
end: monthEnd,
});
return daysInMonth.filter((day) => isDateAvailable(day)).length;
};
const goToPreviousMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)
);
};
const goToNextMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)
);
};
// Генерация времени для селекта
const timeOptions = Array.from({ length: 24 * 2 }, (_, i) => {
const hours = Math.floor(i / 2);
const minutes = (i % 2) * 30;
const timeString = `${String(hours).padStart(2, "0")}:${String(
minutes
).padStart(2, "0")}`;
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="space-y-4 w-full">
<div className="flex items-center justify-between">
<h2 className="text-base font-bold text-[#333333]">
Доступность яхты
</h2>
</div>
<div className="bg-white w-full">
<div className="w-full flex justify-end mb-8">
<div className="flex items-center gap-5">
<button
onClick={goToPreviousMonth}
className="cursor-pointer rounded-full border border-[#dfdfdf] h-12 w-12 flex items-center justify-center"
>
<ChevronLeftIcon className="size-4" />
</button>
<span className="text-2xl text-[#333333]">
{format(currentMonth, "LLLL yyyy", { locale: ru })}
</span>
<button
onClick={goToNextMonth}
className="cursor-pointer rounded-full border border-[#dfdfdf] h-12 w-12 flex items-center justify-center"
>
<ChevronRightIcon className="size-4" />
</button>
</div>
</div>
<Calendar
mode="single"
month={currentMonth}
onMonthChange={setCurrentMonth}
showOutsideDays={false}
className="w-full p-0"
locale={ru}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-4",
nav: "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-2",
month_caption: "hidden",
caption_label: "text-2xl",
button_previous: "hidden",
button_next: "hidden",
table: "w-full border-collapse",
weekdays: "hidden",
weekday:
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
week: "flex w-full",
day: "relative flex-1",
}}
components={{
DayButton: ({ day, ...props }) => {
// Показываем только дни текущего месяца
if (!isSameMonth(day.date, currentMonth)) {
return <div className="hidden" />;
}
const isCrossedOut = shouldBeCrossedOut(day.date);
return (
<button
{...props}
className="relative w-full h-20 flex flex-col items-start justify-start px-2 py-[2px] border border-gray-200"
disabled={isCrossedOut}
>
{isCrossedOut ? (
// Перечеркнутая ячейка для недоступных дней
<>
<span className="text-sm font-medium text-[#333333] self-end">
{day.date.getDate()}
</span>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-gray-300 text-4xl font-light leading-none">
</span>
</div>
</>
) : (
// Доступный день с информацией
<>
{/* Дата и "Доступно:" в одной строке */}
<div className="flex items-center justify-between w-full">
<span className="text-xs text-gray-400">
Доступно:
</span>
<span className="text-sm font-medium text-[#333333]">
{day.date.getDate()}
</span>
</div>
<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>
{/* Цена в нижнем правом углу */}
<span className="absolute bottom-[2px] right-[4px] text-xs text-[#333333] font-medium">
{price} / час
</span>
</>
)}
</button>
);
},
}}
/>
</div>
</div>
<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) {
return (
<div className="w-full">
{/* Навигация по месяцам */}
<div className="flex items-center justify-between mb-4">
<button
onClick={goToPreviousMonth}
className="w-10 h-10 rounded-full border border-[#dfdfdf] flex items-center justify-center hover:bg-gray-50 transition-colors"
>
<ChevronLeftIcon className="size-4 text-[#333333]" />
</button>
<div className="flex flex-col items-center">
<span className="text-lg font-medium text-[#333333] capitalize">
{format(currentMonth, "LLLL", { locale: ru })}
</span>
<span className="text-sm text-[#999999]">
Свободных дней: {getAvailableDaysCount()}
</span>
</div>
<button
onClick={goToNextMonth}
className="w-10 h-10 rounded-full border border-[#dfdfdf] flex items-center justify-center hover:bg-gray-50 transition-colors"
>
<ChevronRightIcon className="size-4 text-[#333333]" />
</button>
</div>
{/* Календарь */}
<div style={{ flexShrink: 0 }}>
<Calendar
mode="single"
month={currentMonth}
onMonthChange={setCurrentMonth}
showOutsideDays={false}
className="w-full p-0"
locale={ru}
formatters={{
formatWeekdayName: (date) => {
const weekdays = ["ВС", "ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ"];
return weekdays[date.getDay()];
},
}}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-2",
nav: "hidden",
month_caption: "hidden",
caption_label: "hidden",
button_previous: "hidden",
button_next: "hidden",
table: "w-full border-collapse table-fixed",
weekdays: "flex w-full mb-2",
weekday:
"flex-1 text-[#999999] text-xs font-normal p-2 text-center",
week: "flex w-full min-h-[50px]",
day: "relative flex-1 min-w-0 flex-shrink-0",
}}
components={{
DayButton: ({ day, ...props }) => {
if (!isSameMonth(day.date, currentMonth)) {
return <div className="hidden" />;
}
const isCrossedOut = shouldBeCrossedOut(day.date);
const hasRes = hasReservationsOnDate(day.date);
return (
<button
{...props}
className={`relative w-full flex items-center justify-center text-sm font-medium transition-colors ${
isCrossedOut
? "text-[#CCCCCC] line-through"
: "text-[#333333] hover:bg-gray-100"
}`}
style={
{
aspectRatio: "1 / 1",
minHeight: "44px",
} as React.CSSProperties
}
disabled={isCrossedOut}
>
{day.date.getDate()}
{hasRes && !isCrossedOut && (
<div className="absolute bottom-1 right-1 w-1.5 h-1.5 bg-[#2F5CD0] rounded-full"></div>
)}
</button>
);
},
}}
/>
</div>
{/* Выбор времени */}
<div className="space-y-4 mb-4" style={{ marginTop: "24px" }}>
<div className="flex gap-3">
<div className="flex-1">
<select
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
>
<option value="">--:--</option>
{timeOptions.map((time) => (
<option key={time.value} value={time.value}>
{time.label}
</option>
))}
</select>
</div>
<div className="flex-1">
<select
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
>
<option value="">--:--</option>
{timeOptions.map((time) => (
<option key={time.value} value={time.value}>
{time.label}
</option>
))}
</select>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-[#999999]">
<Clock size={16} />
<span>По местному времени яхты</span>
</div>
</div>
</div>
);
}
return (
<div className="space-y-4 w-full">
<div className="flex items-center justify-between">
<h2 className="text-base font-bold text-[#333333]">Доступность яхты</h2>
</div>
<div className="bg-white w-full">
<div className="w-full flex justify-end mb-8">
<div className="flex items-center gap-5">
<button
onClick={goToPreviousMonth}
className="cursor-pointer rounded-full border border-[#dfdfdf] h-12 w-12 flex items-center justify-center"
>
<ChevronLeftIcon className="size-4" />
</button>
<span className="text-2xl text-[#333333]">
{format(currentMonth, "LLLL yyyy", { locale: ru })}
</span>
<button
onClick={goToNextMonth}
className="cursor-pointer rounded-full border border-[#dfdfdf] h-12 w-12 flex items-center justify-center"
>
<ChevronRightIcon className="size-4" />
</button>
</div>
</div>
<Calendar
mode="single"
month={currentMonth}
onMonthChange={setCurrentMonth}
showOutsideDays={false}
className="w-full p-0"
locale={ru}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-4",
nav: "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-2",
month_caption: "hidden",
caption_label: "text-2xl",
button_previous: "hidden",
button_next: "hidden",
table: "w-full border-collapse",
weekdays: "hidden",
weekday: "flex-1 text-gray-500 text-xs font-normal p-2 text-center",
week: "flex w-full",
day: "relative flex-1",
}}
components={{
DayButton: ({ day, ...props }) => {
// Показываем только дни текущего месяца
if (!isSameMonth(day.date, currentMonth)) {
return <div className="hidden" />;
}
const isCrossedOut = shouldBeCrossedOut(day.date);
return (
<button
{...props}
className="relative w-full h-20 flex flex-col items-start justify-start px-2 py-[2px] border border-gray-200"
disabled={isCrossedOut}
>
{isCrossedOut ? (
// Перечеркнутая ячейка для недоступных дней
<>
<span className="text-sm font-medium text-[#333333] self-end">
{day.date.getDate()}
</span>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-gray-300 text-4xl font-light leading-none">
</span>
</div>
</>
) : (
// Доступный день с информацией
<>
{/* Дата и "Доступно:" в одной строке */}
<div className="flex items-center justify-between w-full">
<span className="text-xs text-gray-400">
{hasReservationsOnDate(day.date)
? "Бронь:"
: "Доступно:"}
</span>
<span className="text-sm font-medium text-[#333333]">
{day.date.getDate()}
</span>
</div>
{/* Time slots - reservations first */}
{renderTimeSlots(day.date)}
{/* Цена в нижнем правом углу */}
<span className="absolute bottom-[2px] right-[4px] text-xs text-[#333333] font-medium">
{price} / час
</span>
</>
)}
</button>
);
},
}}
/>
</div>
</div>
);
}

View File

@ -1,56 +1,45 @@
"use client";
interface YachtCharacteristicsProps {
yacht: {
year: number;
maxCapacity: number;
comfortableCapacity: number;
length: number;
width: number;
cabins: number;
material: string;
power: number;
};
yacht: CatalogItemLongDto;
}
export function YachtCharacteristics({ yacht }: YachtCharacteristicsProps) {
const characteristics = [
{ label: "Год", value: yacht.year },
{
label: "Максимальная вместимость",
value: `${yacht.maxCapacity} человек`,
},
{
label: "Комфортная вместимость",
value: `${yacht.comfortableCapacity} человек`,
},
{ label: "Длина", value: `${yacht.length} м` },
{ label: "Ширина", value: `${yacht.width} м` },
{ label: "Каюты", value: yacht.cabins },
{ label: "Материал", value: yacht.material },
{ label: "Мощность", value: `${yacht.power} л/с` },
];
const characteristics = [
{ label: "Год", value: yacht.year },
{
label: "Максимальная вместимость",
value: `${yacht.maxCapacity} человек`,
},
{
label: "Комфортная вместимость",
value: `${yacht.comfortCapacity} человек`,
},
{ label: "Длина", value: `${yacht.length} м` },
{ label: "Ширина", value: `${yacht.width} м` },
{ label: "Каюты", value: yacht.cabinsCount },
{ label: "Материал", value: yacht.matherial },
{ label: "Мощность", value: `${yacht.power} л/с` },
];
return (
<div>
<h2 className="text-base font-bold text-[#333333] mb-4">
Характеристики
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6">
{characteristics.map((char, index) => (
<div
key={index}
className="flex justify-between items-center py-4 border-b border-gray-200"
>
<span className="text-base text-[#999999]">
{char.label}
</span>
<span className="text-base font-regular text-[#333333]">
{char.value}
</span>
</div>
))}
</div>
</div>
);
return (
<div>
<h2 className="text-base font-bold text-[#333333] mb-4">
Характеристики
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6">
{characteristics.map((char, index) => (
<div
key={index}
className="flex justify-between items-center py-4 border-b border-gray-200"
>
<span className="text-base text-[#999999]">{char.label}</span>
<span className="text-base font-regular text-[#333333]">
{char.value}
</span>
</div>
))}
</div>
</div>
);
}

View File

@ -4,107 +4,108 @@ import { useState, useEffect } from "react";
import Image from "next/image";
import Icon from "@/components/ui/icon";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi,
} from "@/components/ui/carousel";
import { getImageUrl } from "@/lib/utils";
interface YachtGalleryProps {
images: string[];
badge?: string;
images: string[];
badge?: string;
}
export function YachtGallery({ images, badge }: YachtGalleryProps) {
const [api, setApi] = useState<CarouselApi>();
const [current, setCurrent] = useState(0);
const [api, setApi] = useState<CarouselApi>();
const [current, setCurrent] = useState(0);
useEffect(() => {
if (!api) {
return;
}
useEffect(() => {
if (!api) {
return;
}
setCurrent(api.selectedScrollSnap());
setCurrent(api.selectedScrollSnap());
api.on("select", () => {
setCurrent(api.selectedScrollSnap());
});
}, [api]);
api.on("select", () => {
setCurrent(api.selectedScrollSnap());
});
}, [api]);
const scrollTo = (index: number) => {
api?.scrollTo(index);
};
const scrollTo = (index: number) => {
api?.scrollTo(index);
};
return (
<div className="space-y-4">
{/* Main Image Carousel */}
<div className="relative">
<Carousel
setApi={setApi}
opts={{
align: "start",
loop: false,
}}
className="w-full"
>
<CarouselContent>
{images.map((img, index) => (
<CarouselItem key={index}>
<div className="relative w-full h-[60vh] lg:h-[592px] rounded-0 lg:rounded-[24px] overflow-hidden">
<Image
src={img}
alt={`Yacht image ${index + 1}`}
fill
className="object-cover"
priority={index === 0}
/>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="left-2 lg:left-4 bg-white/80 hover:bg-white border-gray-300 w-10 h-10 lg:w-12 lg:h-12 rounded-full" />
<CarouselNext className="right-2 lg:right-4 bg-white/80 hover:bg-white border-gray-300 w-10 h-10 lg:w-12 lg:h-12 rounded-full" />
</Carousel>
{/* Badge - поверх слайдера, не скроллится */}
{badge && (
<div className="absolute bottom-4 left-4 z-20 pointer-events-none">
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm gap-1">
<Icon size={16} name="restart" />
<span>{badge}</span>
</div>
</div>
)}
{/* Photo counter - поверх слайдера, не скроллится */}
<div className="absolute bottom-4 right-4 z-20 pointer-events-none">
<div className="bg-black/40 text-white px-3 py-1 rounded-lg text-sm">
{current + 1}/{images.length}
</div>
return (
<div className="space-y-4">
{/* Main Image Carousel */}
<div className="relative">
<Carousel
setApi={setApi}
opts={{
align: "start",
loop: false,
}}
className="w-full"
>
<CarouselContent>
{images.map((img, index) => (
<CarouselItem key={index}>
<div className="relative w-full h-[60vh] lg:h-[592px] rounded-0 lg:rounded-[24px] overflow-hidden">
<Image
src={getImageUrl(img)}
alt={`Yacht image ${index + 1}`}
fill
className="object-cover"
priority={index === 0}
/>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="left-2 lg:left-4 bg-white/80 hover:bg-white border-gray-300 w-10 h-10 lg:w-12 lg:h-12 rounded-full" />
<CarouselNext className="right-2 lg:right-4 bg-white/80 hover:bg-white border-gray-300 w-10 h-10 lg:w-12 lg:h-12 rounded-full" />
</Carousel>
{/* Badge - поверх слайдера, не скроллится */}
{badge && (
<div className="absolute bottom-4 left-4 z-20 pointer-events-none">
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm gap-1">
<Icon size={16} name="restart" />
<span>{badge}</span>
</div>
{/* Thumbnails - скрыты на мобильных */}
<div className="hidden lg:flex gap-2 overflow-x-auto pb-2">
{images.map((img, index) => (
<button
key={index}
onClick={() => scrollTo(index)}
className={`relative flex-shrink-0 w-17 h-14 rounded-lg overflow-hidden border-2 transition-all ${
current === index
? "border-[#008299] opacity-100"
: "border-transparent opacity-60 hover:opacity-100"
}`}
>
<Image
src={img}
alt={`Thumbnail ${index + 1}`}
fill
className="object-cover"
/>
</button>
))}
</div>
</div>
)}
{/* Photo counter - поверх слайдера, не скроллится */}
<div className="absolute bottom-4 right-4 z-20 pointer-events-none">
<div className="bg-black/40 text-white px-3 py-1 rounded-lg text-sm">
{current + 1}/{images.length}
</div>
</div>
);
</div>
{/* Thumbnails - скрыты на мобильных */}
<div className="hidden lg:flex gap-2 overflow-x-auto pb-2">
{images.map((img, index) => (
<button
key={index}
onClick={() => scrollTo(index)}
className={`relative flex-shrink-0 w-17 h-14 rounded-lg overflow-hidden border-2 transition-all ${
current === index
? "border-[#008299] opacity-100"
: "border-transparent opacity-60 hover:opacity-100"
}`}
>
<Image
src={getImageUrl(img)}
alt={`Thumbnail ${index + 1}`}
fill
className="object-cover"
/>
</button>
))}
</div>
</div>
);
}

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";
import { useRouter } from "next/navigation";
import { useRouter, useParams } from "next/navigation";
import Link from "next/link";
import { useState } from "react";
import { useState, useEffect } from "react";
import { ArrowLeft, Heart } from "lucide-react";
import Icon from "@/components/ui/icon";
import { YachtGallery } from "./components/YachtGallery";
@ -10,283 +10,287 @@ import { YachtAvailability } from "./components/YachtAvailability";
import { BookingWidget } from "./components/BookingWidget";
import { YachtCharacteristics } from "./components/YachtCharacteristics";
import { ContactInfo } from "./components/ContactInfo";
import { YACHT } from "./const";
import useApiClient from "@/hooks/useApiClient";
import { formatSpeed } from "@/lib/utils";
export default function YachtDetailPage() {
// const params = useParams();
const router = useRouter();
const [activeTab, setActiveTab] = useState<
| "availability"
| "description"
| "characteristics"
| "contact"
| "requisites"
| "reviews"
>("availability");
const { id } = useParams();
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
return (
<main className="bg-[#f4f4f4] min-h-screen ">
{/* Мобильная фиксированная верхняя панель навигации */}
<div className="lg:hidden fixed top-[73px] left-0 right-0 z-[50] bg-white border-b border-gray-200">
<div className="flex items-center justify-between px-4 h-14">
<button
onClick={() => router.back()}
className="flex items-center justify-center"
>
<ArrowLeft size={24} className="text-[#333333]" />
</button>
<h2 className="text-base font-medium text-[#333333]">
Яхта
const client = useApiClient();
useEffect(() => {
(async () => {
const response = await client.get<CatalogItemLongDto>(`/catalog/${id}/`);
setYacht(response.data);
})();
}, [id]);
// const params = useParams();
const router = useRouter();
const [activeTab, setActiveTab] = useState<
| "availability"
| "description"
| "characteristics"
| "contact"
| "requisites"
| "reviews"
>("availability");
if (!yacht) {
return <div />;
}
return (
<main className="bg-[#f4f4f4] min-h-screen ">
{/* Мобильная фиксированная верхняя панель навигации */}
<div className="lg:hidden fixed top-[73px] left-0 right-0 z-[50] bg-white border-b border-gray-200">
<div className="flex items-center justify-between px-4 h-14">
<button
onClick={() => router.back()}
className="flex items-center justify-center"
>
<ArrowLeft size={24} className="text-[#333333]" />
</button>
<h2 className="text-base font-medium text-[#333333]">Яхта</h2>
<button className="flex items-center justify-center">
<Heart size={24} className="text-[#333333]" />
</button>
</div>
</div>
{/* Десктопная версия - Breadcrumbs */}
<div className="hidden lg:block container max-w-6xl mx-auto px-4 py-4">
<div className="text-sm text-[#999999] flex items-center gap-4">
<Link href="/">
<span className="cursor-pointer hover:text-[#333333] transition-colors">
Аренда яхты
</span>
</Link>
<span>&gt;</span>
<Link href="/catalog">
<span className="cursor-pointer hover:text-[#333333] transition-colors">
Моторные яхты
</span>
</Link>
<span>&gt;</span>
<span className="text-[#333333]">{yacht.name}</span>
</div>
</div>
{/* Main Content Container */}
<div className="lg:container lg:max-w-6xl lg:mx-auto lg:px-4 lg:pb-6">
<div className="bg-white lg:rounded-[16px] lg:p-6">
{/* Мобильная версия - без отступов сверху, с отступом для фиксированной панели */}
<div className="lg:hidden pt-[50px]">
{/* Gallery */}
<YachtGallery
images={yacht.galleryUrls || []}
badge={!yacht.hasQuickRent ? "По запросу" : ""}
/>
{/* Yacht Title */}
<div className="px-4 pt-4">
<h1 className="text-xl font-bold text-[#333333] mb-4">
{yacht.name}
</h1>
</div>
{/* Tabs */}
<div className="px-4 border-b border-gray-200 overflow-x-auto">
<div className="flex gap-6 min-w-max">
<button
onClick={() => setActiveTab("availability")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "availability"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Свободные даты
</button>
<button
onClick={() => setActiveTab("description")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "description"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Описание
</button>
<button
onClick={() => setActiveTab("characteristics")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "characteristics"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Характеристики
</button>
<button
onClick={() => setActiveTab("contact")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "contact"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Контактное лицо и реквизиты
</button>
<button
onClick={() => setActiveTab("reviews")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "reviews"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Отзывы
</button>
</div>
</div>
{/* Tab Content */}
<div className="px-4 py-6">
{activeTab === "availability" && (
<YachtAvailability
price={String(yacht.minCost)}
mobile={true}
reservations={yacht.reservations}
/>
)}
{activeTab === "description" && (
<div>
<p className="text-base text-[#666666] leading-relaxed">
{yacht.description}
</p>
</div>
)}
{activeTab === "characteristics" && (
<YachtCharacteristics yacht={yacht} />
)}
{activeTab === "contact" && <ContactInfo {...yacht.owner} />}
{activeTab === "reviews" && (
<div>
<div className="flex items-center gap-2 mb-4">
<Icon name="reviewStar" size={16} />
<h2 className="text-base font-bold text-[#333333]">
Отзывы
</h2>
<button className="flex items-center justify-center">
<Heart size={24} className="text-[#333333]" />
</button>
</div>
<div className="border border-[#DFDFDF] rounded-[12px] flex items-center justify-center py-18">
<p className="text-lg text-[#999999]">
У этой яхты пока нет отзывов
</p>
</div>
</div>
)}
</div>
</div>
{/* Десктопная версия */}
<div className="hidden lg:block">
{/* Yacht Title and Actions */}
<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]">
{yacht.name}
</h1>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 text-[#333333]">
<Icon name="pin" size={32} />
<span className="text-base">{formatSpeed(yacht.speed)}</span>
</div>
<button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors">
<Icon name="share" size={32} />
<span className="text-base">Поделиться</span>
</button>
<button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors">
<Icon name="heart" size={32} />
<span className="text-base">Избранное</span>
</button>
</div>
</div>
{/* Десктопная версия - Breadcrumbs */}
<div className="hidden lg:block container max-w-6xl mx-auto px-4 py-4">
<div className="text-sm text-[#999999] flex items-center gap-4">
<Link href="/">
<span className="cursor-pointer hover:text-[#333333] transition-colors">
Аренда яхты
</span>
</Link>
<span>&gt;</span>
<Link href="/catalog">
<span className="cursor-pointer hover:text-[#333333] transition-colors">
Моторные яхты
</span>
</Link>
<span>&gt;</span>
<span className="text-[#333333]">{YACHT.name}</span>
</div>
</div>
{/* Main Content */}
<div className="space-y-6">
{/* Gallery */}
<YachtGallery
images={yacht.galleryUrls || []}
badge={!yacht.hasQuickRent ? "По запросу" : ""}
/>
{/* Main Content Container */}
<div className="lg:container lg:max-w-6xl lg:mx-auto lg:px-4 lg:pb-6">
<div className="bg-white lg:rounded-[16px] lg:p-6">
{/* Мобильная версия - без отступов сверху, с отступом для фиксированной панели */}
<div className="lg:hidden pt-[50px]">
{/* Gallery */}
<YachtGallery
images={YACHT.images}
badge={YACHT.badge}
/>
{/* Content with Booking Widget on the right */}
<div className="flex flex-col lg:flex-row gap-6 items-start">
{/* Left column - all content below gallery */}
<div className="flex-1 space-y-6">
{/* Availability */}
<YachtAvailability
price={String(yacht.minCost)}
reservations={yacht.reservations}
/>
{/* Yacht Title */}
<div className="px-4 pt-4">
<h1 className="text-xl font-bold text-[#333333] mb-4">
{YACHT.name}
</h1>
</div>
{/* Characteristics */}
<YachtCharacteristics yacht={yacht} />
{/* Tabs */}
<div className="px-4 border-b border-gray-200 overflow-x-auto">
<div className="flex gap-6 min-w-max">
<button
onClick={() => setActiveTab("availability")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "availability"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Свободные даты
</button>
<button
onClick={() => setActiveTab("description")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "description"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Описание
</button>
<button
onClick={() =>
setActiveTab("characteristics")
}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "characteristics"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Характеристики
</button>
<button
onClick={() => setActiveTab("contact")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "contact"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Контактное лицо и реквизиты
</button>
<button
onClick={() => setActiveTab("reviews")}
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
activeTab === "reviews"
? "text-[#008299] border-b-2 border-[#008299]"
: "text-[#999999]"
}`}
>
Отзывы
</button>
</div>
</div>
{/* Description */}
<div>
<h2 className="text-base font-bold text-[#333333] mb-4">
Описание
</h2>
<p className="text-base text-[#666666] leading-relaxed">
{yacht.description}
</p>
</div>
{/* Tab Content */}
<div className="px-4 py-6">
{activeTab === "availability" && (
<YachtAvailability
price={YACHT.price}
mobile={true}
/>
)}
{activeTab === "description" && (
<div>
<p className="text-base text-[#666666] leading-relaxed">
{YACHT.description}
</p>
</div>
)}
{activeTab === "characteristics" && (
<YachtCharacteristics yacht={YACHT} />
)}
{activeTab === "contact" && (
<ContactInfo
contactPerson={YACHT.contactPerson}
requisites={YACHT.requisites}
/>
)}
{activeTab === "reviews" && (
<div>
<div className="flex items-center gap-2 mb-4">
<Icon name="reviewStar" size={16} />
<h2 className="text-base font-bold text-[#333333]">
Отзывы
</h2>
</div>
<div className="border border-[#DFDFDF] rounded-[12px] flex items-center justify-center py-18">
<p className="text-lg text-[#999999]">
У этой яхты пока нет отзывов
</p>
</div>
</div>
)}
</div>
{/* Contact and Requisites */}
<ContactInfo {...yacht.owner} />
{/* Reviews */}
<div>
<div className="flex items-center gap-2 mb-4">
<Icon name="reviewStar" size={16} />
<h2 className="text-base font-bold text-[#333333]">
Отзывы
</h2>
</div>
{/* Десктопная версия */}
<div className="hidden lg:block">
{/* Yacht Title and Actions */}
<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]">
{YACHT.name}
</h1>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 text-[#333333]">
<Icon name="pin" size={32} />
<span className="text-base">
{YACHT.location}
</span>
</div>
<button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors">
<Icon name="share" size={32} />
<span className="text-base">
Поделиться
</span>
</button>
<button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors">
<Icon name="heart" size={32} />
<span className="text-base">Избранное</span>
</button>
</div>
</div>
{/* Main Content */}
<div className="space-y-6">
{/* Gallery */}
<YachtGallery
images={YACHT.images}
badge={YACHT.badge}
/>
{/* Content with Booking Widget on the right */}
<div className="flex flex-col lg:flex-row gap-6 items-start">
{/* Left column - all content below gallery */}
<div className="flex-1 space-y-6">
{/* Availability */}
<YachtAvailability price={YACHT.price} />
{/* Characteristics */}
<YachtCharacteristics yacht={YACHT} />
{/* Description */}
<div>
<h2 className="text-base font-bold text-[#333333] mb-4">
Описание
</h2>
<p className="text-base text-[#666666] leading-relaxed">
{YACHT.description}
</p>
</div>
{/* Contact and Requisites */}
<ContactInfo
contactPerson={YACHT.contactPerson}
requisites={YACHT.requisites}
/>
{/* Reviews */}
<div>
<div className="flex items-center gap-2 mb-4">
<Icon name="reviewStar" size={16} />
<h2 className="text-base font-bold text-[#333333]">
Отзывы
</h2>
</div>
<div className="border border-[#DFDFDF] rounded-[12px] flex items-center justify-center py-18">
<p className="text-lg text-[#999999]">
У этой яхты пока нет отзывов
</p>
</div>
</div>
</div>
{/* Right column - Booking Widget (sticky) */}
<div className="lg:w-74 flex-shrink-0 lg:sticky lg:top-24 self-start">
<BookingWidget price={YACHT.price} />
</div>
</div>
</div>
<div className="border border-[#DFDFDF] rounded-[12px] flex items-center justify-center py-18">
<p className="text-lg text-[#999999]">
У этой яхты пока нет отзывов
</p>
</div>
</div>
</div>
</div>
{/* Мобильная фиксированная нижняя панель бронирования */}
<div className="lg:hidden sticky bottom-0 left-0 right-0 z-[10] bg-white border-t border-gray-200 px-4 py-3">
<div className="flex items-center justify-between">
<div>
<span className="text-lg font-bold text-[#333333]">
{YACHT.price}
</span>
<span className="text-sm text-[#999999] ml-1">
/ час
</span>
</div>
<button
onClick={() => router.push("/confirm")}
className="bg-[#008299] text-white px-6 py-3 rounded-lg font-bold text-base hover:bg-[#006d7a] transition-colors"
>
Забронировать
</button>
{/* Right column - Booking Widget (sticky) */}
<div className="lg:w-74 flex-shrink-0 lg:sticky lg:top-24 self-start">
<BookingWidget price={String(yacht.minCost)} yacht={yacht} />
</div>
</div>
</div>
</main>
);
</div>
</div>
</div>
{/* Мобильная фиксированная нижняя панель бронирования */}
<div className="lg:hidden sticky bottom-0 left-0 right-0 z-[10] bg-white border-t border-gray-200 px-4 py-3">
<div className="flex items-center justify-between">
<div>
<span className="text-lg font-bold text-[#333333]">
{yacht.minCost}
</span>
<span className="text-sm text-[#999999] ml-1">/ час</span>
</div>
<button
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"
>
Забронировать
</button>
</div>
</div>
</main>
);
}

View File

@ -3,11 +3,11 @@
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import Image from "next/image";
import Icon from "@/components/ui/icon";
@ -15,179 +15,170 @@ import { useState } from "react";
import { GuestDatePicker } from "@/components/form/guest-date-picker";
import { formatMinCost, formatWidth, getImageUrl } from "@/lib/utils";
export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
export default function FeaturedYacht({
yacht,
}: {
yacht: CatalogItemShortDto;
}) {
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
const handleThumbnailClick = (imageSrc: string) => {
setSelectedImage(imageSrc);
};
const handleThumbnailClick = (imageSrc: string) => {
setSelectedImage(imageSrc);
};
return (
<div className="mb-10">
<Card className="overflow-hidden bg-white text-gray-900">
<CardContent className="p-0">
<div className="flex flex-col lg:flex-row gap-11 px-6 py-10">
{/* Left side - Yacht details and images */}
<div className="flex-1">
{/* Promoted banner - Mobile only */}
<div
className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative lg:hidden"
style={{
backgroundImage:
"url(/images/badge-bg.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<span className="text-xs font-medium relative z-10">
Заметнее других бронируют быстрее
</span>
</div>
return (
<div className="mb-10">
<Card className="overflow-hidden bg-white text-gray-900">
<CardContent className="p-0">
<div className="flex flex-col lg:flex-row gap-11 px-6 py-10">
{/* Left side - Yacht details and images */}
<div className="flex-1">
{/* Promoted banner - Mobile only */}
<div
className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative lg:hidden"
style={{
backgroundImage: "url(/images/badge-bg.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<span className="text-xs font-medium relative z-10">
Заметнее других бронируют быстрее
</span>
</div>
{/* Header with yacht name and length */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold">
{yacht.name}
</h2>
<div className="flex items-center gap-2 text-gray-600">
<Icon size={16} name="width" />
<span className="text-lg">
{formatWidth(yacht.length)}
</span>
</div>
</div>
{/* Header with yacht name and length */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold">{yacht.name}</h2>
<div className="flex items-center gap-2 text-gray-600">
<Icon size={16} name="width" />
<span className="text-lg">{formatWidth(yacht.length)}</span>
</div>
</div>
{/* Main yacht image */}
<div className="relative mb-6">
<Image
src={getImageUrl(selectedImage)}
alt={yacht.name}
width={600}
height={400}
className="w-full h-80 object-cover rounded-[24px]"
unoptimized
/>
</div>
{/* Main yacht image */}
<div className="relative mb-6">
<Image
src={getImageUrl(selectedImage)}
alt={yacht.name}
width={600}
height={400}
className="w-full h-80 object-cover rounded-[24px]"
unoptimized
/>
</div>
{/* Thumbnail images carousel */}
<div className="relative mb-6">
<Carousel
opts={{
align: "start",
loop: false,
slidesToScroll: 2,
}}
className="w-full"
>
<CarouselContent className="-ml-2 md:-ml-4">
{yacht.galleryUrls.map((thumb, idx) => (
<CarouselItem
key={idx}
className="pl-2 md:pl-4 basis-auto"
>
<div className="relative">
<Image
src={getImageUrl(thumb)}
alt={`${yacht.name
} view ${idx + 1}`}
width={80}
height={60}
className={`w-20 h-16 object-cover rounded-[8px] cursor-pointer border-2 transition-all ${selectedImage ===
thumb
? "border-[#008299]"
: "border-gray-200 hover:border-gray-400"
}`}
onClick={() =>
handleThumbnailClick(
thumb
)
}
unoptimized
/>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 -translate-y-1/2 bg-white hover:bg-gray-50 shadow-lg z-10" />
<CarouselNext className="absolute right-2 top-1/2 -translate-y-1/2 bg-white hover:bg-gray-50 shadow-lg z-10" />
</Carousel>
</div>
{/* Promoted badge */}
{yacht.isFeatured && (
<div className="flex items-center gap-2 text-sm text-gray-400">
<Icon
className="min-w-[21px] min-h-[21px]"
size={21}
name="ad"
/>
<span>
Это объявление продвигается.{" "}
<span className="underline cursor-pointer">
Хотите так же?
</span>
</span>
</div>
)}
{/* Thumbnail images carousel */}
<div className="relative mb-6">
<Carousel
opts={{
align: "start",
loop: false,
slidesToScroll: 2,
}}
className="w-full"
>
<CarouselContent className="-ml-2 md:-ml-4">
{yacht.galleryUrls.map((thumb, idx) => (
<CarouselItem
key={idx}
className="pl-2 md:pl-4 basis-auto"
>
<div className="relative">
<Image
src={getImageUrl(thumb)}
alt={`${yacht.name} view ${idx + 1}`}
width={80}
height={60}
className={`w-20 h-16 object-cover rounded-[8px] cursor-pointer border-2 transition-all ${
selectedImage === thumb
? "border-[#008299]"
: "border-gray-200 hover:border-gray-400"
}`}
onClick={() => handleThumbnailClick(thumb)}
unoptimized
/>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 -translate-y-1/2 bg-white hover:bg-gray-50 shadow-lg z-10" />
<CarouselNext className="absolute right-2 top-1/2 -translate-y-1/2 bg-white hover:bg-gray-50 shadow-lg z-10" />
</Carousel>
</div>
{/* Right side - Booking form */}
<div className="min-w-[296px] flex-0 flex flex-col justify-between">
<div>
{/* Promoted banner - Desktop only */}
<div
className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative hidden lg:flex"
style={{
backgroundImage:
"url(/images/badge-bg.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<span className="text-xs font-medium relative z-10">
Заметнее других бронируют быстрее
</span>
</div>
{/* Promoted badge */}
{yacht.isFeatured && (
<div className="flex items-center gap-2 text-sm text-gray-400">
<Icon
className="min-w-[21px] min-h-[21px]"
size={21}
name="ad"
/>
<span>
Это объявление продвигается.{" "}
<span className="underline cursor-pointer">
Хотите так же?
</span>
</span>
</div>
)}
</div>
<div className="border rounded-[16px] p-6 pb-8 border-gray-200 pt-6">
{/* Price */}
<div className="mb-6">
<p className="text-3xl font-bold whitespace-nowrap">
{formatMinCost(yacht.minCost)}
<span className="text-sm font-normal text-gray-500">
/ час
</span>
</p>
</div>
{/* Right side - Booking form */}
<div className="min-w-[296px] flex-0 flex flex-col justify-between">
<div>
{/* Promoted banner - Desktop only */}
<div
className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative hidden lg:flex"
style={{
backgroundImage: "url(/images/badge-bg.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<span className="text-xs font-medium relative z-10">
Заметнее других бронируют быстрее
</span>
</div>
{/* Booking form */}
<div className="mb-8">
<GuestDatePicker />
</div>
<div className="border rounded-[16px] p-6 pb-8 border-gray-200 pt-6">
{/* Price */}
<div className="mb-6">
<p className="text-3xl font-bold whitespace-nowrap">
{formatMinCost(yacht.minCost)}
<span className="text-sm font-normal text-gray-500">
/ час
</span>
</p>
</div>
{/* Book button */}
<Button
variant="gradient"
className="font-bold text-white h-[64px] w-full px-8"
>
Забронировать
</Button>
{/* Booking form */}
<div className="mb-8">
<GuestDatePicker />
</div>
{/* Total price */}
<div className="flex justify-between items-center text-l mt-6 font-bold text-gray-800">
<span className="font-normal">
Итого:
</span>
<span>0 </span>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
{/* Book button */}
<Button
variant="gradient"
className="font-bold text-white h-[64px] w-full px-8"
>
Забронировать
</Button>
{/* Total price */}
<div className="flex justify-between items-center text-l mt-6 font-bold text-gray-800">
<span className="font-normal">Итого:</span>
<span>0 </span>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -8,157 +8,156 @@ import Link from "next/link";
import FeaturedYacht from "./FeaturedYacht";
import useApiClient from "@/hooks/useApiClient";
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() {
const client = useApiClient();
const client = useApiClient();
const [featuredYacht, setFeaturedYacht] = useState<CatalogItemDto | null>(null);
const [yachtCatalog, setYachtCatalog] = useState<CatalogItemDto[] | null>(null);
const [featuredYacht, setFeaturedYacht] =
useState<CatalogItemShortDto | null>(null);
const [yachtCatalog, setYachtCatalog] = useState<
CatalogItemShortDto[] | null
>(null);
useEffect(() => {
(async () => {
const response = await client.get<MainPageCatalogResponseDto>("/catalog/main-page/");
setFeaturedYacht(response.data.featuredYacht);
setYachtCatalog(response.data.restYachts);
})();
}, [])
useEffect(() => {
(async () => {
const response = await client.get<MainPageCatalogResponseDto>(
"/catalog/main-page/"
);
setFeaturedYacht(response.data.featuredYacht);
setYachtCatalog(response.data.restYachts);
})();
}, []);
return (
<section className="text-white">
<div className="container max-w-6xl mx-auto px-4 mt-6 md:mt-12">
{/* Header Section */}
<div className="mb-6">
<h1 className="text-3xl md:text-4xl font-bold mb-4 text-black">
Яхты и катера в аренду
</h1>
<h2 className="text-l text-black font-bold mb-2">
Онлайн бронирование яхт и катеров
</h2>
<p className="text-gray-700 max-w-3xl leading-relaxed">
Каталог лучших яхт Балаклавы разных ценовых сегментах.
</p>
<p className="text-gray-700 leading-relaxed">
Проверенные лодки с лицензией на перевозки, опытные
капитаны. Выбирайте удобную дату, время и бронируйте.
</p>
</div>
return (
<section className="text-white">
<div className="container max-w-6xl mx-auto px-4 mt-6 md:mt-12">
{/* Header Section */}
<div className="mb-6">
<h1 className="text-3xl md:text-4xl font-bold mb-4 text-black">
Яхты и катера в аренду
</h1>
<h2 className="text-l text-black font-bold mb-2">
Онлайн бронирование яхт и катеров
</h2>
<p className="text-gray-700 max-w-3xl leading-relaxed">
Каталог лучших яхт Балаклавы разных ценовых сегментах.
</p>
<p className="text-gray-700 leading-relaxed">
Проверенные лодки с лицензией на перевозки, опытные капитаны.
Выбирайте удобную дату, время и бронируйте.
</p>
</div>
{/* Featured Yacht Block */}
{featuredYacht && (
<FeaturedYacht yacht={featuredYacht} />
)}
{/* Featured Yacht Block */}
{featuredYacht && <FeaturedYacht yacht={featuredYacht} />}
{/* Yacht Grid */}
{yachtCatalog && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
{yachtCatalog.map((yacht) => (
<Link
key={yacht.id}
href={`/catalog/${yacht.id ?? 0}}`}
className="block"
>
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg">
<CardHeader className="p-0 relative">
<div className="relative">
{/* Best Offer Badge - над карточкой */}
{yacht.topText && (
<div className="w-full flex justify-center">
<div
className="flex w-full items-center justify-center text-white px-4 py-2 text-sm font-medium bg-cover bg-center bg-no-repeat"
style={{
backgroundImage:
"url('/images/best-yacht-bg.jpg')",
}}
>
<span>
{yacht.topText}
</span>
</div>
</div>
)}
<Image
src={getImageUrl(yacht.mainImageUrl)}
alt={yacht.name}
width={400}
height={250}
className="w-full h-48 object-cover"
/>
{/* Badge Overlay */}
{!yacht.hasQuickRent && !yacht.topText && (
<>
<div className="absolute top-3 left-3">
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm flex items-center gap-1">
<Icon
size={16}
name="restart"
/>
<span>
По запросу
</span>
</div>
</div>
</>
)}
</div>
</CardHeader>
<CardContent className="p-4">
<div className="flex justify-between gap-4">
{/* Левая колонка - название и длина */}
<div className="space-y-2">
<h3 className="font-bold text-l">
{yacht.name}
</h3>
<div className="flex items-center gap-1 text-sm">
<Icon size={16} name="width" />
<span>{formatWidth(yacht.length)}</span>
</div>
</div>
{/* Правая колонка - цена и футы */}
<div className="flex flex-col justify-between">
<div className="w-fit">
{yacht.isBestOffer ? (
<p
style={{
background:
"linear-gradient(90deg, #008299 0%, #7E8FFF 100%)",
}}
className="text-l font-bold text-white pl-2 pr-3 rounded-t-[5px] rounded-bl-[10px] rounded-br-[30px] whitespace-nowrap"
>
{formatMinCost(yacht.minCost)} / час
</p>
) : (
<p className="w-fit text-l whitespace-nowrap">
{formatMinCost(yacht.minCost)} / час
</p>
)}
</div>
<div className="flex items-center gap-1 text-sm">
<Icon size={16} name="anchor" />
<span>{formatSpeed(yacht.speed)}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</Link>
))}
{/* Yacht Grid */}
{yachtCatalog && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
{yachtCatalog.map((yacht) => (
<Link
key={yacht.id}
href={`/catalog/${yacht.id ?? 0}`}
className="block"
>
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg">
<CardHeader className="p-0 relative">
<div className="relative">
{/* Best Offer Badge - над карточкой */}
{yacht.topText && (
<div className="w-full flex justify-center">
<div
className="flex w-full items-center justify-center text-white px-4 py-2 text-sm font-medium bg-cover bg-center bg-no-repeat"
style={{
backgroundImage:
"url('/images/best-yacht-bg.jpg')",
}}
>
<span>{yacht.topText}</span>
</div>
</div>
)}
<Image
src={getImageUrl(yacht.mainImageUrl)}
alt={yacht.name}
width={400}
height={250}
className="w-full h-48 object-cover"
/>
{/* Badge Overlay */}
{!yacht.hasQuickRent && !yacht.topText && (
<>
<div className="absolute top-3 left-3">
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm flex items-center gap-1">
<Icon size={16} name="restart" />
<span>По запросу</span>
</div>
</div>
</>
)}
</div>
)}
</CardHeader>
<CardContent className="p-4">
<div className="flex justify-between gap-4">
{/* Левая колонка - название и длина */}
<div className="space-y-2">
<h3 className="font-bold text-l">{yacht.name}</h3>
<div className="flex items-center gap-1 text-sm">
<Icon size={16} name="width" />
<span>{formatWidth(yacht.length)}</span>
</div>
</div>
{/* Call to Action Button */}
<div className="text-center">
<Link href="/catalog">
<Button
size="lg"
className="bg-white text-gray-900 hover:bg-gray-100 px-8 py-3 h-[56px] w-[336px] text-lg font-bold"
>
Каталог яхт
</Button>
</Link>
</div>
</div>
</section>
);
{/* Правая колонка - цена и футы */}
<div className="flex flex-col justify-between">
<div className="w-fit">
{yacht.isBestOffer ? (
<p
style={{
background:
"linear-gradient(90deg, #008299 0%, #7E8FFF 100%)",
}}
className="text-l font-bold text-white pl-2 pr-3 rounded-t-[5px] rounded-bl-[10px] rounded-br-[30px] whitespace-nowrap"
>
{formatMinCost(yacht.minCost)} / час
</p>
) : (
<p className="w-fit text-l whitespace-nowrap">
{formatMinCost(yacht.minCost)} / час
</p>
)}
</div>
<div className="flex items-center gap-1 text-sm">
<Icon size={16} name="anchor" />
<span>{formatSpeed(yacht.speed)}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
)}
{/* Call to Action Button */}
<div className="text-center">
<Link href="/catalog">
<Button
size="lg"
className="bg-white text-gray-900 hover:bg-gray-100 px-8 py-3 h-[56px] w-[336px] text-lg font-bold"
>
Каталог яхт
</Button>
</Link>
</div>
</div>
</section>
);
}

View File

@ -4,456 +4,528 @@ import { Button } from "@/components/ui/button";
import Image from "next/image";
import Link from "next/link";
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 useApiClient from "@/hooks/useApiClient";
import { getImageUrl } from "@/lib/utils";
import { differenceInHours, parseISO } from "date-fns";
export default function ConfirmPage() {
const [promocode, setPromocode] = useState("");
const router = useRouter();
const searchParams = useSearchParams();
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
const [totalHours, setTotalHours] = useState<number>(0);
const [totalPrice, setTotalPrice] = useState<number>(0);
// Извлекаем параметры из 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 client = useApiClient();
const [promocode, setPromocode] = useState("");
const router = useRouter();
const searchParams = useSearchParams();
// Функция для форматирования даты (краткий формат)
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;
}
};
// Извлекаем параметры из URL
const yachtId = searchParams.get("yachtId");
const guestCount = searchParams.get("guests");
const departureDate = searchParams.get("departureDate");
const departureTime = searchParams.get("departureTime");
const arrivalDate = searchParams.get("arrivalDate");
const arrivalTime = searchParams.get("arrivalTime");
// Функция для форматирования даты (полный формат для десктопа)
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;
}
};
useEffect(() => {
(async () => {
const response = await client.get<CatalogItemLongDto>(
`/catalog/${yachtId}/`
);
setYacht(response.data);
})();
}, [yachtId]);
// Функция для форматирования времени
const formatTime = (timeString: string | null) => {
if (!timeString) return null;
// Предполагаем формат HH:mm или HH:mm:ss
return timeString.split(":").slice(0, 2).join(":");
};
// Расчет стоимости при изменении дат
useEffect(() => {
if (
departureDate &&
departureTime &&
arrivalDate &&
arrivalTime &&
yacht?.minCost
) {
try {
// Создаем полные даты
const departureDateTime = parseISO(`${departureDate}T${departureTime}`);
const arrivalDateTime = parseISO(`${arrivalDate}T${arrivalTime}`);
// Форматируем данные для отображения
const departureDateFormatted = formatDate(departureDate);
const departureTimeFormatted = formatTime(departureTime);
const arrivalDateFormatted = formatDate(arrivalDate);
const arrivalTimeFormatted = formatTime(arrivalTime);
// Рассчитываем разницу в часах (с округлением до 0.5 часа)
let hoursDiff = differenceInHours(arrivalDateTime, departureDateTime);
// Полный формат для десктопной версии
const departureDateFormattedFull = formatDateFull(departureDate);
const arrivalDateFormattedFull = formatDateFull(arrivalDate);
// Добавляем разницу в минутах
const minutesDiff =
(arrivalDateTime.getMinutes() - departureDateTime.getMinutes()) / 60;
hoursDiff += minutesDiff;
// Формируем строки для отображения
const departureDisplay = departureDateFormatted && departureTimeFormatted
? `${departureDateFormatted} ${departureTimeFormatted}`
: "Не выбрано";
// Округляем до ближайших 0.5 часа
const roundedHours = Math.ceil(hoursDiff * 2) / 2;
const arrivalDisplay = arrivalDateFormatted && arrivalTimeFormatted
? `${arrivalDateFormatted} ${arrivalTimeFormatted}`
: "Не выбрано";
// Рассчитываем стоимость
const pricePerHour = yacht.minCost;
const total = pricePerHour * roundedHours;
const datesDisplay = departureDateFormattedFull && departureTimeFormatted && arrivalDateFormattedFull && arrivalTimeFormatted
? `${departureDateFormattedFull} в ${departureTimeFormatted}${arrivalDateFormattedFull} в ${arrivalTimeFormatted}`
: "Не выбрано";
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 guestsDisplay = guestCount
? guestCount === "1" ? "1 гость" : `${guestCount} гостей`
: "Не выбрано";
// Функция для форматирования даты (краткий формат)
const formatDate = (dateString: string | null) => {
if (!dateString) return null;
try {
const date = parseISO(dateString);
const months = [
"янв",
"фев",
"мар",
"апр",
"май",
"июн",
"июл",
"авг",
"сен",
"окт",
"ноя",
"дек",
];
const day = date.getDate();
const month = months[date.getMonth()];
return `${day} ${month}`;
} catch {
return null;
}
};
return (
<main className="bg-[#f4f4f4] grow">
{/* Мобильная версия */}
<div className="lg:hidden">
{/* Верхний блок с навигацией */}
<div className="bg-white border-b border-[#DFDFDF]">
<div className="container max-w-6xl mx-auto px-4 py-3">
<div className="flex items-center justify-between gap-4">
{/* Кнопка назад */}
<button
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"
>
<ArrowLeft
size={20}
className="text-[#333333]"
/>
</button>
// Функция для форматирования даты (полный формат для десктопа)
const formatDateFull = (dateString: string | null) => {
if (!dateString) return null;
try {
const date = parseISO(dateString);
const months = [
"января",
"февраля",
"марта",
"апреля",
"мая",
"июня",
"июля",
"августа",
"сентября",
"октября",
"ноября",
"декабря",
];
const day = date.getDate();
const month = months[date.getMonth()];
return `${day} ${month}`;
} catch {
return null;
}
};
{/* Центральный блок с информацией */}
<div className="flex-1 min-w-0 text-center">
<h2 className="text-base font-bold text-[#333333] mb-1">
Яхта Сеньорита
</h2>
<div className="flex justify-center gap-10 text-xs text-[#666666]">
<span>{departureDateFormatted || "Не выбрано"}</span>
<span>Гостей: {guestCount || "Не выбрано"}</span>
</div>
</div>
// Функция для форматирования времени
const formatTime = (timeString: string | null) => {
if (!timeString) return null;
return timeString.split(":").slice(0, 2).join(":");
};
{/* Кнопка избранного */}
<button className="flex-shrink-0 w-10 h-10 flex items-center justify-center hover:opacity-70 transition-opacity">
<Heart
size={20}
className="text-[#333333] stroke-2"
/>
</button>
</div>
</div>
// Форматируем данные для отображения
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} гостей`
: "Не выбрано";
// Форматирование цены с разделителями тысяч
const formatPrice = (price: number) => {
return new Intl.NumberFormat("ru-RU").format(price);
};
if (!yacht) {
return <div />;
}
return (
<main className="bg-[#f4f4f4] grow">
{/* Мобильная версия */}
<div className="lg:hidden">
{/* Верхний блок с навигацией */}
<div className="bg-white border-b border-[#DFDFDF]">
<div className="container max-w-6xl mx-auto px-4 py-3">
<div className="flex items-center justify-between gap-4">
{/* Кнопка назад */}
<button
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"
>
<ArrowLeft size={20} className="text-[#333333]" />
</button>
{/* Центральный блок с информацией */}
<div className="flex-1 min-w-0 text-center">
<h2 className="text-base font-bold text-[#333333] mb-1">
Яхта {yacht.name}
</h2>
<div className="flex justify-center gap-10 text-xs text-[#666666]">
<span>{departureDateFormatted || "Не выбрано"}</span>
<span>Гостей: {guestCount || "Не выбрано"}</span>
</div>
</div>
<div className="container max-w-6xl mx-auto">
<div className="bg-white p-4">
{/* Заголовок с иконкой */}
<div className="flex items-center gap-2 mb-6">
<h1 className="text-xl text-[#333333]">
Ваше бронирование 🛥
</h1>
</div>
{/* Кнопка избранного */}
<button className="flex-shrink-0 w-10 h-10 flex items-center justify-center hover:opacity-70 transition-opacity">
<Heart size={20} className="text-[#333333] stroke-2" />
</button>
</div>
</div>
</div>
{/* Поля Выход и Заход */}
<div className="grid grid-cols-2 gap-3 mb-3">
<div className="relative">
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
Выход
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
<div className="text-[#333333]">
{departureDisplay}
</div>
</div>
</div>
<div className="relative">
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
Заход
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
<div className="text-[#333333]">
{arrivalDisplay}
</div>
</div>
</div>
</div>
{/* По местному времени яхты */}
<div className="flex items-center gap-2 text-sm text-[#333333] mb-4">
<Map size={16} className="text-[#333333]" />
<span>По местному времени яхты</span>
</div>
{/* Гости */}
<div className="mb-6">
<div className="relative">
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
Гостей
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF] flex items-center justify-between">
<span className="text-[#333333]">
{guestsDisplay}
</span>
</div>
</div>
</div>
{/* Правила отмены */}
<div className="mb-8">
<h3 className="text-base font-bold text-[#333333] mb-4">
Правила отмены
</h3>
<p className="text-base text-[#333333]">
При отмене до 10 мая вы получите частичный
возврат.{" "}
<Link
href="#"
className="text-sm text-[#2D908D] hover:text-[#007088] font-bold transition-colors"
>
Подробнее
</Link>
</p>
</div>
{/* Детализация цены */}
<div className="mb-8">
<h3 className="text-base font-bold text-[#333333] mb-4">
Детализация цены
</h3>
<div>
<div className="flex justify-between items-center mb-4">
<span className="text-[#333333]">
26 400 x 2ч
</span>
<span className="text-[#333333]">
52 800
</span>
</div>
<div className="flex justify-between items-center mb-4 pb-4 border-b border-[#DFDFDF]">
<span className="text-[#333333]">
Услуги
</span>
<span className="text-[#333333]">0 Р</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[#333333]">
Итого:
</span>
<span className="font-bold text-[#333333]">
52 800 Р
</span>
</div>
</div>
</div>
{/* Промокод */}
<div className="mb-4 pb-6 border-b border-[#DFDFDF]">
<div className="w-full flex gap-2">
<input
type="text"
placeholder="Промокод"
value={promocode}
onChange={(e) =>
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"
/>
<Button
variant="default"
className="flex-shrink-0 h-[64px] w-[64px] bg-[#2D908D] hover:bg-[#007088] text-white rounded-full p-0 transition-colors duration-200"
>
<ArrowUpRight size={14} />
</Button>
</div>
</div>
{/* Кнопка отправки заявки */}
<Button
variant="default"
className="w-full h-[56px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full transition-colors duration-200"
>
Отправить заявку
</Button>
</div>
</div>
<div className="container max-w-6xl mx-auto">
<div className="bg-white p-4">
{/* Заголовок с иконкой */}
<div className="flex items-center gap-2 mb-6">
<h1 className="text-xl text-[#333333]">Ваше бронирование 🛥</h1>
</div>
{/* Десктопная версия */}
<div className="hidden lg:block">
<div className="container max-w-6xl mx-auto px-4 py-6">
{/* Breadcrumbs - скрыты на мобильных */}
<div className="hidden lg:flex mb-6 text-sm text-[#999999] items-center gap-[16px]">
<Link href="/">
<span className="cursor-pointer hover:text-[#333333] transition-colors">
Аренда яхты
{/* Поля Выход и Заход */}
<div className="grid grid-cols-2 gap-3 mb-3">
<div className="relative">
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
Выход
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
<div className="text-[#333333]">{departureDisplay}</div>
</div>
</div>
<div className="relative">
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
Заход
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
<div className="text-[#333333]">{arrivalDisplay}</div>
</div>
</div>
</div>
{/* По местному времени яхты */}
<div className="flex items-center gap-2 text-sm text-[#333333] mb-4">
<Map size={16} className="text-[#333333]" />
<span>По местному времени яхты</span>
</div>
{/* Гости */}
<div className="mb-6">
<div className="relative">
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
Гостей
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF] flex items-center justify-between">
<span className="text-[#333333]">{guestsDisplay}</span>
</div>
</div>
</div>
{/* Правила отмены */}
<div className="mb-8">
<h3 className="text-base font-bold text-[#333333] mb-4">
Правила отмены
</h3>
<p className="text-base text-[#333333]">
При отмене до 10 мая вы получите частичный возврат.{" "}
<Link
href="#"
className="text-sm text-[#2D908D] hover:text-[#007088] font-bold transition-colors"
>
Подробнее
</Link>
</p>
</div>
{/* Детализация цены */}
<div className="mb-8">
<h3 className="text-base font-bold text-[#333333] mb-4">
Детализация цены
</h3>
<div>
{totalHours > 0 && yacht.minCost ? (
<>
<div className="flex justify-between items-center mb-4">
<span className="text-[#333333]">
{formatPrice(yacht.minCost)} × {totalHours}ч
</span>
<span className="text-[#333333]">
{formatPrice(totalPrice)}
</span>
</div>
<div className="flex justify-between items-center mb-4 pb-4 border-b border-[#DFDFDF]">
<span className="text-[#333333]">Услуги</span>
<span className="text-[#333333]">0 Р</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[#333333]">Итого:</span>
<span className="font-bold text-[#333333]">
{formatPrice(totalPrice)} Р
</span>
</div>
</>
) : (
<div className="text-[#999999] text-center py-4">
Укажите даты для расчета стоимости
</div>
)}
</div>
</div>
{/* Промокод */}
<div className="mb-4 pb-6 border-b border-[#DFDFDF]">
<div className="w-full flex gap-2">
<input
type="text"
placeholder="Промокод"
value={promocode}
onChange={(e) => 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"
/>
<Button
variant="default"
className="flex-shrink-0 h-[64px] w-[64px] bg-[#2D908D] hover:bg-[#007088] text-white rounded-full p-0 transition-colors duration-200"
>
<ArrowUpRight size={14} />
</Button>
</div>
</div>
{/* Кнопка отправки заявки */}
<Button
variant="default"
className="w-full h-[56px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full transition-colors duration-200"
disabled={totalHours === 0}
>
Отправить заявку
</Button>
</div>
</div>
</div>
{/* Десктопная версия */}
<div className="hidden lg:block">
<div className="container max-w-6xl mx-auto px-4 py-6">
{/* Breadcrumbs - скрыты на мобильных */}
<div className="hidden lg:flex mb-6 text-sm text-[#999999] items-center gap-[16px]">
<Link href="/">
<span className="cursor-pointer hover:text-[#333333] transition-colors">
Аренда яхты
</span>
</Link>
<span>&gt;</span>
<span className="text-[#333333]">Ваше бронирование</span>
</div>
<div className="flex flex-col lg:flex-row gap-6">
{/* Левая колонка - Информация о яхте и ценах - скрыта на мобильных */}
<div className="hidden lg:flex w-full lg:w-[336px] flex-shrink-0 flex-col gap-6">
<div className="bg-white rounded-[16px]">
<div className="p-4">
{/* Изображение яхты */}
<div className="relative mb-5">
<Image
src={getImageUrl(yacht.mainImageUrl)}
alt="Яхта"
width={400}
height={250}
className="w-full h-48 object-cover rounded-[8px]"
/>
{/* Плашка владельца */}
<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">
<User size={22} className="text-[#999999]" />
<div className="flex flex-col gap-[4px]">
<span className="text-[#999999]">Владелец</span>
<span className="text-[#333333] font-bold">
{yacht.owner.firstName}
</span>
</div>
</div>
</div>
</div>
{/* Название яхты */}
<h3 className="text-base text-[#333333] pb-3 border-b border-[#DFDFDF] mb-4">
Яхта {yacht.name}
</h3>
{/* Детализация цены */}
<div>
<h4 className="text-base font-bold text-[#333333] mb-4">
Детализация цены
</h4>
<div>
{totalHours > 0 && yacht.minCost ? (
<>
<div className="flex justify-between items-center mb-4">
<span className="text-[#333333]">
{formatPrice(yacht.minCost)} × {totalHours}ч
</span>
</Link>
<span>&gt;</span>
<span className="text-[#333333]">
Ваше бронирование
</span>
</div>
<div className="flex flex-col lg:flex-row gap-6">
{/* Левая колонка - Информация о яхте и ценах - скрыта на мобильных */}
<div className="hidden lg:flex w-full lg:w-[336px] flex-shrink-0 flex-col gap-6">
<div className="bg-white rounded-[16px]">
<div className="p-4">
{/* Изображение яхты */}
<div className="relative mb-5">
<Image
src="/images/yachts/yacht1.jpg"
alt="Яхта"
width={400}
height={250}
className="w-full h-48 object-cover rounded-[8px]"
/>
{/* Плашка владельца */}
<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">
<User
size={22}
className="text-[#999999]"
/>
<div className="flex flex-col gap-[4px]">
<span className="text-[#999999]">
Владелец
</span>
<span className="text-[#333333] font-bold">
Денис
</span>
</div>
</div>
</div>
</div>
{/* Название яхты */}
<h3 className="text-base text-[#333333] pb-3 border-b border-[#DFDFDF] mb-4">
Яхта
</h3>
{/* Детализация цены */}
<div>
<h4 className="text-base font-bold text-[#333333] mb-4">
Детализация цены
</h4>
<div>
<div className="flex justify-between items-center mb-4">
<span className="text-[#333333]">
26 400 x 2ч
</span>
<span className="text-[#333333]">
52 800
</span>
</div>
<div className="flex justify-between items-center border-b border-[#DFDFDF] pb-4 mb-4">
<span className="text-[#333333]">
Услуги
</span>
<span className="text-[#333333]">
0 Р
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[#333333]">
Итого:
</span>
<span className="text-[#333333] font-bold">
52 800 Р
</span>
</div>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-[16px]">
<div className="p-6">
{/* Промокод */}
<div className="w-full flex gap-2">
<input
type="text"
placeholder="Промокод"
value={promocode}
onChange={(e) =>
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"
/>
<Button
variant="default"
className="flex-shrink-0 h-[64px] w-[64px] bg-[#2D908D] hover:bg-[#007088] text-white rounded-full p-0 transition-colors duration-200"
>
<ArrowUpRight size={12} />
</Button>
</div>
</div>
</div>
</div>
{/* Правая колонка - Подтверждение бронирования */}
<div className="flex-1">
<div className="bg-white rounded-[16px] p-8">
{/* Заголовок */}
<h1 className="text-2xl text-[#333333] mb-4">
Проверьте данные
</h1>
<h2 className="text-base text-[#333333] font-bold mb-4">
Ваше бронирование
</h2>
{/* Сведения о бронировании */}
<div className="grid grid-cols-2 gap-4 mb-4">
{/* Даты */}
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
<div className="text-[#333333] mb-1">
Даты
</div>
<div className="text-base text-[#999999]">
{datesDisplay}
</div>
</div>
{/* Гости */}
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
<div>
<div className="text-[#333333] mb-1">
Гости
</div>
<div className="text-base text-[#999999]">
{guestsDisplay}
</div>
</div>
</div>
</div>
{/* Дополнительные услуги */}
<div className="flex items-center h-[88px] border border-[#DFDFDF] rounded-[8px] p-4 mb-6">
<div className="text-base text-[#333333]">
Нет дополнительных услуг
</div>
</div>
{/* Правила отмены */}
<h3 className="text-base font-bold text-[#333333] mb-4">
Правила отмены
</h3>
<p className="text-[#333333]">
При отмене до 10 мая вы получите частичный
возврат.
</p>
<Link
href="#"
className="text-[#2D908D] hover:text-[#007088] font-bold transition-colors"
>
Подробнее
</Link>
{/* Указание времени и кнопка отправки */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 mt-6">
<div className="flex items-center gap-2 text-sm text-[#666666]">
<Map size={20} />
<span className="text-[#333333]">
По местному времени яхты
</span>
</div>
<Button
variant="default"
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"
>
Отправить заявку
</Button>
</div>
</div>
<span className="text-[#333333]">
{formatPrice(totalPrice)}
</span>
</div>
<div className="flex justify-between items-center border-b border-[#DFDFDF] pb-4 mb-4">
<span className="text-[#333333]">Услуги</span>
<span className="text-[#333333]">0 Р</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[#333333]">Итого:</span>
<span className="text-[#333333] font-bold">
{formatPrice(totalPrice)} Р
</span>
</div>
</>
) : (
<div className="text-[#999999] text-center py-4">
Укажите даты для расчета стоимости
</div>
)}
</div>
</div>
</div>
</div>
<div className="bg-white rounded-[16px]">
<div className="p-6">
{/* Промокод */}
<div className="w-full flex gap-2">
<input
type="text"
placeholder="Промокод"
value={promocode}
onChange={(e) => 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"
/>
<Button
variant="default"
className="flex-shrink-0 h-[64px] w-[64px] bg-[#2D908D] hover:bg-[#007088] text-white rounded-full p-0 transition-colors duration-200"
>
<ArrowUpRight size={12} />
</Button>
</div>
</div>
</div>
</div>
</main>
);
{/* Правая колонка - Подтверждение бронирования */}
<div className="flex-1">
<div className="bg-white rounded-[16px] p-8">
{/* Заголовок */}
<h1 className="text-2xl text-[#333333] mb-4">
Проверьте данные
</h1>
<h2 className="text-base text-[#333333] font-bold mb-4">
Ваше бронирование
</h2>
{/* Сведения о бронирования */}
<div className="grid grid-cols-2 gap-4 mb-4">
{/* Даты */}
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
<div className="text-[#333333] mb-1">Даты</div>
<div className="text-base text-[#999999]">
{datesDisplay}
</div>
</div>
{/* Гости */}
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
<div>
<div className="text-[#333333] mb-1">Гости</div>
<div className="text-base text-[#999999]">
{guestsDisplay}
</div>
</div>
</div>
</div>
{/* Дополнительные услуги */}
<div className="flex items-center h-[88px] border border-[#DFDFDF] rounded-[8px] p-4 mb-6">
<div className="text-base text-[#333333]">
Нет дополнительных услуг
</div>
</div>
{/* Правила отмены */}
<h3 className="text-base font-bold text-[#333333] mb-4">
Правила отмены
</h3>
<p className="text-[#333333]">
При отмене до 10 мая вы получите частичный возврат.
</p>
<Link
href="#"
className="text-[#2D908D] hover:text-[#007088] font-bold transition-colors"
>
Подробнее
</Link>
{/* Указание времени и кнопка отправки */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 mt-6">
<div className="flex items-center gap-2 text-sm text-[#666666]">
<Map size={20} />
<span className="text-[#333333]">
По местному времени яхты
</span>
</div>
<Button
variant="default"
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"
disabled={totalHours === 0}
>
Отправить заявку
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
);
}

View File

@ -9,302 +9,295 @@ import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Counter } from "@/components/ui/counter";
import {
Popover,
PopoverContent,
PopoverTrigger,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
interface GuestDatePickerProps {
onApply?: (data: {
date: Date | undefined;
departureTime: string;
arrivalTime: string;
adults: number;
children: number;
}) => void;
className?: string;
onApply?: (data: {
date: Date | undefined;
departureTime: string;
arrivalTime: string;
adults: number;
children: number;
}) => void;
className?: string;
}
interface CommonPopoverContentProps {
date: Date | undefined;
setDate: (date: Date | undefined) => void;
departureTime: string;
setDepartureTime: (time: string) => void;
arrivalTime: string;
setArrivalTime: (time: string) => void;
adults: number;
setAdults: (count: number) => void;
childrenCount: number;
setChildrenCount: (count: number) => void;
handleApply: () => void;
date: Date | undefined;
setDate: (date: Date | undefined) => void;
departureTime: string;
setDepartureTime: (time: string) => void;
arrivalTime: string;
setArrivalTime: (time: string) => void;
adults: number;
setAdults: (count: number) => void;
childrenCount: number;
setChildrenCount: (count: number) => void;
handleApply: () => void;
}
const CommonPopoverContent: React.FC<CommonPopoverContentProps> = ({
date,
setDate,
departureTime,
setDepartureTime,
arrivalTime,
setArrivalTime,
adults,
setAdults,
childrenCount,
setChildrenCount,
handleApply,
date,
setDate,
departureTime,
setDepartureTime,
arrivalTime,
setArrivalTime,
adults,
setAdults,
childrenCount,
setChildrenCount,
handleApply,
}) => {
return (
<PopoverContent className="rounded-[20px] p-6 pb-4 w-[324px]">
{/* Календарь */}
return (
<PopoverContent className="rounded-[20px] p-6 pb-4 w-[324px]">
{/* Календарь */}
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="mb-[24px]"
locale={ru}
disabled={(date) =>
date < new Date(new Date().setHours(0, 0, 0, 0))
}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-4",
button_previous:
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
button_next:
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
month_caption:
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
table: "w-full border-collapse",
weekdays: "flex",
weekday:
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
day_button: "font-bold ring-0 focus:ring-0",
week: "mt-2 flex w-full",
today: "bg-gray-100 text-gray-900 rounded-full",
outside: "text-gray-300",
disabled: "text-gray-400 cursor-not-allowed",
selected:
"rounded-full border-none outline-none !bg-brand text-white",
}}
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="mb-[24px]"
locale={ru}
disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-4",
button_previous:
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
button_next:
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
month_caption:
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
table: "w-full border-collapse",
weekdays: "flex",
weekday: "flex-1 text-gray-500 text-xs font-normal p-2 text-center",
day_button: "font-bold ring-0 focus:ring-0",
week: "mt-2 flex w-full",
today: "bg-gray-100 text-gray-900 rounded-full",
outside: "text-gray-300",
disabled: "text-gray-400 cursor-not-allowed",
selected:
"rounded-full border-none outline-none !bg-brand text-white",
}}
/>
{/* Счетчики гостей */}
<div className="mb-[24px] flex gap-3">
<Counter
label="Взрослые"
value={adults}
onChange={setAdults}
min={0}
max={10}
/>
<Counter
label="Дети"
value={childrenCount}
onChange={setChildrenCount}
min={0}
max={10}
/>
</div>
{/* Поля времени */}
<div className="flex gap-3 mb-6">
<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>
<div className="relative h-full flex align-center">
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
<input
type="time"
value={departureTime}
onChange={(e) => setDepartureTime(e.target.value)}
className="w-full focus:outline-none focus:border-transparent"
/>
</div>
</div>
<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>
<div className="relative h-full flex align-center">
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
<input
type="time"
value={arrivalTime}
onChange={(e) => setArrivalTime(e.target.value)}
className="w-full focus:outline-none focus:border-transparent"
/>
</div>
</div>
</div>
{/* Счетчики гостей */}
<div className="mb-[24px] flex gap-3">
<Counter
label="Взрослые"
value={adults}
onChange={setAdults}
min={0}
max={10}
/>
<Counter
label="Дети"
value={childrenCount}
onChange={setChildrenCount}
min={0}
max={10}
/>
</div>
{/* Поля времени */}
<div className="flex gap-3 mb-6">
<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>
<div className="relative h-full flex align-center">
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
<input
type="time"
value={departureTime}
onChange={(e) => setDepartureTime(e.target.value)}
className="w-full focus:outline-none focus:border-transparent"
/>
</div>
</div>
<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>
<div className="relative h-full flex align-center">
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
<input
type="time"
value={arrivalTime}
onChange={(e) => setArrivalTime(e.target.value)}
className="w-full focus:outline-none focus:border-transparent"
/>
</div>
</div>
</div>
{/* Кнопка Применить */}
<Button
onClick={handleApply}
variant="gradient"
className="font-bold text-white h-[44px] w-full px-8"
>
Применить
</Button>
</PopoverContent>
);
{/* Кнопка Применить */}
<Button
onClick={handleApply}
variant="gradient"
className="font-bold text-white h-[44px] w-full px-8"
>
Применить
</Button>
</PopoverContent>
);
};
export const GuestDatePicker: React.FC<GuestDatePickerProps> = ({
onApply,
className,
onApply,
className,
}) => {
const [date, setDate] = useState<Date>();
const [departureTime, setDepartureTime] = useState("12:00");
const [arrivalTime, setArrivalTime] = useState("13:00");
const [adults, setAdults] = useState(1);
const [children, setChildren] = useState(0);
const [isDepartureOpen, setIsDepartureOpen] = useState(false);
const [isArrivalOpen, setIsArrivalOpen] = useState(false);
const [isGuestOpen, setIsGuestOpen] = useState(false);
const [date, setDate] = useState<Date>();
const [departureTime, setDepartureTime] = useState("12:00");
const [arrivalTime, setArrivalTime] = useState("13:00");
const [adults, setAdults] = useState(1);
const [children, setChildren] = useState(0);
const [isDepartureOpen, setIsDepartureOpen] = useState(false);
const [isArrivalOpen, setIsArrivalOpen] = useState(false);
const [isGuestOpen, setIsGuestOpen] = useState(false);
const handleApply = () => {
onApply?.({
date,
departureTime,
arrivalTime,
adults,
children,
});
setIsDepartureOpen(false);
setIsArrivalOpen(false);
setIsGuestOpen(false);
};
const handleApply = () => {
onApply?.({
date,
departureTime,
arrivalTime,
adults,
children,
});
setIsDepartureOpen(false);
setIsArrivalOpen(false);
setIsGuestOpen(false);
};
const getDepartureDisplayText = () => {
if (!date || !departureTime) return "Выход";
return (
<>
{format(date, "d MMMM", {
locale: ru,
})}
, <span className="font-bold">{departureTime}</span>
</>
);
};
const getArrivalDisplayText = () => {
if (!date || !arrivalTime) return "Заход";
return (
<>
{format(date, "d MMMM", {
locale: ru,
})}
, <span className="font-bold">{arrivalTime}</span>
</>
);
};
const getGuestDisplayText = () => {
if (adults === 1 && children === 0) return "1 гость";
return (
<span className="font-bold">
Взрослых: {adults}, Детей: {children}
</span>
);
};
const getDepartureDisplayText = () => {
if (!date || !departureTime) return "Выход";
return (
<div className={className}>
<div className="space-y-5">
{/* Кнопка Выход */}
<Popover
open={isDepartureOpen}
onOpenChange={setIsDepartureOpen}
>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-[64px] px-4 w-full justify-between font-normal"
>
<div className="flex items-center">
<span>{getDepartureDisplayText()}</span>
</div>
</Button>
</PopoverTrigger>
<CommonPopoverContent
date={date}
setDate={setDate}
departureTime={departureTime}
setDepartureTime={setDepartureTime}
arrivalTime={arrivalTime}
setArrivalTime={setArrivalTime}
adults={adults}
setAdults={setAdults}
childrenCount={children}
setChildrenCount={setChildren}
handleApply={handleApply}
/>
</Popover>
{/* Кнопка Заход */}
<Popover open={isArrivalOpen} onOpenChange={setIsArrivalOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-[64px] px-4 w-full justify-between font-normal"
>
<div className="flex items-center">
<span>{getArrivalDisplayText()}</span>
</div>
</Button>
</PopoverTrigger>
<CommonPopoverContent
date={date}
setDate={setDate}
departureTime={departureTime}
setDepartureTime={setDepartureTime}
arrivalTime={arrivalTime}
setArrivalTime={setArrivalTime}
adults={adults}
setAdults={setAdults}
childrenCount={children}
setChildrenCount={setChildren}
handleApply={handleApply}
/>
</Popover>
{/* Кнопка Гости */}
<Popover open={isGuestOpen} onOpenChange={setIsGuestOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-[64px] px-4 w-full justify-between font-normal"
>
<div className="flex items-center">
<span>{getGuestDisplayText()}</span>
</div>
{isGuestOpen ? (
<ChevronUpIcon className="h-4 w-4" />
) : (
<ChevronDownIcon className="h-4 w-4" />
)}
</Button>
</PopoverTrigger>
<CommonPopoverContent
date={date}
setDate={setDate}
departureTime={departureTime}
setDepartureTime={setDepartureTime}
arrivalTime={arrivalTime}
setArrivalTime={setArrivalTime}
adults={adults}
setAdults={setAdults}
childrenCount={children}
setChildrenCount={setChildren}
handleApply={handleApply}
/>
</Popover>
</div>
</div>
<>
{format(date, "d MMMM", {
locale: ru,
})}
, <span className="font-bold">{departureTime}</span>
</>
);
};
const getArrivalDisplayText = () => {
if (!date || !arrivalTime) return "Заход";
return (
<>
{format(date, "d MMMM", {
locale: ru,
})}
, <span className="font-bold">{arrivalTime}</span>
</>
);
};
const getGuestDisplayText = () => {
if (adults === 1 && children === 0) return "1 гость";
return (
<span className="font-bold">
Взрослых: {adults}, Детей: {children}
</span>
);
};
return (
<div className={className}>
<div className="space-y-5">
{/* Кнопка Выход */}
<Popover open={isDepartureOpen} onOpenChange={setIsDepartureOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-[64px] px-4 w-full justify-between font-normal"
>
<div className="flex items-center">
<span>{getDepartureDisplayText()}</span>
</div>
</Button>
</PopoverTrigger>
<CommonPopoverContent
date={date}
setDate={setDate}
departureTime={departureTime}
setDepartureTime={setDepartureTime}
arrivalTime={arrivalTime}
setArrivalTime={setArrivalTime}
adults={adults}
setAdults={setAdults}
childrenCount={children}
setChildrenCount={setChildren}
handleApply={handleApply}
/>
</Popover>
{/* Кнопка Заход */}
<Popover open={isArrivalOpen} onOpenChange={setIsArrivalOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-[64px] px-4 w-full justify-between font-normal"
>
<div className="flex items-center">
<span>{getArrivalDisplayText()}</span>
</div>
</Button>
</PopoverTrigger>
<CommonPopoverContent
date={date}
setDate={setDate}
departureTime={departureTime}
setDepartureTime={setDepartureTime}
arrivalTime={arrivalTime}
setArrivalTime={setArrivalTime}
adults={adults}
setAdults={setAdults}
childrenCount={children}
setChildrenCount={setChildren}
handleApply={handleApply}
/>
</Popover>
{/* Кнопка Гости */}
<Popover open={isGuestOpen} onOpenChange={setIsGuestOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-[64px] px-4 w-full justify-between font-normal"
>
<div className="flex items-center">
<span>{getGuestDisplayText()}</span>
</div>
{isGuestOpen ? (
<ChevronUpIcon className="h-4 w-4" />
) : (
<ChevronDownIcon className="h-4 w-4" />
)}
</Button>
</PopoverTrigger>
<CommonPopoverContent
date={date}
setDate={setDate}
departureTime={departureTime}
setDepartureTime={setDepartureTime}
arrivalTime={arrivalTime}
setArrivalTime={setArrivalTime}
adults={adults}
setAdults={setAdults}
childrenCount={children}
setChildrenCount={setChildren}
handleApply={handleApply}
/>
</Popover>
</div>
</div>
);
};

View File

@ -8,193 +8,202 @@ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import Icon from "./icon";
interface DatePickerProps {
showIcon?: boolean;
variant?: "default" | "small";
placeholder?: string;
value?: Date | null;
departureTime?: string;
arrivalTime?: string;
onDateChange?: (date: Date | undefined) => void;
onDepartureTimeChange?: (time: string) => void;
onArrivalTimeChange?: (time: string) => void;
showIcon?: boolean;
variant?: "default" | "small";
placeholder?: string;
value?: Date | null;
departureTime?: string;
arrivalTime?: string;
onDateChange?: (date: Date | undefined) => void;
onDepartureTimeChange?: (time: string) => void;
onArrivalTimeChange?: (time: string) => void;
onlyDeparture?: boolean;
onlyArrival?: boolean;
}
export function DatePicker({
showIcon = true,
variant = "default",
placeholder = "Выберите дату и время",
value,
departureTime: externalDepartureTime,
arrivalTime: externalArrivalTime,
onDateChange,
onDepartureTimeChange,
onArrivalTimeChange,
showIcon = true,
variant = "default",
placeholder = "Выберите дату и время",
value,
departureTime: externalDepartureTime,
arrivalTime: externalArrivalTime,
onDateChange,
onDepartureTimeChange,
onArrivalTimeChange,
onlyDeparture,
onlyArrival,
}: 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 [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 isControlled = value !== undefined || externalDepartureTime !== undefined || externalArrivalTime !== undefined;
// Определяем, является ли компонент контролируемым
const isControlled =
value !== undefined ||
externalDepartureTime !== undefined ||
externalArrivalTime !== undefined;
// Используем внешние значения, если они предоставлены, иначе внутренние
const date = value !== undefined ? (value || undefined) : internalDate;
const departureTime = externalDepartureTime !== undefined ? externalDepartureTime : internalDepartureTime;
const arrivalTime = externalArrivalTime !== undefined ? externalArrivalTime : internalArrivalTime;
// Используем внешние значения, если они предоставлены, иначе внутренние
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 if (!isControlled) {
setInternalDate(newDate);
}
};
const handleDateChange = (newDate: Date | undefined) => {
if (onDateChange) {
onDateChange(newDate);
} else if (!isControlled) {
setInternalDate(newDate);
}
};
const handleDepartureTimeChange = (time: string) => {
if (onDepartureTimeChange) {
onDepartureTimeChange(time);
} else if (!isControlled) {
setInternalDepartureTime(time);
}
};
const handleDepartureTimeChange = (time: string) => {
if (onDepartureTimeChange) {
onDepartureTimeChange(time);
} else if (!isControlled) {
setInternalDepartureTime(time);
}
};
const handleArrivalTimeChange = (time: string) => {
if (onArrivalTimeChange) {
onArrivalTimeChange(time);
} else if (!isControlled) {
setInternalArrivalTime(time);
}
};
const handleArrivalTimeChange = (time: string) => {
if (onArrivalTimeChange) {
onArrivalTimeChange(time);
} else if (!isControlled) {
setInternalArrivalTime(time);
}
};
const handleApply = () => {
// Закрываем popover после применения
setOpen(false);
};
const handleApply = () => {
// Закрываем popover после применения
setOpen(false);
};
const heightClass = variant === "small" ? "h-[48px]" : "h-[64px]";
const heightClass = variant === "small" ? "h-[48px]" : "h-[64px]";
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
data-empty={!date}
className={`w-full ${heightClass} justify-between text-left font-normal`}
>
<div className="flex items-center">
{showIcon && (
<Icon
name="calendar"
className="w-4 h-4 text-brand mr-2"
/>
)}
{date ? (
format(
date,
`d MMMM, ${departureTime} - ${arrivalTime}`,
{ locale: ru }
)
) : (
<span>{placeholder}</span>
)}
</div>
{open ? (
<ChevronUpIcon className="w-4 h-4" />
) : (
<ChevronDownIcon className="w-4 h-4" />
)}
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 bg-white rounded-[20px] shadow-lg">
<div className="p-4 w-full">
{/* Календарь */}
<Calendar
mode="single"
selected={date}
onSelect={handleDateChange}
className="mb-4 "
locale={ru}
disabled={(date) =>
date < new Date(new Date().setHours(0, 0, 0, 0))
}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-4",
button_previous:
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
button_next:
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
month_caption:
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
table: "w-full border-collapse",
weekdays: "flex",
weekday:
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
day_button: "font-bold ring-0 focus:ring-0",
week: "mt-2 flex w-full",
today: "bg-gray-100 text-gray-900 rounded-full",
outside: "text-gray-300",
disabled: "text-gray-400 cursor-not-allowed",
selected:
"rounded-full border-none outline-none !bg-brand text-white",
}}
/>
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
data-empty={!date}
className={`w-full ${heightClass} justify-between text-left font-normal`}
>
<div className="flex items-center">
{showIcon && (
<Icon name="calendar" className="w-4 h-4 text-brand mr-2" />
)}
{date ? (
format(date, `d MMMM, ${departureTime} - ${arrivalTime}`, {
locale: ru,
})
) : (
<span>{placeholder}</span>
)}
</div>
{open ? (
<ChevronUpIcon className="w-4 h-4" />
) : (
<ChevronDownIcon className="w-4 h-4" />
)}
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 bg-white rounded-[20px] shadow-lg">
<div className="p-4 w-full">
{/* Календарь */}
<Calendar
mode="single"
selected={date}
onSelect={handleDateChange}
className="mb-4 "
locale={ru}
disabled={(date) =>
date < new Date(new Date().setHours(0, 0, 0, 0))
}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-4",
button_previous:
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
button_next:
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
month_caption:
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
table: "w-full border-collapse",
weekdays: "flex",
weekday:
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
day_button: "font-bold ring-0 focus:ring-0",
week: "mt-2 flex w-full",
today: "bg-gray-100 text-gray-900 rounded-full",
outside: "text-gray-300",
disabled: "text-gray-400 cursor-not-allowed",
selected:
"rounded-full border-none outline-none !bg-brand text-white",
}}
/>
{/* Поля времени */}
<div className="flex gap-3 mb-4">
<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>
<div className="relative h-full flex align-center">
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
<input
type="time"
value={departureTime}
onChange={(e) =>
handleDepartureTimeChange(e.target.value)
}
className="w-full focus:outline-none focus:border-transparent"
/>
</div>
</div>
<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>
<div className="relative h-full flex align-center">
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
<input
type="time"
value={arrivalTime}
onChange={(e) =>
handleArrivalTimeChange(e.target.value)
}
className="w-full focus:outline-none focus:border-transparent"
/>
</div>
</div>
</div>
{/* Кнопка Применить */}
<Button
onClick={handleApply}
variant="gradient"
className="font-bold text-white h-[44px] w-full px-8"
>
Применить
</Button>
{/* Поля времени */}
<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">
<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>
<div className="relative h-full flex align-center">
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
<input
type="time"
value={departureTime}
onChange={(e) => handleDepartureTimeChange(e.target.value)}
className="w-full focus:outline-none focus:border-transparent"
/>
</div>
</PopoverContent>
</Popover>
);
</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">
<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>
<div className="relative h-full flex align-center">
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
<input
type="time"
value={arrivalTime}
onChange={(e) => handleArrivalTimeChange(e.target.value)}
className="w-full focus:outline-none focus:border-transparent"
/>
</div>
</div>
) : null}
</div>
{/* Кнопка Применить */}
<Button
onClick={handleApply}
variant="gradient"
className="font-bold text-white h-[44px] w-full px-8"
>
Применить
</Button>
</div>
</PopoverContent>
</Popover>
);
}

View File

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