Доработка флоу
This commit is contained in:
parent
b249ab597b
commit
63725ff710
|
|
@ -1,11 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DatePicker } from "@/components/ui/date-picker";
|
||||
import { GuestPicker } from "@/components/form/guest-picker";
|
||||
import { format } from "date-fns";
|
||||
import { calculateTotalPrice, formatPrice } from "@/lib/utils";
|
||||
|
||||
interface BookingWidgetProps {
|
||||
price: string;
|
||||
|
|
@ -16,22 +17,43 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
|||
const router = useRouter();
|
||||
const [departureDate, setDepartureDate] = 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 [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) => {
|
||||
setGuests({ adults, children });
|
||||
};
|
||||
|
||||
const handleBook = () => {
|
||||
if (!departureDate || !arrivalDate || !yacht || !yacht.id) return;
|
||||
if (!departureDate || !arrivalDate || !departureTime || !arrivalTime || !yacht || !yacht.id) return;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
yachtId: yacht.id.toString(),
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
departureTime: format(departureDate, "HH:mm"),
|
||||
departureTime: departureTime,
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
arrivalTime: format(arrivalDate, "HH:mm"),
|
||||
arrivalTime: arrivalTime,
|
||||
guests: (guests.adults + guests.children).toString(),
|
||||
});
|
||||
|
||||
|
|
@ -58,6 +80,8 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
|||
showIcon={false}
|
||||
onDateChange={setDepartureDate}
|
||||
value={departureDate}
|
||||
departureTime={departureTime}
|
||||
onDepartureTimeChange={setDepartureTime}
|
||||
onlyDeparture
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -72,6 +96,8 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
|||
showIcon={false}
|
||||
onDateChange={setArrivalDate}
|
||||
value={arrivalDate}
|
||||
arrivalTime={arrivalTime}
|
||||
onArrivalTimeChange={setArrivalTime}
|
||||
onlyArrival
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -95,7 +121,7 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
|||
onClick={handleBook}
|
||||
variant="gradient"
|
||||
className="w-full h-12 font-bold text-white mb-4"
|
||||
disabled={!departureDate || !arrivalDate}
|
||||
disabled={!departureDate || !arrivalDate || !departureTime || !arrivalTime}
|
||||
>
|
||||
Забронировать
|
||||
</Button>
|
||||
|
|
@ -103,7 +129,7 @@ export function BookingWidget({ price, yacht }: BookingWidgetProps) {
|
|||
<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>
|
||||
<span className="text-base font-bold text-[#333333]">{formatPrice(total)} ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,19 +26,53 @@ interface YachtAvailabilityProps {
|
|||
price: string;
|
||||
mobile?: boolean;
|
||||
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({
|
||||
price,
|
||||
mobile = false,
|
||||
reservations = [],
|
||||
selectedDate,
|
||||
startTime: controlledStartTime,
|
||||
endTime: controlledEndTime,
|
||||
onDateChange,
|
||||
onStartTimeChange,
|
||||
onEndTimeChange,
|
||||
}: YachtAvailabilityProps) {
|
||||
const today = startOfDay(new Date());
|
||||
const [currentMonth, setCurrentMonth] = useState(
|
||||
new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
);
|
||||
const [startTime, setStartTime] = useState<string>("");
|
||||
const [endTime, setEndTime] = useState<string>("");
|
||||
|
||||
// Используем контролируемые значения или внутреннее состояние
|
||||
const [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) => {
|
||||
return new Date(2025, 3, i + 1);
|
||||
|
|
@ -214,6 +248,12 @@ export function YachtAvailability({
|
|||
<div style={{ flexShrink: 0 }}>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={(date) => {
|
||||
if (onDateChange) {
|
||||
onDateChange(date);
|
||||
}
|
||||
}}
|
||||
month={currentMonth}
|
||||
onMonthChange={setCurrentMonth}
|
||||
showOutsideDays={false}
|
||||
|
|
@ -248,6 +288,10 @@ export function YachtAvailability({
|
|||
|
||||
const isCrossedOut = shouldBeCrossedOut(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 (
|
||||
<button
|
||||
|
|
@ -255,6 +299,8 @@ export function YachtAvailability({
|
|||
className={`relative w-full flex items-center justify-center text-sm font-medium transition-colors ${
|
||||
isCrossedOut
|
||||
? "text-[#CCCCCC] line-through"
|
||||
: isSelected
|
||||
? "bg-[#008299] text-white rounded-full"
|
||||
: "text-[#333333] hover:bg-gray-100"
|
||||
}`}
|
||||
style={
|
||||
|
|
@ -266,7 +312,7 @@ export function YachtAvailability({
|
|||
disabled={isCrossedOut}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -282,7 +328,7 @@ export function YachtAvailability({
|
|||
<div className="flex-1">
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option value="">--:--</option>
|
||||
|
|
@ -296,7 +342,7 @@ export function YachtAvailability({
|
|||
<div className="flex-1">
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option value="">--:--</option>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import { YachtAvailability } from "./components/YachtAvailability";
|
|||
import { BookingWidget } from "./components/BookingWidget";
|
||||
import { YachtCharacteristics } from "./components/YachtCharacteristics";
|
||||
import { ContactInfo } from "./components/ContactInfo";
|
||||
import { GuestPicker } from "@/components/form/guest-picker";
|
||||
import useApiClient from "@/hooks/useApiClient";
|
||||
import { formatSpeed } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
|
||||
export default function YachtDetailPage() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -21,7 +23,9 @@ export default function YachtDetailPage() {
|
|||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await client.get<CatalogItemLongDto>(`/catalog/${id}/`);
|
||||
const response = await client.get<CatalogItemLongDto>(
|
||||
`/catalog/${id}/`
|
||||
);
|
||||
|
||||
setYacht(response.data);
|
||||
})();
|
||||
|
|
@ -38,6 +42,36 @@ export default function YachtDetailPage() {
|
|||
| "reviews"
|
||||
>("availability");
|
||||
|
||||
// Состояние для мобильного бронирования
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>();
|
||||
const [startTime, setStartTime] = useState<string>("");
|
||||
const [endTime, setEndTime] = useState<string>("");
|
||||
const [guests, setGuests] = useState({ adults: 1, children: 0 });
|
||||
|
||||
const handleGuestsChange = (adults: number, children: number) => {
|
||||
setGuests({ adults, children });
|
||||
};
|
||||
|
||||
const handleBookMobile = () => {
|
||||
if (!selectedDate || !startTime || !endTime || !yacht || !yacht.id)
|
||||
return;
|
||||
|
||||
// Используем выбранную дату как дату отправления и прибытия (можно изменить логику при необходимости)
|
||||
const departureDate = format(selectedDate, "yyyy-MM-dd");
|
||||
const arrivalDate = format(selectedDate, "yyyy-MM-dd");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
yachtId: yacht.id.toString(),
|
||||
departureDate: departureDate,
|
||||
departureTime: startTime,
|
||||
arrivalDate: arrivalDate,
|
||||
arrivalTime: endTime,
|
||||
guests: (guests.adults + guests.children).toString(),
|
||||
});
|
||||
|
||||
router.push(`/confirm?${params.toString()}`);
|
||||
};
|
||||
|
||||
if (!yacht) {
|
||||
return <div />;
|
||||
}
|
||||
|
|
@ -53,7 +87,9 @@ export default function YachtDetailPage() {
|
|||
>
|
||||
<ArrowLeft size={24} className="text-[#333333]" />
|
||||
</button>
|
||||
<h2 className="text-base font-medium text-[#333333]">Яхта</h2>
|
||||
<h2 className="text-base font-medium text-[#333333]">
|
||||
Яхта
|
||||
</h2>
|
||||
<button className="flex items-center justify-center">
|
||||
<Heart size={24} className="text-[#333333]" />
|
||||
</button>
|
||||
|
|
@ -121,7 +157,9 @@ export default function YachtDetailPage() {
|
|||
Описание
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("characteristics")}
|
||||
onClick={() =>
|
||||
setActiveTab("characteristics")
|
||||
}
|
||||
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
|
||||
activeTab === "characteristics"
|
||||
? "text-[#008299] border-b-2 border-[#008299]"
|
||||
|
|
@ -156,11 +194,33 @@ export default function YachtDetailPage() {
|
|||
{/* Tab Content */}
|
||||
<div className="px-4 py-6">
|
||||
{activeTab === "availability" && (
|
||||
<>
|
||||
<YachtAvailability
|
||||
price={String(yacht.minCost)}
|
||||
mobile={true}
|
||||
reservations={yacht.reservations}
|
||||
selectedDate={selectedDate}
|
||||
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>
|
||||
|
|
@ -172,7 +232,9 @@ export default function YachtDetailPage() {
|
|||
{activeTab === "characteristics" && (
|
||||
<YachtCharacteristics yacht={yacht} />
|
||||
)}
|
||||
{activeTab === "contact" && <ContactInfo {...yacht.owner} />}
|
||||
{activeTab === "contact" && (
|
||||
<ContactInfo {...yacht.owner} />
|
||||
)}
|
||||
{activeTab === "reviews" && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
|
|
@ -201,11 +263,15 @@ export default function YachtDetailPage() {
|
|||
<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>
|
||||
<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>
|
||||
<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} />
|
||||
|
|
@ -266,7 +332,10 @@ export default function YachtDetailPage() {
|
|||
|
||||
{/* 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} />
|
||||
<BookingWidget
|
||||
price={String(yacht.minCost)}
|
||||
yacht={yacht}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -281,11 +350,14 @@ export default function YachtDetailPage() {
|
|||
<span className="text-lg font-bold text-[#333333]">
|
||||
{yacht.minCost} ₽
|
||||
</span>
|
||||
<span className="text-sm text-[#999999] ml-1">/ час</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"
|
||||
onClick={handleBookMobile}
|
||||
disabled={!selectedDate || !startTime || !endTime}
|
||||
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"
|
||||
>
|
||||
Забронировать
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -12,20 +12,80 @@ import {
|
|||
import Image from "next/image";
|
||||
import Icon from "@/components/ui/icon";
|
||||
import { useState } from "react";
|
||||
import { GuestDatePicker } from "@/components/form/guest-date-picker";
|
||||
import { formatMinCost, formatWidth, getImageUrl } from "@/lib/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
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({
|
||||
yacht,
|
||||
}: {
|
||||
yacht: CatalogItemShortDto;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
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) => {
|
||||
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 (
|
||||
<div className="mb-10">
|
||||
<Card className="overflow-hidden bg-white text-gray-900">
|
||||
|
|
@ -157,13 +217,18 @@ export default function FeaturedYacht({
|
|||
|
||||
{/* Booking form */}
|
||||
<div className="mb-8">
|
||||
<GuestDatePicker />
|
||||
<GuestDatePicker
|
||||
value={bookingData}
|
||||
onChange={setBookingData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Book button */}
|
||||
<Button
|
||||
variant="gradient"
|
||||
className="font-bold text-white h-[64px] w-full px-8"
|
||||
onClick={handleBookClick}
|
||||
disabled={!bookingData.date || !yacht.id}
|
||||
>
|
||||
Забронировать
|
||||
</Button>
|
||||
|
|
@ -171,7 +236,7 @@ export default function FeaturedYacht({
|
|||
{/* Total price */}
|
||||
<div className="flex justify-between items-center text-l mt-6 font-bold text-gray-800">
|
||||
<span className="font-normal">Итого:</span>
|
||||
<span>0 ₽</span>
|
||||
<span>{formatPrice(getTotalPrice())} ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,16 +7,15 @@ import { User, ArrowUpRight, Map, ArrowLeft, Heart } from "lucide-react";
|
|||
import { useEffect, useState, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import useApiClient from "@/hooks/useApiClient";
|
||||
import { getImageUrl } from "@/lib/utils";
|
||||
import { differenceInHours, parseISO } from "date-fns";
|
||||
import { getImageUrl, formatPrice, calculateTotalPrice } from "@/lib/utils";
|
||||
import { parseISO } from "date-fns";
|
||||
|
||||
function ConfirmPageContent() {
|
||||
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
|
||||
const [totalHours, setTotalHours] = useState<number>(0);
|
||||
const [totalPrice, setTotalPrice] = useState<number>(0);
|
||||
|
||||
const client = useApiClient();
|
||||
const [promocode, setPromocode] = useState("");
|
||||
const [isPromocodeApplied, setIsPromocodeApplied] = useState(false);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
|
|
@ -37,47 +36,26 @@ function ConfirmPageContent() {
|
|||
})();
|
||||
}, [yachtId]);
|
||||
|
||||
// Расчет стоимости при изменении дат
|
||||
useEffect(() => {
|
||||
if (
|
||||
departureDate &&
|
||||
departureTime &&
|
||||
arrivalDate &&
|
||||
arrivalTime &&
|
||||
yacht?.minCost
|
||||
) {
|
||||
try {
|
||||
// Создаем полные даты
|
||||
const departureDateTime = parseISO(`${departureDate}T${departureTime}`);
|
||||
const arrivalDateTime = parseISO(`${arrivalDate}T${arrivalTime}`);
|
||||
// Расчет стоимости через функцию
|
||||
const { totalHours, totalPrice } = calculateTotalPrice(
|
||||
departureDate,
|
||||
departureTime,
|
||||
arrivalDate,
|
||||
arrivalTime,
|
||||
yacht?.minCost || 0
|
||||
);
|
||||
|
||||
// Рассчитываем разницу в часах (с округлением до 0.5 часа)
|
||||
let hoursDiff = differenceInHours(arrivalDateTime, departureDateTime);
|
||||
|
||||
// Добавляем разницу в минутах
|
||||
const minutesDiff =
|
||||
(arrivalDateTime.getMinutes() - departureDateTime.getMinutes()) / 60;
|
||||
hoursDiff += minutesDiff;
|
||||
|
||||
// Округляем до ближайших 0.5 часа
|
||||
const roundedHours = Math.ceil(hoursDiff * 2) / 2;
|
||||
|
||||
// Рассчитываем стоимость
|
||||
const pricePerHour = yacht.minCost;
|
||||
const total = pricePerHour * roundedHours;
|
||||
|
||||
setTotalHours(roundedHours);
|
||||
setTotalPrice(total);
|
||||
} catch (error) {
|
||||
console.error("Error calculating price:", error);
|
||||
setTotalHours(0);
|
||||
setTotalPrice(0);
|
||||
}
|
||||
// Обработчик применения промокода
|
||||
const handlePromocodeApply = () => {
|
||||
if (promocode.trim().toUpperCase() === "DISCOUNT50") {
|
||||
setIsPromocodeApplied(true);
|
||||
} else {
|
||||
setTotalHours(0);
|
||||
setTotalPrice(0);
|
||||
setIsPromocodeApplied(false);
|
||||
}
|
||||
}, [departureDate, departureTime, arrivalDate, arrivalTime, yacht]);
|
||||
};
|
||||
|
||||
// Финальная цена с учетом скидки
|
||||
const finalPrice = isPromocodeApplied ? totalPrice * 0.5 : totalPrice;
|
||||
|
||||
// Функция для форматирования даты (краткий формат)
|
||||
const formatDate = (dateString: string | null) => {
|
||||
|
|
@ -136,7 +114,9 @@ function ConfirmPageContent() {
|
|||
// Функция для форматирования времени
|
||||
const formatTime = (timeString: string | null) => {
|
||||
if (!timeString) return null;
|
||||
return timeString.split(":").slice(0, 2).join(":");
|
||||
// Декодируем URL-encoded строку (например, 00%3A00 -> 00:00)
|
||||
const decoded = decodeURIComponent(timeString);
|
||||
return decoded.split(":").slice(0, 2).join(":");
|
||||
};
|
||||
|
||||
// Форматируем данные для отображения
|
||||
|
|
@ -174,11 +154,6 @@ function ConfirmPageContent() {
|
|||
: `${guestCount} гостей`
|
||||
: "Не выбрано";
|
||||
|
||||
// Форматирование цены с разделителями тысяч
|
||||
const formatPrice = (price: number) => {
|
||||
return new Intl.NumberFormat("ru-RU").format(price);
|
||||
};
|
||||
|
||||
if (!yacht) {
|
||||
return <div />;
|
||||
}
|
||||
|
|
@ -196,7 +171,10 @@ function ConfirmPageContent() {
|
|||
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]" />
|
||||
<ArrowLeft
|
||||
size={20}
|
||||
className="text-[#333333]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Центральный блок с информацией */}
|
||||
|
|
@ -205,14 +183,21 @@ function ConfirmPageContent() {
|
|||
Яхта {yacht.name}
|
||||
</h2>
|
||||
<div className="flex justify-center gap-10 text-xs text-[#666666]">
|
||||
<span>{departureDateFormatted || "Не выбрано"}</span>
|
||||
<span>Гостей: {guestCount || "Не выбрано"}</span>
|
||||
<span>
|
||||
{departureDateFormatted || "Не выбрано"}
|
||||
</span>
|
||||
<span>
|
||||
Гостей: {guestCount || "Не выбрано"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопка избранного */}
|
||||
<button className="flex-shrink-0 w-10 h-10 flex items-center justify-center hover:opacity-70 transition-opacity">
|
||||
<Heart size={20} className="text-[#333333] stroke-2" />
|
||||
<Heart
|
||||
size={20}
|
||||
className="text-[#333333] stroke-2"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -222,7 +207,9 @@ function ConfirmPageContent() {
|
|||
<div className="bg-white p-4">
|
||||
{/* Заголовок с иконкой */}
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<h1 className="text-xl text-[#333333]">Ваше бронирование 🛥️</h1>
|
||||
<h1 className="text-xl text-[#333333]">
|
||||
Ваше бронирование 🛥️
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Поля Выход и Заход */}
|
||||
|
|
@ -232,7 +219,9 @@ function ConfirmPageContent() {
|
|||
Выход
|
||||
</label>
|
||||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
||||
<div className="text-[#333333]">{departureDisplay}</div>
|
||||
<div className="text-[#333333]">
|
||||
{departureDisplay}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
|
|
@ -240,7 +229,9 @@ function ConfirmPageContent() {
|
|||
Заход
|
||||
</label>
|
||||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
||||
<div className="text-[#333333]">{arrivalDisplay}</div>
|
||||
<div className="text-[#333333]">
|
||||
{arrivalDisplay}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -258,7 +249,9 @@ function ConfirmPageContent() {
|
|||
Гостей
|
||||
</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>
|
||||
<span className="text-[#333333]">
|
||||
{guestsDisplay}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -269,7 +262,8 @@ function ConfirmPageContent() {
|
|||
Правила отмены
|
||||
</h3>
|
||||
<p className="text-base text-[#333333]">
|
||||
При отмене до 10 мая вы получите частичный возврат.{" "}
|
||||
При отмене до 10 мая вы получите частичный
|
||||
возврат.{" "}
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-[#2D908D] hover:text-[#007088] font-bold transition-colors"
|
||||
|
|
@ -289,20 +283,37 @@ function ConfirmPageContent() {
|
|||
<>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-[#333333]">
|
||||
{formatPrice(yacht.minCost)}₽ × {totalHours}ч
|
||||
{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>
|
||||
<span className="text-[#333333]">
|
||||
Услуги
|
||||
</span>
|
||||
<span className="text-[#333333]">
|
||||
0 Р
|
||||
</span>
|
||||
</div>
|
||||
{isPromocodeApplied && (
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-[#333333]">
|
||||
Скидка (DISCOUNT50):
|
||||
</span>
|
||||
<span className="text-[#2D908D] font-bold">
|
||||
-{formatPrice(totalPrice * 0.5)} Р
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[#333333]">Итого:</span>
|
||||
<span className="text-[#333333]">
|
||||
Итого:
|
||||
</span>
|
||||
<span className="font-bold text-[#333333]">
|
||||
{formatPrice(totalPrice)} Р
|
||||
{formatPrice(finalPrice)} Р
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -321,11 +332,20 @@ function ConfirmPageContent() {
|
|||
type="text"
|
||||
placeholder="Промокод"
|
||||
value={promocode}
|
||||
onChange={(e) => setPromocode(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setPromocode(e.target.value);
|
||||
setIsPromocodeApplied(false);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handlePromocodeApply();
|
||||
}
|
||||
}}
|
||||
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"
|
||||
onClick={handlePromocodeApply}
|
||||
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} />
|
||||
|
|
@ -338,6 +358,7 @@ function ConfirmPageContent() {
|
|||
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}
|
||||
onClick={() => router.push("/profile/reservations")}
|
||||
>
|
||||
Отправить заявку
|
||||
</Button>
|
||||
|
|
@ -356,7 +377,9 @@ function ConfirmPageContent() {
|
|||
</span>
|
||||
</Link>
|
||||
<span>></span>
|
||||
<span className="text-[#333333]">Ваше бронирование</span>
|
||||
<span className="text-[#333333]">
|
||||
Ваше бронирование
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
|
|
@ -367,7 +390,9 @@ function ConfirmPageContent() {
|
|||
{/* Изображение яхты */}
|
||||
<div className="relative mb-5">
|
||||
<Image
|
||||
src={getImageUrl(yacht.mainImageUrl)}
|
||||
src={getImageUrl(
|
||||
yacht.mainImageUrl
|
||||
)}
|
||||
alt="Яхта"
|
||||
width={400}
|
||||
height={250}
|
||||
|
|
@ -376,9 +401,14 @@ function ConfirmPageContent() {
|
|||
{/* Плашка владельца */}
|
||||
<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]" />
|
||||
<User
|
||||
size={22}
|
||||
className="text-[#999999]"
|
||||
/>
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<span className="text-[#999999]">Владелец</span>
|
||||
<span className="text-[#999999]">
|
||||
Владелец
|
||||
</span>
|
||||
<span className="text-[#333333] font-bold">
|
||||
{yacht.owner.firstName}
|
||||
</span>
|
||||
|
|
@ -401,26 +431,56 @@ function ConfirmPageContent() {
|
|||
<>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-[#333333]">
|
||||
{formatPrice(yacht.minCost)}₽ × {totalHours}ч
|
||||
{formatPrice(
|
||||
yacht.minCost
|
||||
)}
|
||||
₽ × {totalHours}ч
|
||||
</span>
|
||||
<span className="text-[#333333]">
|
||||
{formatPrice(totalPrice)} ₽
|
||||
{formatPrice(
|
||||
totalPrice
|
||||
)}{" "}
|
||||
₽
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center border-b border-[#DFDFDF] pb-4 mb-4">
|
||||
<span className="text-[#333333]">Услуги</span>
|
||||
<span className="text-[#333333]">0 Р</span>
|
||||
<span className="text-[#333333]">
|
||||
Услуги
|
||||
</span>
|
||||
<span className="text-[#333333]">
|
||||
0 Р
|
||||
</span>
|
||||
</div>
|
||||
{isPromocodeApplied && (
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-[#333333]">
|
||||
Скидка (DISCOUNT50):
|
||||
</span>
|
||||
<span className="text-[#2D908D] font-bold">
|
||||
-{formatPrice(
|
||||
totalPrice *
|
||||
0.5
|
||||
)}{" "}
|
||||
Р
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[#333333]">Итого:</span>
|
||||
<span className="text-[#333333]">
|
||||
Итого:
|
||||
</span>
|
||||
<span className="text-[#333333] font-bold">
|
||||
{formatPrice(totalPrice)} Р
|
||||
{formatPrice(
|
||||
finalPrice
|
||||
)}{" "}
|
||||
Р
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-[#999999] text-center py-4">
|
||||
Укажите даты для расчета стоимости
|
||||
Укажите даты для расчета
|
||||
стоимости
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -436,11 +496,20 @@ function ConfirmPageContent() {
|
|||
type="text"
|
||||
placeholder="Промокод"
|
||||
value={promocode}
|
||||
onChange={(e) => setPromocode(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setPromocode(e.target.value);
|
||||
setIsPromocodeApplied(false);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handlePromocodeApply();
|
||||
}
|
||||
}}
|
||||
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"
|
||||
onClick={handlePromocodeApply}
|
||||
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} />
|
||||
|
|
@ -465,7 +534,9 @@ function ConfirmPageContent() {
|
|||
<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-[#333333] mb-1">
|
||||
Даты
|
||||
</div>
|
||||
<div className="text-base text-[#999999]">
|
||||
{datesDisplay}
|
||||
</div>
|
||||
|
|
@ -474,7 +545,9 @@ function ConfirmPageContent() {
|
|||
{/* Гости */}
|
||||
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
|
||||
<div>
|
||||
<div className="text-[#333333] mb-1">Гости</div>
|
||||
<div className="text-[#333333] mb-1">
|
||||
Гости
|
||||
</div>
|
||||
<div className="text-base text-[#999999]">
|
||||
{guestsDisplay}
|
||||
</div>
|
||||
|
|
@ -495,7 +568,8 @@ function ConfirmPageContent() {
|
|||
</h3>
|
||||
|
||||
<p className="text-[#333333]">
|
||||
При отмене до 10 мая вы получите частичный возврат.
|
||||
При отмене до 10 мая вы получите частичный
|
||||
возврат.
|
||||
</p>
|
||||
<Link
|
||||
href="#"
|
||||
|
|
@ -517,6 +591,7 @@ function ConfirmPageContent() {
|
|||
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}
|
||||
onClick={() => router.push("/profile/reservations")}
|
||||
>
|
||||
Отправить заявку
|
||||
</Button>
|
||||
|
|
@ -532,7 +607,13 @@ function ConfirmPageContent() {
|
|||
|
||||
export default function ConfirmPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="bg-[#f4f4f4] grow flex items-center justify-center">Загрузка...</div>}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="bg-[#f4f4f4] grow flex items-center justify-center">
|
||||
Загрузка...
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ConfirmPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,14 +14,18 @@ import {
|
|||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
interface GuestDatePickerProps {
|
||||
onApply?: (data: {
|
||||
export interface GuestDatePickerValue {
|
||||
date: Date | undefined;
|
||||
departureTime: string;
|
||||
arrivalTime: string;
|
||||
adults: number;
|
||||
children: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
interface GuestDatePickerProps {
|
||||
value?: GuestDatePickerValue;
|
||||
onChange?: (value: GuestDatePickerValue) => void;
|
||||
onApply?: (data: GuestDatePickerValue) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
|
@ -149,26 +153,94 @@ const CommonPopoverContent: React.FC<CommonPopoverContentProps> = ({
|
|||
};
|
||||
|
||||
export const GuestDatePicker: React.FC<GuestDatePickerProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onApply,
|
||||
className,
|
||||
}) => {
|
||||
const [date, setDate] = useState<Date>();
|
||||
const [departureTime, setDepartureTime] = useState("12:00");
|
||||
const [arrivalTime, setArrivalTime] = useState("13:00");
|
||||
const [adults, setAdults] = useState(1);
|
||||
const [children, setChildren] = useState(0);
|
||||
// Используем controlled значения, если они переданы, иначе используем внутреннее состояние
|
||||
const isControlled = value !== undefined;
|
||||
|
||||
const [internalDate, setInternalDate] = useState<Date>();
|
||||
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 [isArrivalOpen, setIsArrivalOpen] = useState(false);
|
||||
const [isGuestOpen, setIsGuestOpen] = useState(false);
|
||||
|
||||
const handleApply = () => {
|
||||
onApply?.({
|
||||
const currentValue = {
|
||||
date,
|
||||
departureTime,
|
||||
arrivalTime,
|
||||
adults,
|
||||
children,
|
||||
});
|
||||
};
|
||||
onApply?.(currentValue);
|
||||
setIsDepartureOpen(false);
|
||||
setIsArrivalOpen(false);
|
||||
setIsGuestOpen(false);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ export function DatePicker({
|
|||
const [internalDate, setInternalDate] = React.useState<Date>();
|
||||
const [internalDepartureTime, setInternalDepartureTime] =
|
||||
React.useState("12:00");
|
||||
const [internalArrivalTime, setInternalArrivalTime] = React.useState("13:00");
|
||||
const [internalArrivalTime, setInternalArrivalTime] =
|
||||
React.useState("13:00");
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
// Определяем, является ли компонент контролируемым
|
||||
|
|
@ -105,12 +106,25 @@ export function DatePicker({
|
|||
>
|
||||
<div className="flex items-center">
|
||||
{showIcon && (
|
||||
<Icon name="calendar" className="w-4 h-4 text-brand mr-2" />
|
||||
<Icon
|
||||
name="calendar"
|
||||
className="w-4 h-4 text-brand mr-2"
|
||||
/>
|
||||
)}
|
||||
{date ? (
|
||||
format(date, `d MMMM, ${departureTime} - ${arrivalTime}`, {
|
||||
(() => {
|
||||
let timeFormat = "";
|
||||
if (onlyDeparture) {
|
||||
timeFormat = `d MMMM, ${departureTime}`;
|
||||
} else if (onlyArrival) {
|
||||
timeFormat = `d MMMM, ${arrivalTime}`;
|
||||
} else {
|
||||
timeFormat = `d MMMM, ${departureTime} - ${arrivalTime}`;
|
||||
}
|
||||
return format(date, timeFormat, {
|
||||
locale: ru,
|
||||
})
|
||||
});
|
||||
})()
|
||||
) : (
|
||||
<span>{placeholder}</span>
|
||||
)}
|
||||
|
|
@ -159,7 +173,7 @@ export function DatePicker({
|
|||
|
||||
{/* Поля времени */}
|
||||
<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">
|
||||
<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">
|
||||
Выход
|
||||
|
|
@ -169,14 +183,18 @@ export function DatePicker({
|
|||
<input
|
||||
type="time"
|
||||
value={departureTime}
|
||||
onChange={(e) => handleDepartureTimeChange(e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleDepartureTimeChange(
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="w-full focus:outline-none focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{!onlyArrival ? (
|
||||
{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">
|
||||
Заход
|
||||
|
|
@ -186,12 +204,16 @@ export function DatePicker({
|
|||
<input
|
||||
type="time"
|
||||
value={arrivalTime}
|
||||
onChange={(e) => handleArrivalTimeChange(e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleArrivalTimeChange(
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="w-full focus:outline-none focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Кнопка Применить */}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const useApiClient = () => {
|
|||
const authPopup = useAuthPopup();
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: "/api",
|
||||
baseURL: "http://89.169.188.2/api",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { differenceInHours, parseISO } from "date-fns";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
|
|
@ -29,3 +30,55 @@ export const formatSpeed = (speed: number): string => {
|
|||
export const formatMinCost = (minCost: number): string => {
|
||||
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