Доработка флоу
This commit is contained in:
parent
b249ab597b
commit
63725ff710
|
|
@ -1,11 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
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";
|
import { format } from "date-fns";
|
||||||
|
import { calculateTotalPrice, formatPrice } from "@/lib/utils";
|
||||||
|
|
||||||
interface BookingWidgetProps {
|
interface BookingWidgetProps {
|
||||||
price: string;
|
price: string;
|
||||||
|
|
@ -16,22 +17,43 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [departureDate, setDepartureDate] = useState<Date | undefined>();
|
const [departureDate, setDepartureDate] = useState<Date | undefined>();
|
||||||
const [arrivalDate, setArrivalDate] = useState<Date | undefined>();
|
const [arrivalDate, setArrivalDate] = useState<Date | undefined>();
|
||||||
|
const [departureTime, setDepartureTime] = useState<string>("12:00");
|
||||||
|
const [arrivalTime, setArrivalTime] = useState<string>("13:00");
|
||||||
const [guests, setGuests] = useState({ adults: 1, children: 0 });
|
const [guests, setGuests] = useState({ adults: 1, children: 0 });
|
||||||
const [total] = useState(0);
|
|
||||||
|
// Расчет итоговой стоимости
|
||||||
|
const total = useMemo(() => {
|
||||||
|
if (!departureDate || !arrivalDate || !departureTime || !arrivalTime || !yacht?.minCost) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const departureDateStr = format(departureDate, "yyyy-MM-dd");
|
||||||
|
const arrivalDateStr = format(arrivalDate, "yyyy-MM-dd");
|
||||||
|
|
||||||
|
const { totalPrice } = calculateTotalPrice(
|
||||||
|
departureDateStr,
|
||||||
|
departureTime,
|
||||||
|
arrivalDateStr,
|
||||||
|
arrivalTime,
|
||||||
|
yacht.minCost
|
||||||
|
);
|
||||||
|
|
||||||
|
return totalPrice;
|
||||||
|
}, [departureDate, arrivalDate, departureTime, arrivalTime, yacht?.minCost]);
|
||||||
|
|
||||||
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;
|
if (!departureDate || !arrivalDate || !departureTime || !arrivalTime || !yacht || !yacht.id) return;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
yachtId: yacht.id.toString(),
|
yachtId: yacht.id.toString(),
|
||||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||||
departureTime: format(departureDate, "HH:mm"),
|
departureTime: departureTime,
|
||||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||||
arrivalTime: format(arrivalDate, "HH:mm"),
|
arrivalTime: arrivalTime,
|
||||||
guests: (guests.adults + guests.children).toString(),
|
guests: (guests.adults + guests.children).toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -58,6 +80,8 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
||||||
showIcon={false}
|
showIcon={false}
|
||||||
onDateChange={setDepartureDate}
|
onDateChange={setDepartureDate}
|
||||||
value={departureDate}
|
value={departureDate}
|
||||||
|
departureTime={departureTime}
|
||||||
|
onDepartureTimeChange={setDepartureTime}
|
||||||
onlyDeparture
|
onlyDeparture
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,6 +96,8 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
||||||
showIcon={false}
|
showIcon={false}
|
||||||
onDateChange={setArrivalDate}
|
onDateChange={setArrivalDate}
|
||||||
value={arrivalDate}
|
value={arrivalDate}
|
||||||
|
arrivalTime={arrivalTime}
|
||||||
|
onArrivalTimeChange={setArrivalTime}
|
||||||
onlyArrival
|
onlyArrival
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -95,7 +121,7 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
||||||
onClick={handleBook}
|
onClick={handleBook}
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
className="w-full h-12 font-bold text-white mb-4"
|
className="w-full h-12 font-bold text-white mb-4"
|
||||||
disabled={!departureDate || !arrivalDate}
|
disabled={!departureDate || !arrivalDate || !departureTime || !arrivalTime}
|
||||||
>
|
>
|
||||||
Забронировать
|
Забронировать
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -103,7 +129,7 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
||||||
<div className="pt-4 border-t border-gray-200">
|
<div className="pt-4 border-t border-gray-200">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-base text-[#333333]">Итого:</span>
|
<span className="text-base text-[#333333]">Итого:</span>
|
||||||
<span className="text-base font-bold text-[#333333]">{total} ₽</span>
|
<span className="text-base font-bold text-[#333333]">{formatPrice(total)} ₽</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,19 +26,53 @@ interface YachtAvailabilityProps {
|
||||||
price: string;
|
price: string;
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
reservations?: Reservation[];
|
reservations?: Reservation[];
|
||||||
|
// Controlled props для мобильной версии
|
||||||
|
selectedDate?: Date;
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
onDateChange?: (date: Date | undefined) => void;
|
||||||
|
onStartTimeChange?: (time: string) => void;
|
||||||
|
onEndTimeChange?: (time: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function YachtAvailability({
|
export function YachtAvailability({
|
||||||
price,
|
price,
|
||||||
mobile = false,
|
mobile = false,
|
||||||
reservations = [],
|
reservations = [],
|
||||||
|
selectedDate,
|
||||||
|
startTime: controlledStartTime,
|
||||||
|
endTime: controlledEndTime,
|
||||||
|
onDateChange,
|
||||||
|
onStartTimeChange,
|
||||||
|
onEndTimeChange,
|
||||||
}: 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 [endTime, setEndTime] = useState<string>("");
|
// Используем контролируемые значения или внутреннее состояние
|
||||||
|
const [internalStartTime, setInternalStartTime] = useState<string>("");
|
||||||
|
const [internalEndTime, setInternalEndTime] = useState<string>("");
|
||||||
|
|
||||||
|
const startTime = mobile && controlledStartTime !== undefined ? controlledStartTime : internalStartTime;
|
||||||
|
const endTime = mobile && controlledEndTime !== undefined ? controlledEndTime : internalEndTime;
|
||||||
|
|
||||||
|
const handleStartTimeChange = (time: string) => {
|
||||||
|
if (mobile && onStartTimeChange) {
|
||||||
|
onStartTimeChange(time);
|
||||||
|
} else {
|
||||||
|
setInternalStartTime(time);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndTimeChange = (time: string) => {
|
||||||
|
if (mobile && onEndTimeChange) {
|
||||||
|
onEndTimeChange(time);
|
||||||
|
} else {
|
||||||
|
setInternalEndTime(time);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -214,6 +248,12 @@ export function YachtAvailability({
|
||||||
<div style={{ flexShrink: 0 }}>
|
<div style={{ flexShrink: 0 }}>
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
|
selected={selectedDate}
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (onDateChange) {
|
||||||
|
onDateChange(date);
|
||||||
|
}
|
||||||
|
}}
|
||||||
month={currentMonth}
|
month={currentMonth}
|
||||||
onMonthChange={setCurrentMonth}
|
onMonthChange={setCurrentMonth}
|
||||||
showOutsideDays={false}
|
showOutsideDays={false}
|
||||||
|
|
@ -248,6 +288,10 @@ export function YachtAvailability({
|
||||||
|
|
||||||
const isCrossedOut = shouldBeCrossedOut(day.date);
|
const isCrossedOut = shouldBeCrossedOut(day.date);
|
||||||
const hasRes = hasReservationsOnDate(day.date);
|
const hasRes = hasReservationsOnDate(day.date);
|
||||||
|
const isSelected = selectedDate &&
|
||||||
|
selectedDate.getDate() === day.date.getDate() &&
|
||||||
|
selectedDate.getMonth() === day.date.getMonth() &&
|
||||||
|
selectedDate.getFullYear() === day.date.getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -255,6 +299,8 @@ export function YachtAvailability({
|
||||||
className={`relative w-full flex items-center justify-center text-sm font-medium transition-colors ${
|
className={`relative w-full flex items-center justify-center text-sm font-medium transition-colors ${
|
||||||
isCrossedOut
|
isCrossedOut
|
||||||
? "text-[#CCCCCC] line-through"
|
? "text-[#CCCCCC] line-through"
|
||||||
|
: isSelected
|
||||||
|
? "bg-[#008299] text-white rounded-full"
|
||||||
: "text-[#333333] hover:bg-gray-100"
|
: "text-[#333333] hover:bg-gray-100"
|
||||||
}`}
|
}`}
|
||||||
style={
|
style={
|
||||||
|
|
@ -266,7 +312,7 @@ export function YachtAvailability({
|
||||||
disabled={isCrossedOut}
|
disabled={isCrossedOut}
|
||||||
>
|
>
|
||||||
{day.date.getDate()}
|
{day.date.getDate()}
|
||||||
{hasRes && !isCrossedOut && (
|
{hasRes && !isCrossedOut && !isSelected && (
|
||||||
<div className="absolute bottom-1 right-1 w-1.5 h-1.5 bg-[#2F5CD0] rounded-full"></div>
|
<div className="absolute bottom-1 right-1 w-1.5 h-1.5 bg-[#2F5CD0] rounded-full"></div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -282,7 +328,7 @@ export function YachtAvailability({
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<select
|
<select
|
||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={(e) => setStartTime(e.target.value)}
|
onChange={(e) => handleStartTimeChange(e.target.value)}
|
||||||
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
|
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
|
||||||
>
|
>
|
||||||
<option value="">--:--</option>
|
<option value="">--:--</option>
|
||||||
|
|
@ -296,7 +342,7 @@ export function YachtAvailability({
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<select
|
<select
|
||||||
value={endTime}
|
value={endTime}
|
||||||
onChange={(e) => setEndTime(e.target.value)}
|
onChange={(e) => handleEndTimeChange(e.target.value)}
|
||||||
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
|
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
|
||||||
>
|
>
|
||||||
<option value="">--:--</option>
|
<option value="">--:--</option>
|
||||||
|
|
|
||||||
|
|
@ -10,287 +10,359 @@ 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 { GuestPicker } from "@/components/form/guest-picker";
|
||||||
import useApiClient from "@/hooks/useApiClient";
|
import useApiClient from "@/hooks/useApiClient";
|
||||||
import { formatSpeed } from "@/lib/utils";
|
import { formatSpeed } from "@/lib/utils";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
export default function YachtDetailPage() {
|
export default function YachtDetailPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
|
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
|
||||||
|
|
||||||
const client = useApiClient();
|
const client = useApiClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const response = await client.get<CatalogItemLongDto>(`/catalog/${id}/`);
|
const response = await client.get<CatalogItemLongDto>(
|
||||||
|
`/catalog/${id}/`
|
||||||
|
);
|
||||||
|
|
||||||
setYacht(response.data);
|
setYacht(response.data);
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
// const params = useParams();
|
// const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
| "availability"
|
| "availability"
|
||||||
| "description"
|
| "description"
|
||||||
| "characteristics"
|
| "characteristics"
|
||||||
| "contact"
|
| "contact"
|
||||||
| "requisites"
|
| "requisites"
|
||||||
| "reviews"
|
| "reviews"
|
||||||
>("availability");
|
>("availability");
|
||||||
|
|
||||||
if (!yacht) {
|
// Состояние для мобильного бронирования
|
||||||
return <div />;
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>();
|
||||||
}
|
const [startTime, setStartTime] = useState<string>("");
|
||||||
|
const [endTime, setEndTime] = useState<string>("");
|
||||||
|
const [guests, setGuests] = useState({ adults: 1, children: 0 });
|
||||||
|
|
||||||
return (
|
const handleGuestsChange = (adults: number, children: number) => {
|
||||||
<main className="bg-[#f4f4f4] min-h-screen ">
|
setGuests({ adults, children });
|
||||||
{/* Мобильная фиксированная верхняя панель навигации */}
|
};
|
||||||
<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 */}
|
const handleBookMobile = () => {
|
||||||
<div className="hidden lg:block container max-w-6xl mx-auto px-4 py-4">
|
if (!selectedDate || !startTime || !endTime || !yacht || !yacht.id)
|
||||||
<div className="text-sm text-[#999999] flex items-center gap-4">
|
return;
|
||||||
<Link href="/">
|
|
||||||
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
|
||||||
Аренда яхты
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<span>></span>
|
|
||||||
<Link href="/catalog">
|
|
||||||
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
|
||||||
Моторные яхты
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<span>></span>
|
|
||||||
<span className="text-[#333333]">{yacht.name}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content Container */}
|
// Используем выбранную дату как дату отправления и прибытия (можно изменить логику при необходимости)
|
||||||
<div className="lg:container lg:max-w-6xl lg:mx-auto lg:px-4 lg:pb-6">
|
const departureDate = format(selectedDate, "yyyy-MM-dd");
|
||||||
<div className="bg-white lg:rounded-[16px] lg:p-6">
|
const arrivalDate = format(selectedDate, "yyyy-MM-dd");
|
||||||
{/* Мобильная версия - без отступов сверху, с отступом для фиксированной панели */}
|
|
||||||
<div className="lg:hidden pt-[50px]">
|
|
||||||
{/* Gallery */}
|
|
||||||
<YachtGallery
|
|
||||||
images={yacht.galleryUrls || []}
|
|
||||||
badge={!yacht.hasQuickRent ? "По запросу" : ""}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Yacht Title */}
|
const params = new URLSearchParams({
|
||||||
<div className="px-4 pt-4">
|
yachtId: yacht.id.toString(),
|
||||||
<h1 className="text-xl font-bold text-[#333333] mb-4">
|
departureDate: departureDate,
|
||||||
{yacht.name}
|
departureTime: startTime,
|
||||||
</h1>
|
arrivalDate: arrivalDate,
|
||||||
</div>
|
arrivalTime: endTime,
|
||||||
|
guests: (guests.adults + guests.children).toString(),
|
||||||
|
});
|
||||||
|
|
||||||
{/* Tabs */}
|
router.push(`/confirm?${params.toString()}`);
|
||||||
<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 */}
|
if (!yacht) {
|
||||||
<div className="px-4 py-6">
|
return <div />;
|
||||||
{activeTab === "availability" && (
|
}
|
||||||
<YachtAvailability
|
|
||||||
price={String(yacht.minCost)}
|
return (
|
||||||
mobile={true}
|
<main className="bg-[#f4f4f4] min-h-screen ">
|
||||||
reservations={yacht.reservations}
|
{/* Мобильная фиксированная верхняя панель навигации */}
|
||||||
/>
|
<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">
|
||||||
{activeTab === "description" && (
|
<button
|
||||||
<div>
|
onClick={() => router.back()}
|
||||||
<p className="text-base text-[#666666] leading-relaxed">
|
className="flex items-center justify-center"
|
||||||
{yacht.description}
|
>
|
||||||
</p>
|
<ArrowLeft size={24} className="text-[#333333]" />
|
||||||
</div>
|
</button>
|
||||||
)}
|
<h2 className="text-base font-medium text-[#333333]">
|
||||||
{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>
|
||||||
</div>
|
<button className="flex items-center justify-center">
|
||||||
<div className="border border-[#DFDFDF] rounded-[12px] flex items-center justify-center py-18">
|
<Heart size={24} className="text-[#333333]" />
|
||||||
<p className="text-lg text-[#999999]">
|
</button>
|
||||||
У этой яхты пока нет отзывов
|
|
||||||
</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>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Десктопная версия - Breadcrumbs */}
|
||||||
<div className="space-y-6">
|
<div className="hidden lg:block container max-w-6xl mx-auto px-4 py-4">
|
||||||
{/* Gallery */}
|
<div className="text-sm text-[#999999] flex items-center gap-4">
|
||||||
<YachtGallery
|
<Link href="/">
|
||||||
images={yacht.galleryUrls || []}
|
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
||||||
badge={!yacht.hasQuickRent ? "По запросу" : ""}
|
Аренда яхты
|
||||||
/>
|
</span>
|
||||||
|
</Link>
|
||||||
|
<span>></span>
|
||||||
|
<Link href="/catalog">
|
||||||
|
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
||||||
|
Моторные яхты
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<span>></span>
|
||||||
|
<span className="text-[#333333]">{yacht.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Content with Booking Widget on the right */}
|
{/* Main Content Container */}
|
||||||
<div className="flex flex-col lg:flex-row gap-6 items-start">
|
<div className="lg:container lg:max-w-6xl lg:mx-auto lg:px-4 lg:pb-6">
|
||||||
{/* Left column - all content below gallery */}
|
<div className="bg-white lg:rounded-[16px] lg:p-6">
|
||||||
<div className="flex-1 space-y-6">
|
{/* Мобильная версия - без отступов сверху, с отступом для фиксированной панели */}
|
||||||
{/* Availability */}
|
<div className="lg:hidden pt-[50px]">
|
||||||
<YachtAvailability
|
{/* Gallery */}
|
||||||
price={String(yacht.minCost)}
|
<YachtGallery
|
||||||
reservations={yacht.reservations}
|
images={yacht.galleryUrls || []}
|
||||||
/>
|
badge={!yacht.hasQuickRent ? "По запросу" : ""}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Characteristics */}
|
{/* Yacht Title */}
|
||||||
<YachtCharacteristics yacht={yacht} />
|
<div className="px-4 pt-4">
|
||||||
|
<h1 className="text-xl font-bold text-[#333333] mb-4">
|
||||||
|
{yacht.name}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Tabs */}
|
||||||
<div>
|
<div className="px-4 border-b border-gray-200 overflow-x-auto">
|
||||||
<h2 className="text-base font-bold text-[#333333] mb-4">
|
<div className="flex gap-6 min-w-max">
|
||||||
Описание
|
<button
|
||||||
</h2>
|
onClick={() => setActiveTab("availability")}
|
||||||
<p className="text-base text-[#666666] leading-relaxed">
|
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
|
||||||
{yacht.description}
|
activeTab === "availability"
|
||||||
</p>
|
? "text-[#008299] border-b-2 border-[#008299]"
|
||||||
</div>
|
: "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>
|
||||||
|
|
||||||
{/* Contact and Requisites */}
|
{/* Tab Content */}
|
||||||
<ContactInfo {...yacht.owner} />
|
<div className="px-4 py-6">
|
||||||
|
{activeTab === "availability" && (
|
||||||
{/* Reviews */}
|
<>
|
||||||
<div>
|
<YachtAvailability
|
||||||
<div className="flex items-center gap-2 mb-4">
|
price={String(yacht.minCost)}
|
||||||
<Icon name="reviewStar" size={16} />
|
mobile={true}
|
||||||
<h2 className="text-base font-bold text-[#333333]">
|
reservations={yacht.reservations}
|
||||||
Отзывы
|
selectedDate={selectedDate}
|
||||||
</h2>
|
startTime={startTime}
|
||||||
|
endTime={endTime}
|
||||||
|
onDateChange={setSelectedDate}
|
||||||
|
onStartTimeChange={setStartTime}
|
||||||
|
onEndTimeChange={setEndTime}
|
||||||
|
/>
|
||||||
|
{/* Выбор гостей для мобильной версии */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
|
</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">
|
||||||
</p>
|
{/* 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>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Gallery */}
|
||||||
|
<YachtGallery
|
||||||
|
images={yacht.galleryUrls || []}
|
||||||
|
badge={!yacht.hasQuickRent ? "По запросу" : ""}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content with Booking Widget on the right */}
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6 items-start">
|
||||||
|
{/* Left column - all content below gallery */}
|
||||||
|
<div className="flex-1 space-y-6">
|
||||||
|
{/* Availability */}
|
||||||
|
<YachtAvailability
|
||||||
|
price={String(yacht.minCost)}
|
||||||
|
reservations={yacht.reservations}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 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 {...yacht.owner} />
|
||||||
|
|
||||||
|
{/* Reviews */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Icon name="reviewStar" size={16} />
|
||||||
|
<h2 className="text-base font-bold text-[#333333]">
|
||||||
|
Отзывы
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="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={String(yacht.minCost)}
|
||||||
|
yacht={yacht}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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={String(yacht.minCost)} yacht={yacht} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Мобильная фиксированная нижняя панель бронирования */}
|
{/* Мобильная фиксированная нижняя панель бронирования */}
|
||||||
<div className="lg:hidden sticky bottom-0 left-0 right-0 z-[10] bg-white border-t border-gray-200 px-4 py-3">
|
<div className="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 className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-lg font-bold text-[#333333]">
|
<span className="text-lg font-bold text-[#333333]">
|
||||||
{yacht.minCost} ₽
|
{yacht.minCost} ₽
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-[#999999] ml-1">/ час</span>
|
<span className="text-sm text-[#999999] ml-1">
|
||||||
</div>
|
/ час
|
||||||
<button
|
</span>
|
||||||
onClick={() => router.push(`/confirm?yachtId=${yacht.id}`)}
|
</div>
|
||||||
className="bg-[#008299] text-white px-6 py-3 rounded-lg font-bold text-base hover:bg-[#006d7a] transition-colors"
|
<button
|
||||||
>
|
onClick={handleBookMobile}
|
||||||
Забронировать
|
disabled={!selectedDate || !startTime || !endTime}
|
||||||
</button>
|
className="bg-[#008299] text-white px-6 py-3 rounded-lg font-bold text-base hover:bg-[#006d7a] transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||||
</div>
|
>
|
||||||
</div>
|
Забронировать
|
||||||
</main>
|
</button>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,80 @@ import {
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Icon from "@/components/ui/icon";
|
import Icon from "@/components/ui/icon";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GuestDatePicker } from "@/components/form/guest-date-picker";
|
import { useRouter } from "next/navigation";
|
||||||
import { formatMinCost, formatWidth, getImageUrl } from "@/lib/utils";
|
import { GuestDatePicker, GuestDatePickerValue } from "@/components/form/guest-date-picker";
|
||||||
|
import { formatMinCost, formatWidth, getImageUrl, calculateTotalPrice, formatPrice } from "@/lib/utils";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
export default function FeaturedYacht({
|
export default function FeaturedYacht({
|
||||||
yacht,
|
yacht,
|
||||||
}: {
|
}: {
|
||||||
yacht: CatalogItemShortDto;
|
yacht: CatalogItemShortDto;
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
|
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
|
||||||
|
const [bookingData, setBookingData] = useState<GuestDatePickerValue>({
|
||||||
|
date: undefined,
|
||||||
|
departureTime: "12:00",
|
||||||
|
arrivalTime: "13:00",
|
||||||
|
adults: 1,
|
||||||
|
children: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const handleThumbnailClick = (imageSrc: string) => {
|
const handleThumbnailClick = (imageSrc: string) => {
|
||||||
setSelectedImage(imageSrc);
|
setSelectedImage(imageSrc);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Расчет итоговой стоимости
|
||||||
|
const getTotalPrice = () => {
|
||||||
|
if (!bookingData.date || !bookingData.departureTime || !bookingData.arrivalTime) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Форматируем дату в ISO строку для calculateTotalPrice
|
||||||
|
const dateString = format(bookingData.date, "yyyy-MM-dd");
|
||||||
|
|
||||||
|
const { totalPrice } = calculateTotalPrice(
|
||||||
|
dateString,
|
||||||
|
bookingData.departureTime,
|
||||||
|
dateString, // Используем ту же дату для arrival
|
||||||
|
bookingData.arrivalTime,
|
||||||
|
yacht.minCost
|
||||||
|
);
|
||||||
|
|
||||||
|
return totalPrice;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик нажатия на кнопку "Забронировать"
|
||||||
|
const handleBookClick = () => {
|
||||||
|
if (!bookingData.date || !yacht.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Форматируем дату в формат yyyy-MM-dd
|
||||||
|
const dateString = format(bookingData.date, "yyyy-MM-dd");
|
||||||
|
|
||||||
|
// Кодируем время для URL (00:00 -> 00%3A00)
|
||||||
|
const encodedDepartureTime = encodeURIComponent(bookingData.departureTime);
|
||||||
|
const encodedArrivalTime = encodeURIComponent(bookingData.arrivalTime);
|
||||||
|
|
||||||
|
// Вычисляем общее количество гостей
|
||||||
|
const totalGuests = bookingData.adults + bookingData.children;
|
||||||
|
|
||||||
|
// Формируем URL с параметрами
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
yachtId: yacht.id.toString(),
|
||||||
|
departureDate: dateString,
|
||||||
|
departureTime: encodedDepartureTime,
|
||||||
|
arrivalDate: dateString, // Используем ту же дату для arrival
|
||||||
|
arrivalTime: encodedArrivalTime,
|
||||||
|
guests: totalGuests.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Переходим на страницу подтверждения
|
||||||
|
router.push(`/confirm?${params.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
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">
|
||||||
|
|
@ -157,13 +217,18 @@ export default function FeaturedYacht({
|
||||||
|
|
||||||
{/* Booking form */}
|
{/* Booking form */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<GuestDatePicker />
|
<GuestDatePicker
|
||||||
|
value={bookingData}
|
||||||
|
onChange={setBookingData}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Book button */}
|
{/* Book button */}
|
||||||
<Button
|
<Button
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
className="font-bold text-white h-[64px] w-full px-8"
|
className="font-bold text-white h-[64px] w-full px-8"
|
||||||
|
onClick={handleBookClick}
|
||||||
|
disabled={!bookingData.date || !yacht.id}
|
||||||
>
|
>
|
||||||
Забронировать
|
Забронировать
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -171,7 +236,7 @@ export default function FeaturedYacht({
|
||||||
{/* Total price */}
|
{/* Total price */}
|
||||||
<div className="flex justify-between items-center text-l mt-6 font-bold text-gray-800">
|
<div className="flex justify-between items-center text-l mt-6 font-bold text-gray-800">
|
||||||
<span className="font-normal">Итого:</span>
|
<span className="font-normal">Итого:</span>
|
||||||
<span>0 ₽</span>
|
<span>{formatPrice(getTotalPrice())} ₽</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -14,14 +14,18 @@ import {
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
|
|
||||||
|
export interface GuestDatePickerValue {
|
||||||
|
date: Date | undefined;
|
||||||
|
departureTime: string;
|
||||||
|
arrivalTime: string;
|
||||||
|
adults: number;
|
||||||
|
children: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface GuestDatePickerProps {
|
interface GuestDatePickerProps {
|
||||||
onApply?: (data: {
|
value?: GuestDatePickerValue;
|
||||||
date: Date | undefined;
|
onChange?: (value: GuestDatePickerValue) => void;
|
||||||
departureTime: string;
|
onApply?: (data: GuestDatePickerValue) => void;
|
||||||
arrivalTime: string;
|
|
||||||
adults: number;
|
|
||||||
children: number;
|
|
||||||
}) => void;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,26 +153,94 @@ const CommonPopoverContent: React.FC<CommonPopoverContentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GuestDatePicker: React.FC<GuestDatePickerProps> = ({
|
export const GuestDatePicker: React.FC<GuestDatePickerProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
onApply,
|
onApply,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const [date, setDate] = useState<Date>();
|
// Используем controlled значения, если они переданы, иначе используем внутреннее состояние
|
||||||
const [departureTime, setDepartureTime] = useState("12:00");
|
const isControlled = value !== undefined;
|
||||||
const [arrivalTime, setArrivalTime] = useState("13:00");
|
|
||||||
const [adults, setAdults] = useState(1);
|
const [internalDate, setInternalDate] = useState<Date>();
|
||||||
const [children, setChildren] = useState(0);
|
const [internalDepartureTime, setInternalDepartureTime] = useState("12:00");
|
||||||
|
const [internalArrivalTime, setInternalArrivalTime] = useState("13:00");
|
||||||
|
const [internalAdults, setInternalAdults] = useState(1);
|
||||||
|
const [internalChildren, setInternalChildren] = useState(0);
|
||||||
|
|
||||||
|
const date = isControlled ? value.date : internalDate;
|
||||||
|
const departureTime = isControlled ? value.departureTime : internalDepartureTime;
|
||||||
|
const arrivalTime = isControlled ? value.arrivalTime : internalArrivalTime;
|
||||||
|
const adults = isControlled ? value.adults : internalAdults;
|
||||||
|
const children = isControlled ? value.children : internalChildren;
|
||||||
|
|
||||||
|
const setDate = (newDate: Date | undefined) => {
|
||||||
|
if (isControlled) {
|
||||||
|
onChange?.({
|
||||||
|
...value,
|
||||||
|
date: newDate,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setInternalDate(newDate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDepartureTime = (newTime: string) => {
|
||||||
|
if (isControlled) {
|
||||||
|
onChange?.({
|
||||||
|
...value,
|
||||||
|
departureTime: newTime,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setInternalDepartureTime(newTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setArrivalTime = (newTime: string) => {
|
||||||
|
if (isControlled) {
|
||||||
|
onChange?.({
|
||||||
|
...value,
|
||||||
|
arrivalTime: newTime,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setInternalArrivalTime(newTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAdults = (newAdults: number) => {
|
||||||
|
if (isControlled) {
|
||||||
|
onChange?.({
|
||||||
|
...value,
|
||||||
|
adults: newAdults,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setInternalAdults(newAdults);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setChildren = (newChildren: number) => {
|
||||||
|
if (isControlled) {
|
||||||
|
onChange?.({
|
||||||
|
...value,
|
||||||
|
children: newChildren,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setInternalChildren(newChildren);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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?.({
|
const currentValue = {
|
||||||
date,
|
date,
|
||||||
departureTime,
|
departureTime,
|
||||||
arrivalTime,
|
arrivalTime,
|
||||||
adults,
|
adults,
|
||||||
children,
|
children,
|
||||||
});
|
};
|
||||||
|
onApply?.(currentValue);
|
||||||
setIsDepartureOpen(false);
|
setIsDepartureOpen(false);
|
||||||
setIsArrivalOpen(false);
|
setIsArrivalOpen(false);
|
||||||
setIsGuestOpen(false);
|
setIsGuestOpen(false);
|
||||||
|
|
|
||||||
|
|
@ -8,202 +8,224 @@ 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;
|
onlyDeparture?: boolean;
|
||||||
onlyArrival?: 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,
|
onlyDeparture,
|
||||||
onlyArrival,
|
onlyArrival,
|
||||||
}: DatePickerProps) {
|
}: DatePickerProps) {
|
||||||
const [internalDate, setInternalDate] = React.useState<Date>();
|
const [internalDate, setInternalDate] = React.useState<Date>();
|
||||||
const [internalDepartureTime, setInternalDepartureTime] =
|
const [internalDepartureTime, setInternalDepartureTime] =
|
||||||
React.useState("12:00");
|
React.useState("12:00");
|
||||||
const [internalArrivalTime, setInternalArrivalTime] = React.useState("13:00");
|
const [internalArrivalTime, setInternalArrivalTime] =
|
||||||
const [open, setOpen] = React.useState(false);
|
React.useState("13:00");
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
// Определяем, является ли компонент контролируемым
|
// Определяем, является ли компонент контролируемым
|
||||||
const isControlled =
|
const isControlled =
|
||||||
value !== undefined ||
|
value !== undefined ||
|
||||||
externalDepartureTime !== undefined ||
|
externalDepartureTime !== undefined ||
|
||||||
externalArrivalTime !== undefined;
|
externalArrivalTime !== undefined;
|
||||||
|
|
||||||
// Используем внешние значения, если они предоставлены, иначе внутренние
|
// Используем внешние значения, если они предоставлены, иначе внутренние
|
||||||
const date = value !== undefined ? value || undefined : internalDate;
|
const date = value !== undefined ? value || undefined : internalDate;
|
||||||
const departureTime =
|
const departureTime =
|
||||||
externalDepartureTime !== undefined
|
externalDepartureTime !== undefined
|
||||||
? externalDepartureTime
|
? externalDepartureTime
|
||||||
: internalDepartureTime;
|
: internalDepartureTime;
|
||||||
const arrivalTime =
|
const arrivalTime =
|
||||||
externalArrivalTime !== undefined
|
externalArrivalTime !== undefined
|
||||||
? externalArrivalTime
|
? externalArrivalTime
|
||||||
: internalArrivalTime;
|
: 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 name="calendar" className="w-4 h-4 text-brand mr-2" />
|
<Icon
|
||||||
)}
|
name="calendar"
|
||||||
{date ? (
|
className="w-4 h-4 text-brand mr-2"
|
||||||
format(date, `d MMMM, ${departureTime} - ${arrivalTime}`, {
|
/>
|
||||||
locale: ru,
|
)}
|
||||||
})
|
{date ? (
|
||||||
) : (
|
(() => {
|
||||||
<span>{placeholder}</span>
|
let timeFormat = "";
|
||||||
)}
|
if (onlyDeparture) {
|
||||||
</div>
|
timeFormat = `d MMMM, ${departureTime}`;
|
||||||
{open ? (
|
} else if (onlyArrival) {
|
||||||
<ChevronUpIcon className="w-4 h-4" />
|
timeFormat = `d MMMM, ${arrivalTime}`;
|
||||||
) : (
|
} else {
|
||||||
<ChevronDownIcon className="w-4 h-4" />
|
timeFormat = `d MMMM, ${departureTime} - ${arrivalTime}`;
|
||||||
)}
|
}
|
||||||
</Button>
|
return format(date, timeFormat, {
|
||||||
</PopoverTrigger>
|
locale: ru,
|
||||||
<PopoverContent className="p-0 bg-white rounded-[20px] shadow-lg">
|
});
|
||||||
<div className="p-4 w-full">
|
})()
|
||||||
{/* Календарь */}
|
) : (
|
||||||
<Calendar
|
<span>{placeholder}</span>
|
||||||
mode="single"
|
)}
|
||||||
selected={date}
|
</div>
|
||||||
onSelect={handleDateChange}
|
{open ? (
|
||||||
className="mb-4 "
|
<ChevronUpIcon className="w-4 h-4" />
|
||||||
locale={ru}
|
) : (
|
||||||
disabled={(date) =>
|
<ChevronDownIcon className="w-4 h-4" />
|
||||||
date < new Date(new Date().setHours(0, 0, 0, 0))
|
)}
|
||||||
}
|
</Button>
|
||||||
classNames={{
|
</PopoverTrigger>
|
||||||
root: "w-full",
|
<PopoverContent className="p-0 bg-white rounded-[20px] shadow-lg">
|
||||||
month: "flex w-full flex-col gap-4",
|
<div className="p-4 w-full">
|
||||||
button_previous:
|
{/* Календарь */}
|
||||||
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
|
<Calendar
|
||||||
button_next:
|
mode="single"
|
||||||
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
|
selected={date}
|
||||||
month_caption:
|
onSelect={handleDateChange}
|
||||||
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
|
className="mb-4 "
|
||||||
table: "w-full border-collapse",
|
locale={ru}
|
||||||
weekdays: "flex",
|
disabled={(date) =>
|
||||||
weekday:
|
date < new Date(new Date().setHours(0, 0, 0, 0))
|
||||||
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
|
}
|
||||||
day_button: "font-bold ring-0 focus:ring-0",
|
classNames={{
|
||||||
week: "mt-2 flex w-full",
|
root: "w-full",
|
||||||
today: "bg-gray-100 text-gray-900 rounded-full",
|
month: "flex w-full flex-col gap-4",
|
||||||
outside: "text-gray-300",
|
button_previous:
|
||||||
disabled: "text-gray-400 cursor-not-allowed",
|
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
|
||||||
selected:
|
button_next:
|
||||||
"rounded-full border-none outline-none !bg-brand text-white",
|
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
|
||||||
}}
|
month_caption:
|
||||||
/>
|
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
|
||||||
|
table: "w-full border-collapse",
|
||||||
|
weekdays: "flex",
|
||||||
|
weekday:
|
||||||
|
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
|
||||||
|
day_button: "font-bold ring-0 focus:ring-0",
|
||||||
|
week: "mt-2 flex w-full",
|
||||||
|
today: "bg-gray-100 text-gray-900 rounded-full",
|
||||||
|
outside: "text-gray-300",
|
||||||
|
disabled: "text-gray-400 cursor-not-allowed",
|
||||||
|
selected:
|
||||||
|
"rounded-full border-none outline-none !bg-brand text-white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Поля времени */}
|
{/* Поля времени */}
|
||||||
<div className="flex gap-3 mb-4">
|
<div className="flex gap-3 mb-4">
|
||||||
{!onlyDeparture ? (
|
{onlyDeparture && (
|
||||||
<div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center">
|
<div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center">
|
||||||
<label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1">
|
<label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1">
|
||||||
Выход
|
Выход
|
||||||
</label>
|
</label>
|
||||||
<div className="relative h-full flex align-center">
|
<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" />
|
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
value={departureTime}
|
value={departureTime}
|
||||||
onChange={(e) => handleDepartureTimeChange(e.target.value)}
|
onChange={(e) =>
|
||||||
className="w-full focus:outline-none focus:border-transparent"
|
handleDepartureTimeChange(
|
||||||
/>
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full focus:outline-none focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Кнопка Применить */}
|
||||||
|
<Button
|
||||||
|
onClick={handleApply}
|
||||||
|
variant="gradient"
|
||||||
|
className="font-bold text-white h-[44px] w-full px-8"
|
||||||
|
>
|
||||||
|
Применить
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PopoverContent>
|
||||||
) : null}
|
</Popover>
|
||||||
|
);
|
||||||
{!onlyArrival ? (
|
|
||||||
<div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center">
|
|
||||||
<label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1">
|
|
||||||
Заход
|
|
||||||
</label>
|
|
||||||
<div className="relative h-full flex align-center">
|
|
||||||
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={arrivalTime}
|
|
||||||
onChange={(e) => handleArrivalTimeChange(e.target.value)}
|
|
||||||
className="w-full focus:outline-none focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Кнопка Применить */}
|
|
||||||
<Button
|
|
||||||
onClick={handleApply}
|
|
||||||
variant="gradient"
|
|
||||||
className="font-bold text-white h-[44px] w-full px-8"
|
|
||||||
>
|
|
||||||
Применить
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const useApiClient = () => {
|
||||||
const authPopup = useAuthPopup();
|
const authPopup = useAuthPopup();
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: "/api",
|
baseURL: "http://89.169.188.2/api",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
import { differenceInHours, parseISO } from "date-fns";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
|
|
@ -28,4 +29,56 @@ export const formatSpeed = (speed: number): string => {
|
||||||
|
|
||||||
export const formatMinCost = (minCost: number): string => {
|
export const formatMinCost = (minCost: number): string => {
|
||||||
return "от " + minCost + " ₽";
|
return "от " + minCost + " ₽";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatPrice = (price: number): string => {
|
||||||
|
return new Intl.NumberFormat("ru-RU").format(price);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TotalPriceResult {
|
||||||
|
totalHours: number;
|
||||||
|
totalPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calculateTotalPrice = (
|
||||||
|
departureDate: string | null,
|
||||||
|
departureTime: string | null,
|
||||||
|
arrivalDate: string | null,
|
||||||
|
arrivalTime: string | null,
|
||||||
|
pricePerHour: number
|
||||||
|
): TotalPriceResult => {
|
||||||
|
if (
|
||||||
|
!departureDate ||
|
||||||
|
!departureTime ||
|
||||||
|
!arrivalDate ||
|
||||||
|
!arrivalTime ||
|
||||||
|
!pricePerHour
|
||||||
|
) {
|
||||||
|
return { totalHours: 0, totalPrice: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Создаем полные даты
|
||||||
|
const departureDateTime = parseISO(`${departureDate}T${departureTime}`);
|
||||||
|
const arrivalDateTime = parseISO(`${arrivalDate}T${arrivalTime}`);
|
||||||
|
|
||||||
|
// Рассчитываем разницу в часах (с округлением до 0.5 часа)
|
||||||
|
let hoursDiff = differenceInHours(arrivalDateTime, departureDateTime);
|
||||||
|
|
||||||
|
// Добавляем разницу в минутах
|
||||||
|
const minutesDiff =
|
||||||
|
(arrivalDateTime.getMinutes() - departureDateTime.getMinutes()) / 60;
|
||||||
|
hoursDiff += minutesDiff;
|
||||||
|
|
||||||
|
// Округляем до ближайших 0.5 часа
|
||||||
|
const roundedHours = Math.ceil(hoursDiff * 2) / 2;
|
||||||
|
|
||||||
|
// Рассчитываем стоимость
|
||||||
|
const total = pricePerHour * roundedHours;
|
||||||
|
|
||||||
|
return { totalHours: roundedHours, totalPrice: total };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error calculating price:", error);
|
||||||
|
return { totalHours: 0, totalPrice: 0 };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue