663 lines
34 KiB
TypeScript
663 lines
34 KiB
TypeScript
"use client";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import Image from "next/image";
|
||
import Link from "next/link";
|
||
import { useMutation } from "@tanstack/react-query";
|
||
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, formatPrice, calculateTotalPrice } from "@/lib/utils";
|
||
import { parseISO } from "date-fns";
|
||
import { CatalogItemLongDto } from "@/api/types";
|
||
|
||
function ConfirmPageContent() {
|
||
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
|
||
|
||
const client = useApiClient();
|
||
const [promocode, setPromocode] = useState("");
|
||
const [isPromocodeApplied, setIsPromocodeApplied] = useState(false);
|
||
const router = useRouter();
|
||
const searchParams = useSearchParams();
|
||
|
||
// Извлекаем параметры из URL
|
||
const yachtId = searchParams.get("yachtId");
|
||
const guestCount = searchParams.get("guests");
|
||
const departureDate = searchParams.get("departureDate");
|
||
const departureTime = searchParams.get("departureTime");
|
||
const arrivalDate = searchParams.get("arrivalDate");
|
||
const arrivalTime = searchParams.get("arrivalTime");
|
||
|
||
useEffect(() => {
|
||
(async () => {
|
||
const response = await client.get<CatalogItemLongDto>(
|
||
`/catalog/${yachtId}/`
|
||
);
|
||
setYacht(response.data);
|
||
})();
|
||
}, [yachtId]);
|
||
|
||
// Расчет стоимости через функцию
|
||
const { totalHours, totalPrice } = calculateTotalPrice(
|
||
departureDate,
|
||
departureTime,
|
||
arrivalDate,
|
||
arrivalTime,
|
||
yacht?.minCost || 0
|
||
);
|
||
|
||
// Обработчик применения промокода
|
||
const handlePromocodeApply = () => {
|
||
if (promocode.trim().toUpperCase() === "DISCOUNT50") {
|
||
setIsPromocodeApplied(true);
|
||
} else {
|
||
setIsPromocodeApplied(false);
|
||
}
|
||
};
|
||
|
||
// Финальная цена с учетом скидки
|
||
const finalPrice = isPromocodeApplied ? totalPrice * 0.5 : totalPrice;
|
||
|
||
// Функция для форматирования даты (краткий формат)
|
||
const formatDate = (dateString: string | null) => {
|
||
if (!dateString) return null;
|
||
try {
|
||
const date = parseISO(dateString);
|
||
const months = [
|
||
"янв",
|
||
"фев",
|
||
"мар",
|
||
"апр",
|
||
"май",
|
||
"июн",
|
||
"июл",
|
||
"авг",
|
||
"сен",
|
||
"окт",
|
||
"ноя",
|
||
"дек",
|
||
];
|
||
const day = date.getDate();
|
||
const month = months[date.getMonth()];
|
||
return `${day} ${month}`;
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// Функция для форматирования даты (полный формат для десктопа)
|
||
const formatDateFull = (dateString: string | null) => {
|
||
if (!dateString) return null;
|
||
try {
|
||
const date = parseISO(dateString);
|
||
const months = [
|
||
"января",
|
||
"февраля",
|
||
"марта",
|
||
"апреля",
|
||
"мая",
|
||
"июня",
|
||
"июля",
|
||
"августа",
|
||
"сентября",
|
||
"октября",
|
||
"ноября",
|
||
"декабря",
|
||
];
|
||
const day = date.getDate();
|
||
const month = months[date.getMonth()];
|
||
return `${day} ${month}`;
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// Функция для форматирования времени
|
||
const formatTime = (timeString: string | null) => {
|
||
if (!timeString) return null;
|
||
// Декодируем URL-encoded строку (например, 00%3A00 -> 00:00)
|
||
const decoded = decodeURIComponent(timeString);
|
||
return decoded.split(":").slice(0, 2).join(":");
|
||
};
|
||
|
||
// Форматируем данные для отображения
|
||
const departureDateFormatted = formatDate(departureDate);
|
||
const departureTimeFormatted = formatTime(departureTime);
|
||
const arrivalDateFormatted = formatDate(arrivalDate);
|
||
const arrivalTimeFormatted = formatTime(arrivalTime);
|
||
|
||
// Полный формат для десктопной версии
|
||
const departureDateFormattedFull = formatDateFull(departureDate);
|
||
const arrivalDateFormattedFull = formatDateFull(arrivalDate);
|
||
|
||
// Формируем строки для отображения
|
||
const departureDisplay =
|
||
departureDateFormatted && departureTimeFormatted
|
||
? `${departureDateFormatted} ${departureTimeFormatted}`
|
||
: "Не выбрано";
|
||
|
||
const arrivalDisplay =
|
||
arrivalDateFormatted && arrivalTimeFormatted
|
||
? `${arrivalDateFormatted} ${arrivalTimeFormatted}`
|
||
: "Не выбрано";
|
||
|
||
const datesDisplay =
|
||
departureDateFormattedFull &&
|
||
departureTimeFormatted &&
|
||
arrivalDateFormattedFull &&
|
||
arrivalTimeFormatted
|
||
? `${departureDateFormattedFull} в ${departureTimeFormatted} — ${arrivalDateFormattedFull} в ${arrivalTimeFormatted}`
|
||
: "Не выбрано";
|
||
|
||
const guestsDisplay = guestCount
|
||
? guestCount === "1"
|
||
? "1 гость"
|
||
: `${guestCount} гостей`
|
||
: "Не выбрано";
|
||
|
||
const { mutate } = useMutation({
|
||
mutationKey: ["create-reservation", yachtId],
|
||
mutationFn: async () => {
|
||
if (
|
||
!departureDate ||
|
||
!departureTime ||
|
||
!yachtId ||
|
||
!arrivalDate ||
|
||
!arrivalTime
|
||
) {
|
||
throw new Error("Ошибка получения данных бронирования");
|
||
}
|
||
|
||
const departureDateTime = new Date(
|
||
`${departureDate}T${departureTime}`
|
||
);
|
||
const arrivalDateTime = new Date(`${arrivalDate}T${arrivalTime}`);
|
||
|
||
const startUtc = Math.floor(departureDateTime.getTime() / 1000);
|
||
const endUtc = Math.floor(arrivalDateTime.getTime() / 1000);
|
||
|
||
const body = {
|
||
startUtc,
|
||
endUtc,
|
||
yachtId: Number(yachtId),
|
||
reservatorId: Number("userId"), // TODO
|
||
};
|
||
|
||
await client.post("/reservations", body);
|
||
|
||
router.push("/profile/reservations");
|
||
},
|
||
});
|
||
|
||
if (!yacht) {
|
||
return <div />;
|
||
}
|
||
|
||
return (
|
||
<main className="bg-[#f4f4f4] grow">
|
||
{/* Мобильная версия */}
|
||
<div className="lg:hidden">
|
||
{/* Верхний блок с навигацией */}
|
||
<div className="bg-white border-b border-[#DFDFDF]">
|
||
<div className="container max-w-6xl mx-auto px-4 py-3">
|
||
<div className="flex items-center justify-between gap-4">
|
||
{/* Кнопка назад */}
|
||
<button
|
||
onClick={() => router.back()}
|
||
className="flex-shrink-0 w-10 h-10 rounded-full border border-[#DFDFDF] flex items-center justify-center hover:bg-[#f4f4f4] transition-colors"
|
||
>
|
||
<ArrowLeft
|
||
size={20}
|
||
className="text-[#333333]"
|
||
/>
|
||
</button>
|
||
|
||
{/* Центральный блок с информацией */}
|
||
<div className="flex-1 min-w-0 text-center">
|
||
<h2 className="text-base font-bold text-[#333333] mb-1">
|
||
Яхта {yacht.name}
|
||
</h2>
|
||
<div className="flex justify-center gap-10 text-xs text-[#666666]">
|
||
<span>
|
||
{departureDateFormatted || "Не выбрано"}
|
||
</span>
|
||
<span>
|
||
Гостей: {guestCount || "Не выбрано"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Кнопка избранного */}
|
||
<button className="flex-shrink-0 w-10 h-10 flex items-center justify-center hover:opacity-70 transition-opacity">
|
||
<Heart
|
||
size={20}
|
||
className="text-[#333333] stroke-2"
|
||
/>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="container max-w-6xl mx-auto">
|
||
<div className="bg-white p-4">
|
||
{/* Заголовок с иконкой */}
|
||
<div className="flex items-center gap-2 mb-6">
|
||
<h1 className="text-xl text-[#333333]">
|
||
Ваше бронирование 🛥️
|
||
</h1>
|
||
</div>
|
||
|
||
{/* Поля Выход и Заход */}
|
||
<div className="grid grid-cols-2 gap-3 mb-3">
|
||
<div className="relative">
|
||
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
|
||
Выход
|
||
</label>
|
||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
||
<div className="text-[#333333]">
|
||
{departureDisplay}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="relative">
|
||
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
|
||
Заход
|
||
</label>
|
||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
|
||
<div className="text-[#333333]">
|
||
{arrivalDisplay}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* По местному времени яхты */}
|
||
<div className="flex items-center gap-2 text-sm text-[#333333] mb-4">
|
||
<Map size={16} className="text-[#333333]" />
|
||
<span>По местному времени яхты</span>
|
||
</div>
|
||
|
||
{/* Гости */}
|
||
<div className="mb-6">
|
||
<div className="relative">
|
||
<label className="absolute -top-2 left-8 px-1 bg-white text-xs text-[#999999]">
|
||
Гостей
|
||
</label>
|
||
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF] flex items-center justify-between">
|
||
<span className="text-[#333333]">
|
||
{guestsDisplay}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Правила отмены */}
|
||
<div className="mb-8">
|
||
<h3 className="text-base font-bold text-[#333333] mb-4">
|
||
Правила отмены
|
||
</h3>
|
||
<p className="text-base text-[#333333]">
|
||
При отмене до 10 мая вы получите частичный
|
||
возврат.{" "}
|
||
<Link
|
||
href="#"
|
||
className="text-sm text-[#2D908D] hover:text-[#007088] font-bold transition-colors"
|
||
>
|
||
Подробнее
|
||
</Link>
|
||
</p>
|
||
</div>
|
||
|
||
{/* Детализация цены */}
|
||
<div className="mb-8">
|
||
<h3 className="text-base font-bold text-[#333333] mb-4">
|
||
Детализация цены
|
||
</h3>
|
||
<div>
|
||
{totalHours > 0 && yacht.minCost ? (
|
||
<>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<span className="text-[#333333]">
|
||
{formatPrice(yacht.minCost)}₽ ×{" "}
|
||
{totalHours}ч
|
||
</span>
|
||
<span className="text-[#333333]">
|
||
{formatPrice(totalPrice)} ₽
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between items-center mb-4 pb-4 border-b border-[#DFDFDF]">
|
||
<span className="text-[#333333]">
|
||
Услуги
|
||
</span>
|
||
<span className="text-[#333333]">
|
||
0 Р
|
||
</span>
|
||
</div>
|
||
{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="font-bold text-[#333333]">
|
||
{formatPrice(finalPrice)} Р
|
||
</span>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="text-[#999999] text-center py-4">
|
||
Укажите даты для расчета стоимости
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Промокод */}
|
||
<div className="mb-4 pb-6 border-b border-[#DFDFDF]">
|
||
<div className="w-full flex gap-2">
|
||
<input
|
||
type="text"
|
||
placeholder="Промокод"
|
||
value={promocode}
|
||
onChange={(e) => {
|
||
setPromocode(e.target.value);
|
||
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} />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Кнопка отправки заявки */}
|
||
<Button
|
||
variant="default"
|
||
className="w-full h-[56px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full transition-colors duration-200"
|
||
disabled={totalHours === 0}
|
||
onClick={() => mutate()}
|
||
>
|
||
Отправить заявку
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Десктопная версия */}
|
||
<div className="hidden lg:block">
|
||
<div className="container max-w-6xl mx-auto px-4 py-6">
|
||
{/* Breadcrumbs - скрыты на мобильных */}
|
||
<div className="hidden lg:flex mb-6 text-sm text-[#999999] items-center gap-[16px]">
|
||
<Link href="/">
|
||
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
||
Аренда яхты
|
||
</span>
|
||
</Link>
|
||
<span>></span>
|
||
<span className="text-[#333333]">
|
||
Ваше бронирование
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex flex-col lg:flex-row gap-6">
|
||
{/* Левая колонка - Информация о яхте и ценах - скрыта на мобильных */}
|
||
<div className="hidden lg:flex w-full lg:w-[336px] flex-shrink-0 flex-col gap-6">
|
||
<div className="bg-white rounded-[16px]">
|
||
<div className="p-4">
|
||
{/* Изображение яхты */}
|
||
<div className="relative mb-5">
|
||
<Image
|
||
src={getImageUrl(
|
||
yacht.mainImageUrl
|
||
)}
|
||
alt="Яхта"
|
||
width={400}
|
||
height={250}
|
||
className="w-full h-48 object-cover rounded-[8px]"
|
||
/>
|
||
{/* Плашка владельца */}
|
||
<div className="absolute top-2 left-2">
|
||
<div className="bg-white backdrop-blur-sm px-4 py-2 rounded-[8px] flex items-center gap-2">
|
||
<User
|
||
size={22}
|
||
className="text-[#999999]"
|
||
/>
|
||
<div className="flex flex-col gap-[4px]">
|
||
<span className="text-[#999999]">
|
||
Владелец
|
||
</span>
|
||
<span className="text-[#333333] font-bold">
|
||
{yacht.owner.firstName}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* Название яхты */}
|
||
<h3 className="text-base text-[#333333] pb-3 border-b border-[#DFDFDF] mb-4">
|
||
Яхта {yacht.name}
|
||
</h3>
|
||
|
||
{/* Детализация цены */}
|
||
<div>
|
||
<h4 className="text-base font-bold text-[#333333] mb-4">
|
||
Детализация цены
|
||
</h4>
|
||
<div>
|
||
{totalHours > 0 && yacht.minCost ? (
|
||
<>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<span className="text-[#333333]">
|
||
{formatPrice(
|
||
yacht.minCost
|
||
)}
|
||
₽ × {totalHours}ч
|
||
</span>
|
||
<span className="text-[#333333]">
|
||
{formatPrice(
|
||
totalPrice
|
||
)}{" "}
|
||
₽
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between items-center border-b border-[#DFDFDF] pb-4 mb-4">
|
||
<span className="text-[#333333]">
|
||
Услуги
|
||
</span>
|
||
<span className="text-[#333333]">
|
||
0 Р
|
||
</span>
|
||
</div>
|
||
{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] font-bold">
|
||
{formatPrice(
|
||
finalPrice
|
||
)}{" "}
|
||
Р
|
||
</span>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="text-[#999999] text-center py-4">
|
||
Укажите даты для расчета
|
||
стоимости
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-[16px]">
|
||
<div className="p-6">
|
||
{/* Промокод */}
|
||
<div className="w-full flex gap-2">
|
||
<input
|
||
type="text"
|
||
placeholder="Промокод"
|
||
value={promocode}
|
||
onChange={(e) => {
|
||
setPromocode(e.target.value);
|
||
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} />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Правая колонка - Подтверждение бронирования */}
|
||
<div className="flex-1">
|
||
<div className="bg-white rounded-[16px] p-8">
|
||
{/* Заголовок */}
|
||
<h1 className="text-2xl text-[#333333] mb-4">
|
||
Проверьте данные
|
||
</h1>
|
||
<h2 className="text-base text-[#333333] font-bold mb-4">
|
||
Ваше бронирование
|
||
</h2>
|
||
|
||
{/* Сведения о бронирования */}
|
||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||
{/* Даты */}
|
||
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
|
||
<div className="text-[#333333] mb-1">
|
||
Даты
|
||
</div>
|
||
<div className="text-base text-[#999999]">
|
||
{datesDisplay}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Гости */}
|
||
<div className="grow-1 border border-[#DFDFDF] rounded-[8px] p-4">
|
||
<div>
|
||
<div className="text-[#333333] mb-1">
|
||
Гости
|
||
</div>
|
||
<div className="text-base text-[#999999]">
|
||
{guestsDisplay}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Дополнительные услуги */}
|
||
<div className="flex items-center h-[88px] border border-[#DFDFDF] rounded-[8px] p-4 mb-6">
|
||
<div className="text-base text-[#333333]">
|
||
Нет дополнительных услуг
|
||
</div>
|
||
</div>
|
||
|
||
{/* Правила отмены */}
|
||
<h3 className="text-base font-bold text-[#333333] mb-4">
|
||
Правила отмены
|
||
</h3>
|
||
|
||
<p className="text-[#333333]">
|
||
При отмене до 10 мая вы получите частичный
|
||
возврат.
|
||
</p>
|
||
<Link
|
||
href="#"
|
||
className="text-[#2D908D] hover:text-[#007088] font-bold transition-colors"
|
||
>
|
||
Подробнее
|
||
</Link>
|
||
|
||
{/* Указание времени и кнопка отправки */}
|
||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 mt-6">
|
||
<div className="flex items-center gap-2 text-sm text-[#666666]">
|
||
<Map size={20} />
|
||
<span className="text-[#333333]">
|
||
По местному времени яхты
|
||
</span>
|
||
</div>
|
||
<Button
|
||
variant="default"
|
||
size="lg"
|
||
className="flex-shrink-0 h-[64px] w-[270px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full p-0 transition-colors duration-200 hover:shadow-lg"
|
||
disabled={totalHours === 0}
|
||
onClick={() => mutate()}
|
||
>
|
||
Отправить заявку
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
export default function ConfirmPage() {
|
||
return (
|
||
<Suspense
|
||
fallback={
|
||
<div className="bg-[#f4f4f4] grow flex items-center justify-center">
|
||
Загрузка...
|
||
</div>
|
||
}
|
||
>
|
||
<ConfirmPageContent />
|
||
</Suspense>
|
||
);
|
||
}
|