From bc31770c5129577845ecbb18fa3bdeb93d67ae3e Mon Sep 17 00:00:00 2001 From: Sergey Bolshakov Date: Sat, 13 Dec 2025 23:47:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BC=D0=BE=D0=B1=D0=B8=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[id]/components/YachtAvailability.tsx | 196 ++++++++- .../catalog/[id]/components/YachtGallery.tsx | 13 +- src/app/catalog/[id]/const.ts | 37 ++ src/app/catalog/[id]/page.tsx | 330 +++++++++++----- src/app/catalog/components/CatalogSidebar.tsx | 11 +- src/app/catalog/page.tsx | 84 +++- src/app/confirm/page.tsx | 373 +++++++++++++----- src/components/layout/Header.tsx | 2 +- 8 files changed, 819 insertions(+), 227 deletions(-) create mode 100644 src/app/catalog/[id]/const.ts diff --git a/src/app/catalog/[id]/components/YachtAvailability.tsx b/src/app/catalog/[id]/components/YachtAvailability.tsx index 4548260..768e297 100644 --- a/src/app/catalog/[id]/components/YachtAvailability.tsx +++ b/src/app/catalog/[id]/components/YachtAvailability.tsx @@ -2,19 +2,33 @@ import { useState } from "react"; import { Calendar } from "@/components/ui/calendar"; -import { isSameMonth, isBefore, startOfDay, format } from "date-fns"; +import { + isSameMonth, + isBefore, + startOfDay, + format, + eachDayOfInterval, + startOfMonth, + endOfMonth, +} from "date-fns"; import { ru } from "date-fns/locale"; -import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import { ChevronLeftIcon, ChevronRightIcon, Clock } from "lucide-react"; interface YachtAvailabilityProps { price: string; + mobile?: boolean; } -export function YachtAvailability({ price }: YachtAvailabilityProps) { +export function YachtAvailability({ + price, + mobile = false, +}: YachtAvailabilityProps) { const today = startOfDay(new Date()); const [currentMonth, setCurrentMonth] = useState( new Date(today.getFullYear(), today.getMonth(), 1) ); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); const unavailableDates = Array.from({ length: 26 }, (_, i) => { return new Date(2025, 3, i + 1); @@ -38,26 +52,182 @@ export function YachtAvailability({ price }: YachtAvailabilityProps) { return isDateUnavailable(date) || isDateInPast(date); }; + const isDateAvailable = (date: Date) => { + return !shouldBeCrossedOut(date) && isSameMonth(date, currentMonth); + }; + + const getAvailableDaysCount = () => { + const monthStart = startOfMonth(currentMonth); + const monthEnd = endOfMonth(currentMonth); + const daysInMonth = eachDayOfInterval({ + start: monthStart, + end: monthEnd, + }); + return daysInMonth.filter((day) => isDateAvailable(day)).length; + }; + const goToPreviousMonth = () => { setCurrentMonth( - new Date( - currentMonth.getFullYear(), - currentMonth.getMonth() - 1, - 1 - ) + new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1) ); }; const goToNextMonth = () => { setCurrentMonth( - new Date( - currentMonth.getFullYear(), - currentMonth.getMonth() + 1, - 1 - ) + new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1) ); }; + // Генерация времени для селекта + const timeOptions = Array.from({ length: 24 * 2 }, (_, i) => { + const hours = Math.floor(i / 2); + const minutes = (i % 2) * 30; + const timeString = `${String(hours).padStart(2, "0")}:${String( + minutes + ).padStart(2, "0")}`; + return { value: timeString, label: timeString }; + }); + + if (mobile) { + return ( +
+ {/* Навигация по месяцам */} +
+ +
+ + {format(currentMonth, "LLLL", { locale: ru })} + + + Свободных дней: {getAvailableDaysCount()} + +
+ +
+ + {/* Календарь */} +
+ { + const weekdays = [ + "ВС", + "ПН", + "ВТ", + "СР", + "ЧТ", + "ПТ", + "СБ", + ]; + return weekdays[date.getDay()]; + }, + }} + classNames={{ + root: "w-full", + month: "flex w-full flex-col gap-2", + nav: "hidden", + month_caption: "hidden", + caption_label: "hidden", + button_previous: "hidden", + button_next: "hidden", + table: "w-full border-collapse table-fixed", + weekdays: "flex w-full mb-2", + weekday: + "flex-1 text-[#999999] text-xs font-normal p-2 text-center", + week: "flex w-full min-h-[50px]", + day: "relative flex-1 min-w-0 flex-shrink-0", + }} + components={{ + DayButton: ({ day, ...props }) => { + if (!isSameMonth(day.date, currentMonth)) { + return
; + } + + const isCrossedOut = shouldBeCrossedOut( + day.date + ); + + return ( + + ); + }, + }} + /> +
+ + {/* Выбор времени */} +
+
+
+ +
+
+ +
+
+
+ + По местному времени яхты +
+
+
+ ); + } + return (
diff --git a/src/app/catalog/[id]/components/YachtGallery.tsx b/src/app/catalog/[id]/components/YachtGallery.tsx index 0014d38..60f5b10 100644 --- a/src/app/catalog/[id]/components/YachtGallery.tsx +++ b/src/app/catalog/[id]/components/YachtGallery.tsx @@ -52,7 +52,7 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) { {images.map((img, index) => ( -
+
{`Yacht ))} - - + + {/* Badge - поверх слайдера, не скроллится */} {badge && ( -
+
{badge} @@ -84,8 +84,8 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) {
- {/* Thumbnails */} -
+ {/* Thumbnails - скрыты на мобильных */} +
{images.map((img, index) => ( +

+ Яхта +

+ +
+
+ + {/* Десктопная версия - Breadcrumbs */} +
+
Аренда яхты @@ -69,69 +59,111 @@ export default function YachtDetailPage() { > - {yacht.name} + {YACHT.name}
+
- {/* Main Content Container */} -
- {/* Yacht Title and Actions */} -
-

- {yacht.name} -

-
-
- - - {yacht.location} - -
- - -
-
- - {/* Main Content */} -
+ {/* Main Content Container */} +
+
+ {/* Мобильная версия - без отступов сверху, с отступом для фиксированной панели */} +
{/* Gallery */} - {/* Content with Booking Widget on the right */} -
- {/* Left column - all content below gallery */} -
- {/* Availability */} - + {/* Yacht Title */} +
+

+ {YACHT.name} +

+
- {/* Characteristics */} - + {/* Tabs */} +
+
+ + + + + +
+
- {/* Description */} + {/* Tab Content */} +
+ {activeTab === "availability" && ( + + )} + {activeTab === "description" && (
-

- Описание -

- {yacht.description} + {YACHT.description}

- - {/* Contact and Requisites */} + )} + {activeTab === "characteristics" && ( + + )} + {activeTab === "contact" && ( - - {/* Reviews */} + )} + {activeTab === "reviews" && (
@@ -145,16 +177,116 @@ export default function YachtDetailPage() {

-
+ )} +
+
- {/* Right column - Booking Widget (sticky) */} -
- + {/* Десктопная версия */} +
+ {/* Yacht Title and Actions */} +
+

+ {YACHT.name} +

+
+
+ + + {YACHT.location} + +
+ + +
+
+ + {/* Main Content */} +
+ {/* Gallery */} + + + {/* Content with Booking Widget on the right */} +
+ {/* Left column - all content below gallery */} +
+ {/* Availability */} + + + {/* Characteristics */} + + + {/* Description */} +
+

+ Описание +

+

+ {YACHT.description} +

+
+ + {/* Contact and Requisites */} + + + {/* Reviews */} +
+
+ +

+ Отзывы +

+
+
+

+ У этой яхты пока нет отзывов +

+
+
+
+ + {/* Right column - Booking Widget (sticky) */} +
+ +
+ + {/* Мобильная фиксированная нижняя панель бронирования */} +
+
+
+ + {YACHT.price} ₽ + + + / час + +
+ +
+
); } diff --git a/src/app/catalog/components/CatalogSidebar.tsx b/src/app/catalog/components/CatalogSidebar.tsx index ddd1240..1486cbe 100644 --- a/src/app/catalog/components/CatalogSidebar.tsx +++ b/src/app/catalog/components/CatalogSidebar.tsx @@ -10,7 +10,11 @@ import { DatePicker } from "@/components/ui/date-picker"; import { GuestPicker } from "@/components/form/guest-picker"; import Icon from "@/components/ui/icon"; -export default function CatalogSidebar() { +interface CatalogSidebarProps { + onApply?: () => void; +} + +export default function CatalogSidebar({ onApply }: CatalogSidebarProps) { const [lengthRange, setLengthRange] = useState([7, 50]); const [priceRange, setPriceRange] = useState([3000, 200000]); const [yearRange, setYearRange] = useState([1991, 2025]); @@ -254,7 +258,10 @@ export default function CatalogSidebar() {
{/* Кнопка Применить */} -
diff --git a/src/app/catalog/page.tsx b/src/app/catalog/page.tsx index 9b386ff..04d57b5 100644 --- a/src/app/catalog/page.tsx +++ b/src/app/catalog/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from "react"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import Image from "next/image"; import Icon from "@/components/ui/icon"; @@ -12,6 +13,8 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Sliders, X, ArrowLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; const yachts = [ { @@ -113,11 +116,13 @@ const yachts = [ ]; export default function CatalogPage() { + const [isFiltersOpen, setIsFiltersOpen] = useState(false); + return (
- {/* Breadcrumbs */} -
+ {/* Breadcrumbs - скрыты на мобильных */} +
Аренда яхты @@ -127,21 +132,86 @@ export default function CatalogPage() { Каталог яхт
+ {/* Мобильная навигационная панель */} +
+
+ {/* Кнопка "Назад" */} + + + + + {/* Текстовый блок */} +
+
+ Балаклава +
+
+ Когда? · С кем? +
+
+ + {/* Кнопка "Фильтры" */} + +
+
+
- {/* Sidebar */} -
+ {/* Sidebar - скрыт на мобильных, виден на десктопе */} +
+ {/* Мобильное модальное окно фильтров */} + {isFiltersOpen && ( +
+ {/* Overlay */} +
setIsFiltersOpen(false)} + /> + {/* Модальное окно */} +
+
+

+ Фильтры +

+ +
+
+ setIsFiltersOpen(false)} + /> +
+
+
+ )} + {/* Main Content */}
{/* Header */} -
-
+
+

Аренда яхт

-

+

Доступно яхт:{" "} {yachts.length} diff --git a/src/app/confirm/page.tsx b/src/app/confirm/page.tsx index c21bcc8..cf49109 100644 --- a/src/app/confirm/page.tsx +++ b/src/app/confirm/page.tsx @@ -3,126 +3,303 @@ import { Button } from "@/components/ui/button"; import Image from "next/image"; import Link from "next/link"; -import { User, ArrowUpRight, Map } from "lucide-react"; +import { User, ArrowUpRight, Map, ArrowLeft, Heart } from "lucide-react"; import { useState } from "react"; +import { useRouter } from "next/navigation"; export default function ConfirmPage() { const [promocode, setPromocode] = useState(""); + const router = useRouter(); return (

-
- {/* Breadcrumbs */} -
- - - Аренда яхты - - - > - Ваше бронирование + {/* Мобильная версия */} +
+ {/* Верхний блок с навигацией */} +
+
+
+ {/* Кнопка назад */} + + + {/* Центральный блок с информацией */} +
+

+ Яхта Сеньорита +

+
+ 09 авг. + Гостей: 1 +
+
+ + {/* Кнопка избранного */} + +
+
-
- {/* Левая колонка - Информация о яхте и ценах */} -
-
-
- {/* Изображение яхты */} -
- Яхта - {/* Плашка владельца */} -
-
- -
- - Владелец +
+
+ {/* Заголовок с иконкой */} +
+

+ Ваше бронирование 🛥️ +

+
+ + {/* Поля Выход и Заход */} +
+
+ +
+
+ 9 Авг 00:00 +
+
+
+
+ +
+
+ 9 Авг 02:00 +
+
+
+
+ + {/* По местному времени яхты */} +
+ + По местному времени яхты +
+ + {/* Гости */} +
+
+ +
+ + 1 гость + +
+
+
+ + {/* Правила отмены */} +
+

+ Правила отмены +

+

+ При отмене до 10 мая вы получите частичный + возврат.{" "} + + Подробнее + +

+
+ + {/* Детализация цены */} +
+

+ Детализация цены +

+
+
+ + 26 400₽ x 2ч + + + 52 800 ₽ + +
+
+ + Услуги + + 0 Р +
+
+ + Итого: + + + 52 800 Р + +
+
+
+ + {/* Промокод */} +
+
+ + setPromocode(e.target.value) + } + className="flex-1 min-w-0 px-4 py-3 h-[64px] border border-[#DFDFDF] rounded-full text-sm text-[#757575] focus:outline-none focus:ring-2 focus:ring-[#008299] focus:border-transparent" + /> + +
+
+ + {/* Кнопка отправки заявки */} + +
+
+
+ + {/* Десктопная версия */} +
+
+ {/* Breadcrumbs - скрыты на мобильных */} +
+ + + Аренда яхты + + + > + + Ваше бронирование + +
+ +
+ {/* Левая колонка - Информация о яхте и ценах - скрыта на мобильных */} +
+
+
+ {/* Изображение яхты */} +
+ Яхта + {/* Плашка владельца */} +
+
+ +
+ + Владелец + + + Денис + +
+
+
+
+ {/* Название яхты */} +

+ Яхта +

+ + {/* Детализация цены */} +
+

+ Детализация цены +

+
+
+ + 26 400₽ x 2ч + + + 52 800 ₽ + +
+
+ + Услуги + + + 0 Р + +
+
+ + Итого: - Денис + 52 800 Р
- {/* Название яхты */} -

- Яхта -

+
- {/* Детализация цены */} -
-

- Детализация цены -

-
-
- - 26 400₽ x 2ч - - - 52 800 ₽ - -
-
- - Услуги - - - 0 Р - -
-
- - Итого: - - - 52 800 Р - -
+
+
+ {/* Промокод */} +
+ + setPromocode(e.target.value) + } + className="flex-1 min-w-0 px-4 sm:px-8 py-5 h-[64px] border border-[#DFDFDF] rounded-full text-base text-[#757575] focus:outline-none focus:ring-2 focus:ring-[#008299] focus:border-transparent" + /> +
-
-
- {/* Промокод */} -
- - setPromocode(e.target.value) - } - className="flex-1 min-w-0 px-4 sm:px-8 py-5 h-[64px] border border-[#DFDFDF] rounded-full text-base text-[#757575] focus:outline-none focus:ring-2 focus:ring-[#008299] focus:border-transparent" - /> - -
-
-
-
- - {/* Правая колонка - Подтверждение бронирования */} -
-
-
+ {/* Правая колонка - Подтверждение бронирования */} +
+
{/* Заголовок */}

Проверьте данные diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index b856dca..8829eae 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -11,7 +11,7 @@ export default function Header() { const [isAuthDialogOpen, setIsAuthDialogOpen] = useState(false); return ( -
+
{/* Логотип */}