Вёрстка страницы 'Яхта'

This commit is contained in:
Sergey Bolshakov 2025-12-12 02:19:42 +03:00
parent 81d293434c
commit d5e466b879
7 changed files with 642 additions and 4 deletions

View File

@ -0,0 +1,99 @@
"use client";
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<Date | undefined>();
const [arrivalDate, setArrivalDate] = useState<Date | undefined>();
const [guests, setGuests] = useState({ adults: 1, children: 0 });
const [total] = useState(0);
const handleGuestsChange = (adults: number, children: number) => {
setGuests({ adults, children });
};
const handleBook = () => {
// Логика бронирования
console.log("Booking:", {
departureDate,
arrivalDate,
guests,
});
};
return (
<div className="bg-white border border-gray-200 rounded-lg p-6 sticky top-24">
<div className="mb-6">
<p className="text-2xl font-bold text-[#333333] mb-2">
от {price} р/час
</p>
</div>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium text-[#333333] mb-2">
Выход
</label>
<DatePicker
variant="small"
placeholder="Выберите дату и время"
showIcon={false}
/>
</div>
<div>
<label className="block text-sm font-medium text-[#333333] mb-2">
Заход
</label>
<DatePicker
variant="small"
placeholder="Выберите дату и время"
showIcon={false}
/>
</div>
<div>
<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>
</div>
<Button
onClick={handleBook}
variant="gradient"
className="w-full h-12 font-bold text-white mb-4"
>
Забронировать
</Button>
<div className="pt-4 border-t border-gray-200">
<div className="flex justify-between items-center">
<span className="text-base font-semibold text-[#333333]">
Итого:
</span>
<span className="text-xl font-bold text-[#333333]">
{total} Р
</span>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,77 @@
"use client";
import Image from "next/image";
interface ContactInfoProps {
contactPerson: {
name: string;
avatar: string;
};
requisites: {
ip: string;
inn: string;
ogrn: string;
};
}
export function ContactInfo({
contactPerson,
requisites,
}: ContactInfoProps) {
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row gap-6">
<div className="flex items-start gap-4">
<div className="relative w-16 h-16 rounded-full overflow-hidden bg-gray-200 flex items-center justify-center">
<Image
src={contactPerson.avatar}
alt={contactPerson.name}
width={64}
height={64}
className="object-cover"
/>
</div>
<div>
<h3 className="text-base font-semibold text-[#333333] mb-1">
Контактное лицо
</h3>
<p className="text-base text-[#333333]">
{contactPerson.name}
</p>
</div>
</div>
<div className="flex-1">
<h3 className="text-base font-semibold text-[#333333] mb-3">
Реквизиты
</h3>
<div className="space-y-2">
<div className="flex flex-col sm:flex-row sm:justify-between gap-1">
<span className="text-sm text-[#999999]">ИП:</span>
<span className="text-sm text-[#333333]">
{requisites.ip}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:justify-between gap-1">
<span className="text-sm text-[#999999]">ИНН:</span>
<span className="text-sm text-[#333333]">
{requisites.inn}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:justify-between gap-1">
<span className="text-sm text-[#999999]">
ОГРН/ОГРНИП:
</span>
<span className="text-sm text-[#333333]">
{requisites.ogrn}
</span>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,150 @@
"use client";
import { useState } from "react";
import { Calendar } from "@/components/ui/calendar";
import { format } from "date-fns";
import { ru } from "date-fns/locale";
import { ChevronLeft, ChevronRight } from "lucide-react";
import Icon from "@/components/ui/icon";
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 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) =>
d.getDate() === date.getDate() &&
d.getMonth() === date.getMonth() &&
d.getFullYear() === date.getFullYear()
);
};
const handlePreviousMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)
);
};
const handleNextMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)
);
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-[#333333]">
Доступность яхты
</h2>
<div className="flex items-center gap-2 text-sm text-[#999999]">
<Icon name="calendar" size={16} />
<span>По местному времени яхты</span>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<button
onClick={handlePreviousMonth}
className="p-2 hover:bg-gray-100 rounded-md transition-colors"
>
<ChevronLeft className="w-5 h-5 text-gray-600" />
</button>
<h3 className="text-lg font-semibold text-[#333333] capitalize">
{format(currentMonth, "LLLL yyyy", { locale: ru })}
</h3>
<button
onClick={handleNextMonth}
className="p-2 hover:bg-gray-100 rounded-md transition-colors"
>
<ChevronRight className="w-5 h-5 text-gray-600" />
</button>
</div>
<Calendar
mode="single"
month={currentMonth}
onMonthChange={setCurrentMonth}
className="w-full"
locale={ru}
classNames={{
root: "w-full",
month: "flex w-full flex-col gap-4",
month_caption:
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
table: "w-full border-collapse",
weekdays: "flex",
weekday:
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
week: "mt-2 flex w-full",
day: "relative",
}}
components={{
DayButton: ({ day, ...props }) => {
const isAvailable = isDateAvailable(day.date);
const isUnavailable = isDateUnavailable(day.date);
const isDay30 = day.date.getDate() === 30;
return (
<button
{...props}
className={`relative w-full h-16 flex flex-col items-center justify-center rounded-md transition-colors ${
isAvailable
? "bg-[#008299] text-white hover:bg-[#008299]"
: isUnavailable || isDay30
? "text-gray-400 cursor-not-allowed bg-gray-100"
: "hover:bg-gray-100"
}`}
disabled={isUnavailable || isDay30}
>
<span className="text-sm font-medium">
{day.date.getDate()}
</span>
{isAvailable && (
<>
<span className="text-[10px] mt-1 text-center leading-tight">
Доступно: 08:00-20:00
</span>
<span className="text-[10px] mt-0.5 text-center leading-tight">
{price} р/час
</span>
</>
)}
{(isUnavailable || isDay30) && (
<span className="text-lg mt-1"></span>
)}
</button>
);
},
}}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,59 @@
"use client";
interface YachtCharacteristicsProps {
yacht: {
year: number;
maxCapacity: number;
comfortableCapacity: number;
length: number;
width: number;
cabins: number;
material: string;
power: number;
};
}
export function YachtCharacteristics({ yacht }: YachtCharacteristicsProps) {
const characteristics = [
{ label: "Год", value: yacht.year },
{
label: "Максимальная вместимость",
value: `${yacht.maxCapacity} человек`,
},
{
label: "Комфортная вместимость",
value: `${yacht.comfortableCapacity} человек`,
},
{ label: "Длина", value: `${yacht.length} м` },
{ label: "Ширина", value: `${yacht.width} м` },
{ label: "Каюты", value: yacht.cabins },
{ label: "Материал", value: yacht.material },
{ label: "Мощность", value: `${yacht.power} л/с` },
];
return (
<div>
<h2 className="text-xl font-bold text-[#333333] mb-4">
Характеристики
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{characteristics.map((char, index) => (
<div
key={index}
className="flex justify-between items-center py-2 border-b border-gray-200"
>
<span className="text-base text-[#999999]">
{char.label}:
</span>
<span className="text-base font-medium text-[#333333]">
{char.value}
</span>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,107 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import Icon from "@/components/ui/icon";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi,
} from "@/components/ui/carousel";
interface YachtGalleryProps {
images: string[];
badge?: string;
}
export function YachtGallery({ images, badge }: YachtGalleryProps) {
const [api, setApi] = useState<CarouselApi>();
const [current, setCurrent] = useState(0);
useEffect(() => {
if (!api) {
return;
}
setCurrent(api.selectedScrollSnap());
api.on("select", () => {
setCurrent(api.selectedScrollSnap());
});
}, [api]);
const scrollTo = (index: number) => {
api?.scrollTo(index);
};
return (
<div className="space-y-4">
{/* Main Image Carousel */}
<div className="relative">
<Carousel
setApi={setApi}
opts={{
align: "start",
loop: false,
}}
className="w-full"
>
<CarouselContent>
{images.map((img, index) => (
<CarouselItem key={index}>
<div className="relative w-full h-[500px] rounded-lg overflow-hidden">
<Image
src={img}
alt={`Yacht image ${index + 1}`}
fill
className="object-cover"
priority={index === 0}
/>
{badge && (
<div className="absolute top-4 left-4 z-10">
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm gap-1">
<Icon size={16} name="restart" />
<span>{badge}</span>
</div>
</div>
)}
<div className="absolute bottom-4 right-4 bg-black/40 text-white px-3 py-1 rounded-lg text-sm">
{index + 1}/{images.length}
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="left-4 bg-white/80 hover:bg-white border-gray-300" />
<CarouselNext className="right-4 bg-white/80 hover:bg-white border-gray-300" />
</Carousel>
</div>
{/* Thumbnails */}
<div className="flex gap-2 overflow-x-auto pb-2">
{images.map((img, index) => (
<button
key={index}
onClick={() => scrollTo(index)}
className={`relative flex-shrink-0 w-24 h-24 rounded-lg overflow-hidden border-2 transition-all ${
current === index
? "border-[#008299] opacity-100"
: "border-transparent opacity-60 hover:opacity-100"
}`}
>
<Image
src={img}
alt={`Thumbnail ${index + 1}`}
fill
className="object-cover"
/>
</button>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,147 @@
"use client";
import { useParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import Icon from "@/components/ui/icon";
import { YachtGallery } from "./components/YachtGallery";
import { YachtAvailability } from "./components/YachtAvailability";
import { BookingWidget } from "./components/BookingWidget";
import { YachtCharacteristics } from "./components/YachtCharacteristics";
import { ContactInfo } from "./components/ContactInfo";
export default function YachtDetailPage() {
const params = useParams();
const id = params.id as string;
// Данные яхты (в реальном приложении будут загружаться из API)
const yacht = {
id: id,
name: "Яхта Название",
location: "7 Футов",
price: "18 000",
images: [
"/images/yachts/yacht1.jpg",
"/images/yachts/yacht2.jpg",
"/images/yachts/yacht3.jpg",
"/images/yachts/yacht4.jpg",
"/images/yachts/yacht5.jpg",
"/images/yachts/yacht6.jpg",
],
badge: "По запросу",
year: 2000,
maxCapacity: 11,
comfortableCapacity: 11,
length: 13,
width: 4,
cabins: 2,
material: "Стеклопластик",
power: 740,
description: "Яхта",
contactPerson: {
name: "Денис",
avatar: "/images/logo.svg",
},
requisites: {
ip: "Иванов Иван Иванович",
inn: "23000000000",
ogrn: "310000000000001",
},
};
return (
<main className="bg-[#f4f4f4] min-h-screen">
<div className="container max-w-6xl mx-auto px-4 py-6">
{/* Breadcrumbs */}
<div className="mb-6 text-sm text-[#999999] flex items-center gap-4">
<Link href="/">
<span className="cursor-pointer hover:text-[#333333] transition-colors">
Аренда яхты
</span>
</Link>
<span>&gt;</span>
<Link href="/catalog">
<span className="cursor-pointer hover:text-[#333333] transition-colors">
Моторные яхты
</span>
</Link>
<span>&gt;</span>
<span className="text-[#333333]">{yacht.name}</span>
</div>
{/* Main Content Container */}
<div className="bg-white rounded-[16px] p-6">
{/* Yacht Title and Actions */}
<div className="mb-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h1 className="text-2xl md:text-3xl font-bold text-[#333333]">
{yacht.name}
</h1>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 text-[#333333]">
<Icon name="map" size={20} />
<span className="text-base">{yacht.location}</span>
</div>
<button className="flex items-center gap-2 text-[#333333] hover:text-[#008299] transition-colors">
<Icon name="ad" size={20} />
<span className="text-base">Поделиться</span>
</button>
<button className="flex items-center gap-2 text-[#333333] hover:text-[#008299] transition-colors">
<Icon name="like" size={20} />
<span className="text-base">Избранное</span>
</button>
</div>
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Gallery and Availability */}
<div className="lg:col-span-2 space-y-6">
{/* Gallery */}
<YachtGallery
images={yacht.images}
badge={yacht.badge}
/>
{/* Availability */}
<YachtAvailability price={yacht.price} />
{/* Characteristics */}
<YachtCharacteristics yacht={yacht} />
{/* Description */}
<div>
<h2 className="text-xl font-bold text-[#333333] mb-4">
Описание
</h2>
<p className="text-base text-[#333333] leading-relaxed">
{yacht.description}
</p>
</div>
{/* Contact and Requisites */}
<ContactInfo
contactPerson={yacht.contactPerson}
requisites={yacht.requisites}
/>
{/* Reviews */}
<div>
<h2 className="text-xl font-bold text-[#333333] mb-4">
Отзывы
</h2>
<p className="text-base text-[#999999]">
У этой яхты пока нет отзывов
</p>
</div>
</div>
{/* Right Column - Booking Widget */}
<div className="lg:col-span-1">
<BookingWidget price={yacht.price} />
</div>
</div>
</div>
</div>
</main>
);
}

View File

@ -48,11 +48,10 @@ export const GuestPicker: React.FC<GuestPickerProps> = ({
const getDisplayText = () => { const getDisplayText = () => {
const total = adults + childrenCount; const total = adults + childrenCount;
if (total === 0) return placeholder; if (total === 0) return placeholder;
if (total === 1) return "1 гость";
if (childrenCount === 0) if (childrenCount === 0)
return `${adults} ${adults === 1 ? "взрослый" : "взрослых"}`; return `${adults} ${adults === 1 ? "гость" : "гостей"}`;
return `${adults} ${ return `${total} ${total === 1 ? "гость" : "гостей"}`;
adults === 1 ? "взрослый" : "взрослых"
}, ${childrenCount} ${childrenCount === 1 ? "ребенок" : "детей"}`;
}; };
return ( return (