travelmarine-frontend/src/app/confirm/page.tsx

663 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>&gt;</span>
<span className="text-[#333333]">
Ваше бронирование
</span>
</div>
<div className="flex flex-col lg:flex-row gap-6">
{/* Левая колонка - Информация о яхте и ценах - скрыта на мобильных */}
<div className="hidden lg:flex w-full lg:w-[336px] flex-shrink-0 flex-col gap-6">
<div className="bg-white rounded-[16px]">
<div className="p-4">
{/* Изображение яхты */}
<div className="relative mb-5">
<Image
src={getImageUrl(
yacht.mainImageUrl
)}
alt="Яхта"
width={400}
height={250}
className="w-full h-48 object-cover rounded-[8px]"
/>
{/* Плашка владельца */}
<div className="absolute top-2 left-2">
<div className="bg-white backdrop-blur-sm px-4 py-2 rounded-[8px] flex items-center gap-2">
<User
size={22}
className="text-[#999999]"
/>
<div className="flex flex-col gap-[4px]">
<span className="text-[#999999]">
Владелец
</span>
<span className="text-[#333333] font-bold">
{yacht.owner.firstName}
</span>
</div>
</div>
</div>
</div>
{/* Название яхты */}
<h3 className="text-base text-[#333333] pb-3 border-b border-[#DFDFDF] mb-4">
Яхта {yacht.name}
</h3>
{/* Детализация цены */}
<div>
<h4 className="text-base font-bold text-[#333333] mb-4">
Детализация цены
</h4>
<div>
{totalHours > 0 && yacht.minCost ? (
<>
<div className="flex justify-between items-center mb-4">
<span className="text-[#333333]">
{formatPrice(
yacht.minCost
)}
× {totalHours}ч
</span>
<span 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>
);
}