From ec00216b93160e25ce5f1794190c9ceeb335c448 Mon Sep 17 00:00:00 2001 From: Sergey Bolshakov Date: Fri, 12 Dec 2025 20:48:52 +0300 Subject: [PATCH] =?UTF-8?q?=D0=AF=D1=85=D1=82=D0=B0,=20=D0=B4=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/images/avatar.png | Bin 0 -> 5906 bytes public/images/icons/heart.svg | 11 ++ public/images/icons/pin.svg | 4 + public/images/icons/review-star.svg | 10 + public/images/icons/share.svg | 6 + .../catalog/[id]/components/BookingWidget.tsx | 21 +-- .../catalog/[id]/components/ContactInfo.tsx | 81 ++++---- .../[id]/components/YachtAvailability.tsx | 173 ++++++++++-------- .../[id]/components/YachtCharacteristics.tsx | 13 +- .../catalog/[id]/components/YachtGallery.tsx | 30 +-- src/app/catalog/[id]/page.tsx | 103 ++++++----- src/components/ui/icon.tsx | 14 +- 12 files changed, 267 insertions(+), 199 deletions(-) create mode 100644 public/images/avatar.png create mode 100644 public/images/icons/heart.svg create mode 100644 public/images/icons/pin.svg create mode 100644 public/images/icons/review-star.svg create mode 100644 public/images/icons/share.svg diff --git a/public/images/avatar.png b/public/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..92e4bd29579ae9ebb67ce73a6fdc27226967675f GIT binary patch literal 5906 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 literal 0 HcmV?d00001 diff --git a/public/images/icons/heart.svg b/public/images/icons/heart.svg new file mode 100644 index 0000000..bd086a2 --- /dev/null +++ b/public/images/icons/heart.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/icons/pin.svg b/public/images/icons/pin.svg new file mode 100644 index 0000000..3a10091 --- /dev/null +++ b/public/images/icons/pin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/icons/review-star.svg b/public/images/icons/review-star.svg new file mode 100644 index 0000000..63790e7 --- /dev/null +++ b/public/images/icons/review-star.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/icons/share.svg b/public/images/icons/share.svg new file mode 100644 index 0000000..26b1876 --- /dev/null +++ b/public/images/icons/share.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app/catalog/[id]/components/BookingWidget.tsx b/src/app/catalog/[id]/components/BookingWidget.tsx index 26311c8..a920e38 100644 --- a/src/app/catalog/[id]/components/BookingWidget.tsx +++ b/src/app/catalog/[id]/components/BookingWidget.tsx @@ -4,15 +4,14 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { DatePicker } from "@/components/ui/date-picker"; import { GuestPicker } from "@/components/form/guest-picker"; -import Icon from "@/components/ui/icon"; interface BookingWidgetProps { price: string; } export function BookingWidget({ price }: BookingWidgetProps) { - const [departureDate, setDepartureDate] = useState(); - const [arrivalDate, setArrivalDate] = useState(); + const [departureDate] = useState(); + const [arrivalDate] = useState(); const [guests, setGuests] = useState({ adults: 1, children: 0 }); const [total] = useState(0); @@ -30,10 +29,13 @@ export function BookingWidget({ price }: BookingWidgetProps) { }; return ( -
+

- от {price} р/час + от {price} ₽{" "} + + /час +

@@ -85,15 +87,12 @@ export function BookingWidget({ price }: BookingWidgetProps) {
- - Итого: - - - {total} Р + Итого: + + {total} ₽
); } - diff --git a/src/app/catalog/[id]/components/ContactInfo.tsx b/src/app/catalog/[id]/components/ContactInfo.tsx index 14a5698..6104b67 100644 --- a/src/app/catalog/[id]/components/ContactInfo.tsx +++ b/src/app/catalog/[id]/components/ContactInfo.tsx @@ -14,64 +14,57 @@ interface ContactInfoProps { }; } -export function ContactInfo({ - contactPerson, - requisites, -}: ContactInfoProps) { +export function ContactInfo({ contactPerson, requisites }: ContactInfoProps) { return ( -
-
-
-
+
+
+
+
{contactPerson.name}
-
-

- Контактное лицо +
+

+ {contactPerson.name}

- {contactPerson.name} + Контактное лицо

+
-
-

- Реквизиты -

-
-
- ИП: - - {requisites.ip} - -
-
- ИНН: - - {requisites.inn} - -
-
- - ОГРН/ОГРНИП: - - - {requisites.ogrn} - -
+
+

+ Реквизиты +

+
+
+ ИП + + {requisites.ip} + +
+
+ ИНН + + {requisites.inn} + +
+
+ + ОГРН/ОГРНИП + + + {requisites.ogrn} +
); } - - - diff --git a/src/app/catalog/[id]/components/YachtAvailability.tsx b/src/app/catalog/[id]/components/YachtAvailability.tsx index 8be9f9b..4548260 100644 --- a/src/app/catalog/[id]/components/YachtAvailability.tsx +++ b/src/app/catalog/[id]/components/YachtAvailability.tsx @@ -2,38 +2,24 @@ import { useState } from "react"; import { Calendar } from "@/components/ui/calendar"; -import { format } from "date-fns"; +import { isSameMonth, isBefore, startOfDay, format } from "date-fns"; import { ru } from "date-fns/locale"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import Icon from "@/components/ui/icon"; +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; interface YachtAvailabilityProps { price: string; } export function YachtAvailability({ price }: YachtAvailabilityProps) { - const [currentMonth, setCurrentMonth] = useState(new Date(2025, 3, 1)); // Апрель 2025 - - // Генерируем доступные даты (27, 28, 29 апреля доступны) - const availableDates = [ - new Date(2025, 3, 27), - new Date(2025, 3, 28), - new Date(2025, 3, 29), - ]; + const today = startOfDay(new Date()); + const [currentMonth, setCurrentMonth] = useState( + new Date(today.getFullYear(), today.getMonth(), 1) + ); const unavailableDates = Array.from({ length: 26 }, (_, i) => { return new Date(2025, 3, i + 1); }); - const isDateAvailable = (date: Date) => { - return availableDates.some( - (d) => - d.getDate() === date.getDate() && - d.getMonth() === date.getMonth() && - d.getFullYear() === date.getFullYear() - ); - }; - const isDateUnavailable = (date: Date) => { return unavailableDates.some( (d) => @@ -43,101 +29,135 @@ export function YachtAvailability({ price }: YachtAvailabilityProps) { ); }; - const handlePreviousMonth = () => { + const isDateInPast = (date: Date) => { + return isBefore(startOfDay(date), today); + }; + + const shouldBeCrossedOut = (date: Date) => { + // Перечеркиваем если день занят или находится до текущего дня + return isDateUnavailable(date) || isDateInPast(date); + }; + + const goToPreviousMonth = () => { setCurrentMonth( - new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1) + new Date( + currentMonth.getFullYear(), + currentMonth.getMonth() - 1, + 1 + ) ); }; - const handleNextMonth = () => { + const goToNextMonth = () => { setCurrentMonth( - new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1) + new Date( + currentMonth.getFullYear(), + currentMonth.getMonth() + 1, + 1 + ) ); }; return ( -
+
-

+

Доступность яхты

-
- - По местному времени яхты -
-
-
- -

- {format(currentMonth, "LLLL yyyy", { locale: ru })} -

- +
+
+
+ + + {format(currentMonth, "LLLL yyyy", { locale: ru })} + + +
- { - const isAvailable = isDateAvailable(day.date); - const isUnavailable = isDateUnavailable(day.date); - const isDay30 = day.date.getDate() === 30; + // Показываем только дни текущего месяца + if (!isSameMonth(day.date, currentMonth)) { + return
; + } + + const isCrossedOut = shouldBeCrossedOut(day.date); return ( ); }, @@ -147,4 +167,3 @@ export function YachtAvailability({ price }: YachtAvailabilityProps) {
); } - diff --git a/src/app/catalog/[id]/components/YachtCharacteristics.tsx b/src/app/catalog/[id]/components/YachtCharacteristics.tsx index 90f5458..7c2822f 100644 --- a/src/app/catalog/[id]/components/YachtCharacteristics.tsx +++ b/src/app/catalog/[id]/components/YachtCharacteristics.tsx @@ -33,19 +33,19 @@ export function YachtCharacteristics({ yacht }: YachtCharacteristicsProps) { return (
-

+

Характеристики

-
+
{characteristics.map((char, index) => (
- {char.label}: + {char.label} - + {char.value}
@@ -54,6 +54,3 @@ export function YachtCharacteristics({ yacht }: YachtCharacteristicsProps) {
); } - - - diff --git a/src/app/catalog/[id]/components/YachtGallery.tsx b/src/app/catalog/[id]/components/YachtGallery.tsx index 16ace2e..0014d38 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} -
-
- )} -
- {index + 1}/{images.length} -
))} @@ -78,6 +67,21 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) { + {/* Badge - поверх слайдера, не скроллится */} + {badge && ( +
+
+ + {badge} +
+
+ )} + {/* Photo counter - поверх слайдера, не скроллится */} +
+
+ {current + 1}/{images.length} +
+
{/* Thumbnails */} @@ -86,7 +90,7 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) { -
- {/* Main Content Grid */} -
- {/* Left Column - Gallery and Availability */} -
+ {/* Main Content */} +
{/* Gallery */} - {/* Availability */} - + {/* Content with Booking Widget on the right */} +
+ {/* Left column - all content below gallery */} +
+ {/* Availability */} + - {/* Characteristics */} - + {/* Characteristics */} + - {/* Description */} -
-

- Описание -

-

- {yacht.description} -

-
+ {/* Description */} +
+

+ Описание +

+

+ {yacht.description} +

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

- Отзывы -

-

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

+ {/* Reviews */} +
+
+ +

+ Отзывы +

+
+
+

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

+
+
+
+ + {/* Right column - Booking Widget (sticky) */} +
+ +
- - {/* Right Column - Booking Widget */} -
- -
-
diff --git a/src/components/ui/icon.tsx b/src/components/ui/icon.tsx index 16c05b0..047f4e2 100644 --- a/src/components/ui/icon.tsx +++ b/src/components/ui/icon.tsx @@ -22,6 +22,10 @@ import PriceMinIcon from "../../../public/images/icons/price-min.svg"; import PriceMaxIcon from "../../../public/images/icons/price-max.svg"; import BoatYearMinIcon from "../../../public/images/icons/boat-year-min.svg"; import BoatYearMaxIcon from "../../../public/images/icons/boat-year-max.svg"; +import PinIcon from "../../../public/images/icons/pin.svg"; +import HeartIcon from "../../../public/images/icons/heart.svg"; +import ShareIcon from "../../../public/images/icons/share.svg"; +import ReviewStarIcon from "../../../public/images/icons/review-star.svg"; // Объект с иконками для удобного доступа const icons = { @@ -45,6 +49,10 @@ const icons = { priceMax: PriceMaxIcon, boatYearMin: BoatYearMinIcon, boatYearMax: BoatYearMaxIcon, + pin: PinIcon, + heart: HeartIcon, + share: ShareIcon, + reviewStar: ReviewStarIcon, }; export type IconName = @@ -67,7 +75,11 @@ export type IconName = | "priceMin" | "priceMax" | "boatYearMin" - | "boatYearMax"; + | "boatYearMax" + | "pin" + | "heart" + | "share" + | "reviewStar"; export interface IconProps extends Omit, "name" | "preserveAspectRatio"> {