Add all possible bullshit

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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