Add all possible bullshit
This commit is contained in:
parent
d177eee970
commit
745d58ab3a
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:00—20: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:00—20: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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
@ -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>></span>
|
||||
<Link href="/catalog">
|
||||
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
||||
Моторные яхты
|
||||
</span>
|
||||
</Link>
|
||||
<span>></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>></span>
|
||||
<Link href="/catalog">
|
||||
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
||||
Моторные яхты
|
||||
</span>
|
||||
</Link>
|
||||
<span>></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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
: "Не выбрано";
|
||||
|
||||
const arrivalDisplay = arrivalDateFormatted && arrivalTimeFormatted
|
||||
? `${arrivalDateFormatted} ${arrivalTimeFormatted}`
|
||||
: "Не выбрано";
|
||||
// Округляем до ближайших 0.5 часа
|
||||
const roundedHours = Math.ceil(hoursDiff * 2) / 2;
|
||||
|
||||
const datesDisplay = departureDateFormattedFull && departureTimeFormatted && arrivalDateFormattedFull && arrivalTimeFormatted
|
||||
? `${departureDateFormattedFull} в ${departureTimeFormatted} — ${arrivalDateFormattedFull} в ${arrivalTimeFormatted}`
|
||||
: "Не выбрано";
|
||||
// Рассчитываем стоимость
|
||||
const pricePerHour = yacht.minCost;
|
||||
const total = pricePerHour * roundedHours;
|
||||
|
||||
const guestsDisplay = guestCount
|
||||
? guestCount === "1" ? "1 гость" : `${guestCount} гостей`
|
||||
: "Не выбрано";
|
||||
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]);
|
||||
|
||||
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 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;
|
||||
}
|
||||
};
|
||||
|
||||
{/* Центральный блок с информацией */}
|
||||
<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 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;
|
||||
}
|
||||
};
|
||||
|
||||
{/* Кнопка избранного */}
|
||||
<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 formatTime = (timeString: string | null) => {
|
||||
if (!timeString) return null;
|
||||
return timeString.split(":").slice(0, 2).join(":");
|
||||
};
|
||||
|
||||
// Форматируем данные для отображения
|
||||
const departureDateFormatted = formatDate(departureDate);
|
||||
const departureTimeFormatted = formatTime(departureTime);
|
||||
const arrivalDateFormatted = formatDate(arrivalDate);
|
||||
const arrivalTimeFormatted = formatTime(arrivalTime);
|
||||
|
||||
// Полный формат для десктопной версии
|
||||
const departureDateFormattedFull = formatDateFull(departureDate);
|
||||
const arrivalDateFormattedFull = formatDateFull(arrivalDate);
|
||||
|
||||
// Формируем строки для отображения
|
||||
const departureDisplay =
|
||||
departureDateFormatted && departureTimeFormatted
|
||||
? `${departureDateFormatted} ${departureTimeFormatted}`
|
||||
: "Не выбрано";
|
||||
|
||||
const arrivalDisplay =
|
||||
arrivalDateFormatted && arrivalTimeFormatted
|
||||
? `${arrivalDateFormatted} ${arrivalTimeFormatted}`
|
||||
: "Не выбрано";
|
||||
|
||||
const datesDisplay =
|
||||
departureDateFormattedFull &&
|
||||
departureTimeFormatted &&
|
||||
arrivalDateFormattedFull &&
|
||||
arrivalTimeFormatted
|
||||
? `${departureDateFormattedFull} в ${departureTimeFormatted} — ${arrivalDateFormattedFull} в ${arrivalTimeFormatted}`
|
||||
: "Не выбрано";
|
||||
|
||||
const guestsDisplay = guestCount
|
||||
? guestCount === "1"
|
||||
? "1 гость"
|
||||
: `${guestCount} гостей`
|
||||
: "Не выбрано";
|
||||
|
||||
// Форматирование цены с разделителями тысяч
|
||||
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>></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>></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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
export function DatePicker({
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue