мобильные версии страниц
This commit is contained in:
parent
2383f75fe2
commit
bc31770c51
|
|
@ -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<string>("");
|
||||
const [endTime, setEndTime] = useState<string>("");
|
||||
|
||||
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 (
|
||||
<div className="w-full">
|
||||
{/* Навигация по месяцам */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<button
|
||||
onClick={goToPreviousMonth}
|
||||
className="w-10 h-10 rounded-full border border-[#dfdfdf] flex items-center justify-center hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<ChevronLeftIcon className="size-4 text-[#333333]" />
|
||||
</button>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-lg font-medium text-[#333333] capitalize">
|
||||
{format(currentMonth, "LLLL", { locale: ru })}
|
||||
</span>
|
||||
<span className="text-sm text-[#999999]">
|
||||
Свободных дней: {getAvailableDaysCount()}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={goToNextMonth}
|
||||
className="w-10 h-10 rounded-full border border-[#dfdfdf] flex items-center justify-center hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<ChevronRightIcon className="size-4 text-[#333333]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Календарь */}
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<Calendar
|
||||
mode="single"
|
||||
month={currentMonth}
|
||||
onMonthChange={setCurrentMonth}
|
||||
showOutsideDays={false}
|
||||
className="w-full p-0"
|
||||
locale={ru}
|
||||
formatters={{
|
||||
formatWeekdayName: (date) => {
|
||||
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 <div className="hidden" />;
|
||||
}
|
||||
|
||||
const isCrossedOut = shouldBeCrossedOut(
|
||||
day.date
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={`relative w-full flex items-center justify-center text-sm font-medium transition-colors ${
|
||||
isCrossedOut
|
||||
? "text-[#CCCCCC] line-through"
|
||||
: "text-[#333333] hover:bg-gray-100"
|
||||
}`}
|
||||
style={
|
||||
{
|
||||
aspectRatio: "1 / 1",
|
||||
minHeight: "44px",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
disabled={isCrossedOut}
|
||||
>
|
||||
{day.date.getDate()}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Выбор времени */}
|
||||
<div className="space-y-4 mb-4" style={{ marginTop: "24px" }}>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1">
|
||||
<select
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
|
||||
>
|
||||
<option value="">--:--</option>
|
||||
{timeOptions.map((time) => (
|
||||
<option key={time.value} value={time.value}>
|
||||
{time.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<select
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-[#DFDFDF] rounded-lg text-base text-[#333333] bg-white appearance-none"
|
||||
>
|
||||
<option value="">--:--</option>
|
||||
{timeOptions.map((time) => (
|
||||
<option key={time.value} value={time.value}>
|
||||
{time.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-[#999999]">
|
||||
<Clock size={16} />
|
||||
<span>По местному времени яхты</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) {
|
|||
<CarouselContent>
|
||||
{images.map((img, index) => (
|
||||
<CarouselItem key={index}>
|
||||
<div className="relative w-full h-[592px] rounded-lg overflow-hidden">
|
||||
<div className="relative w-full h-[60vh] lg:h-[592px] rounded-0 lg:rounded-[24px] overflow-hidden">
|
||||
<Image
|
||||
src={img}
|
||||
alt={`Yacht image ${index + 1}`}
|
||||
|
|
@ -64,12 +64,12 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) {
|
|||
</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" />
|
||||
<CarouselPrevious className="left-2 lg:left-4 bg-white/80 hover:bg-white border-gray-300 w-10 h-10 lg:w-12 lg:h-12 rounded-full" />
|
||||
<CarouselNext className="right-2 lg:right-4 bg-white/80 hover:bg-white border-gray-300 w-10 h-10 lg:w-12 lg:h-12 rounded-full" />
|
||||
</Carousel>
|
||||
{/* Badge - поверх слайдера, не скроллится */}
|
||||
{badge && (
|
||||
<div className="absolute top-4 left-4 z-20 pointer-events-none">
|
||||
<div className="absolute bottom-4 left-4 z-20 pointer-events-none">
|
||||
<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>
|
||||
|
|
@ -84,8 +84,8 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thumbnails */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
{/* Thumbnails - скрыты на мобильных */}
|
||||
<div className="hidden lg:flex gap-2 overflow-x-auto pb-2">
|
||||
{images.map((img, index) => (
|
||||
<button
|
||||
key={index}
|
||||
|
|
@ -108,4 +108,3 @@ export function YachtGallery({ images, badge }: YachtGalleryProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
export const YACHT = {
|
||||
id: 1,
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
|
@ -1,62 +1,52 @@
|
|||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { ArrowLeft, Heart } from "lucide-react";
|
||||
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";
|
||||
import { YACHT } from "./const";
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
// const params = useParams();
|
||||
const router = useRouter();
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
| "availability"
|
||||
| "description"
|
||||
| "characteristics"
|
||||
| "contact"
|
||||
| "requisites"
|
||||
| "reviews"
|
||||
>("availability");
|
||||
|
||||
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">
|
||||
<main className="bg-[#f4f4f4] min-h-screen ">
|
||||
{/* Мобильная фиксированная верхняя панель навигации */}
|
||||
<div className="lg:hidden fixed top-[73px] left-0 right-0 z-[50] bg-white border-b border-gray-200">
|
||||
<div className="flex items-center justify-between px-4 h-14">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ArrowLeft size={24} className="text-[#333333]" />
|
||||
</button>
|
||||
<h2 className="text-base font-medium text-[#333333]">
|
||||
Яхта
|
||||
</h2>
|
||||
<button className="flex items-center justify-center">
|
||||
<Heart size={24} className="text-[#333333]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Десктопная версия - Breadcrumbs */}
|
||||
<div className="hidden lg:block container max-w-6xl mx-auto px-4 py-4">
|
||||
<div className="text-sm text-[#999999] flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
||||
Аренда яхты
|
||||
|
|
@ -69,26 +59,147 @@ export default function YachtDetailPage() {
|
|||
</span>
|
||||
</Link>
|
||||
<span>></span>
|
||||
<span className="text-[#333333]">{yacht.name}</span>
|
||||
<span className="text-[#333333]">{YACHT.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Container */}
|
||||
<div className="bg-white rounded-[16px] p-6">
|
||||
<div className="lg:container lg:max-w-6xl lg:mx-auto lg:px-4 lg:pb-6">
|
||||
<div className="bg-white lg:rounded-[16px] lg:p-6">
|
||||
{/* Мобильная версия - без отступов сверху, с отступом для фиксированной панели */}
|
||||
<div className="lg:hidden pt-[50px]">
|
||||
{/* Gallery */}
|
||||
<YachtGallery
|
||||
images={YACHT.images}
|
||||
badge={YACHT.badge}
|
||||
/>
|
||||
|
||||
{/* Yacht Title */}
|
||||
<div className="px-4 pt-4">
|
||||
<h1 className="text-xl font-bold text-[#333333] mb-4">
|
||||
{YACHT.name}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="px-4 border-b border-gray-200 overflow-x-auto">
|
||||
<div className="flex gap-6 min-w-max">
|
||||
<button
|
||||
onClick={() => setActiveTab("availability")}
|
||||
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
|
||||
activeTab === "availability"
|
||||
? "text-[#008299] border-b-2 border-[#008299]"
|
||||
: "text-[#999999]"
|
||||
}`}
|
||||
>
|
||||
Свободные даты
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("description")}
|
||||
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
|
||||
activeTab === "description"
|
||||
? "text-[#008299] border-b-2 border-[#008299]"
|
||||
: "text-[#999999]"
|
||||
}`}
|
||||
>
|
||||
Описание
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setActiveTab("characteristics")
|
||||
}
|
||||
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
|
||||
activeTab === "characteristics"
|
||||
? "text-[#008299] border-b-2 border-[#008299]"
|
||||
: "text-[#999999]"
|
||||
}`}
|
||||
>
|
||||
Характеристики
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("contact")}
|
||||
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
|
||||
activeTab === "contact"
|
||||
? "text-[#008299] border-b-2 border-[#008299]"
|
||||
: "text-[#999999]"
|
||||
}`}
|
||||
>
|
||||
Контактное лицо и реквизиты
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("reviews")}
|
||||
className={`pb-3 text-base font-medium transition-colors whitespace-nowrap ${
|
||||
activeTab === "reviews"
|
||||
? "text-[#008299] border-b-2 border-[#008299]"
|
||||
: "text-[#999999]"
|
||||
}`}
|
||||
>
|
||||
Отзывы
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="px-4 py-6">
|
||||
{activeTab === "availability" && (
|
||||
<YachtAvailability
|
||||
price={YACHT.price}
|
||||
mobile={true}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "description" && (
|
||||
<div>
|
||||
<p className="text-base text-[#666666] leading-relaxed">
|
||||
{YACHT.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "characteristics" && (
|
||||
<YachtCharacteristics yacht={YACHT} />
|
||||
)}
|
||||
{activeTab === "contact" && (
|
||||
<ContactInfo
|
||||
contactPerson={YACHT.contactPerson}
|
||||
requisites={YACHT.requisites}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "reviews" && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Icon name="reviewStar" size={16} />
|
||||
<h2 className="text-base font-bold text-[#333333]">
|
||||
Отзывы
|
||||
</h2>
|
||||
</div>
|
||||
<div className="border border-[#DFDFDF] rounded-[12px] flex items-center justify-center py-18">
|
||||
<p className="text-lg text-[#999999]">
|
||||
У этой яхты пока нет отзывов
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Десктопная версия */}
|
||||
<div className="hidden lg:block">
|
||||
{/* 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-xl md:text-2 xl font-bold text-[#333333]">
|
||||
{yacht.name}
|
||||
<h1 className="text-xl md:text-2xl 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="pin" size={32} />
|
||||
<span className="text-base">
|
||||
{yacht.location}
|
||||
{YACHT.location}
|
||||
</span>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors">
|
||||
<Icon name="share" size={32} />
|
||||
<span className="text-base">Поделиться</span>
|
||||
<span className="text-base">
|
||||
Поделиться
|
||||
</span>
|
||||
</button>
|
||||
<button className="flex items-center gap-2 cursor-pointer text-[#333333] hover:text-[#008299] transition-colors">
|
||||
<Icon name="heart" size={32} />
|
||||
|
|
@ -101,8 +212,8 @@ export default function YachtDetailPage() {
|
|||
<div className="space-y-6">
|
||||
{/* Gallery */}
|
||||
<YachtGallery
|
||||
images={yacht.images}
|
||||
badge={yacht.badge}
|
||||
images={YACHT.images}
|
||||
badge={YACHT.badge}
|
||||
/>
|
||||
|
||||
{/* Content with Booking Widget on the right */}
|
||||
|
|
@ -110,10 +221,10 @@ export default function YachtDetailPage() {
|
|||
{/* Left column - all content below gallery */}
|
||||
<div className="flex-1 space-y-6">
|
||||
{/* Availability */}
|
||||
<YachtAvailability price={yacht.price} />
|
||||
<YachtAvailability price={YACHT.price} />
|
||||
|
||||
{/* Characteristics */}
|
||||
<YachtCharacteristics yacht={yacht} />
|
||||
<YachtCharacteristics yacht={YACHT} />
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
|
|
@ -121,14 +232,14 @@ export default function YachtDetailPage() {
|
|||
Описание
|
||||
</h2>
|
||||
<p className="text-base text-[#666666] leading-relaxed">
|
||||
{yacht.description}
|
||||
{YACHT.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Contact and Requisites */}
|
||||
<ContactInfo
|
||||
contactPerson={yacht.contactPerson}
|
||||
requisites={yacht.requisites}
|
||||
contactPerson={YACHT.contactPerson}
|
||||
requisites={YACHT.requisites}
|
||||
/>
|
||||
|
||||
{/* Reviews */}
|
||||
|
|
@ -149,12 +260,33 @@ export default function YachtDetailPage() {
|
|||
|
||||
{/* Right column - Booking Widget (sticky) */}
|
||||
<div className="lg:w-74 flex-shrink-0 lg:sticky lg:top-24 self-start">
|
||||
<BookingWidget price={yacht.price} />
|
||||
<BookingWidget price={YACHT.price} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Мобильная фиксированная нижняя панель бронирования */}
|
||||
<div className="lg:hidden sticky bottom-0 left-0 right-0 z-[10] bg-white border-t border-gray-200 px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-lg font-bold text-[#333333]">
|
||||
{YACHT.price} ₽
|
||||
</span>
|
||||
<span className="text-sm text-[#999999] ml-1">
|
||||
/ час
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push("/confirm")}
|
||||
className="bg-[#008299] text-white px-6 py-3 rounded-lg font-bold text-base hover:bg-[#006d7a] transition-colors"
|
||||
>
|
||||
Забронировать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
|
||||
{/* Кнопка Применить */}
|
||||
<Button className="w-full bg-[#008299] hover:bg-[#006d7f] text-white font-bold h-12 mt-2">
|
||||
<Button
|
||||
onClick={onApply}
|
||||
className="w-full bg-[#008299] hover:bg-[#006d7f] text-white font-bold h-12 mt-2"
|
||||
>
|
||||
Применить
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="bg-[#f4f4f4] min-h-screen">
|
||||
<div className="container max-w-6xl mx-auto px-4 py-6">
|
||||
{/* Breadcrumbs */}
|
||||
<div className="mb-4 text-sm text-[#999999] flex items-center gap-[16px]">
|
||||
{/* Breadcrumbs - скрыты на мобильных */}
|
||||
<div className="hidden lg:flex mb-4 text-sm text-[#999999] items-center gap-[16px]">
|
||||
<Link href="/">
|
||||
<span className="cursor-pointer hover:text-[#333333] transition-colors">
|
||||
Аренда яхты
|
||||
|
|
@ -127,21 +132,86 @@ export default function CatalogPage() {
|
|||
<span className="text-[#333333]">Каталог яхт</span>
|
||||
</div>
|
||||
|
||||
{/* Мобильная навигационная панель */}
|
||||
<div className="lg:hidden mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Кнопка "Назад" */}
|
||||
<Link href="/">
|
||||
<button className="flex-shrink-0 w-8 h-8 flex items-center justify-center">
|
||||
<ArrowLeft
|
||||
size={32}
|
||||
className="text-[#333333] font-bold"
|
||||
/>
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
{/* Текстовый блок */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-bold text-[#333333] text-base truncate">
|
||||
Балаклава
|
||||
</div>
|
||||
<div className="text-sm text-[#999999] truncate">
|
||||
Когда? · С кем?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопка "Фильтры" */}
|
||||
<Button
|
||||
onClick={() => setIsFiltersOpen(true)}
|
||||
className="flex-shrink-0 w-[56px] h-[56px] rounded-full bg-white hover:bg-gray-50 text-black p-0"
|
||||
>
|
||||
<Sliders size={20} className="text-[#333333]" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-[74px]">
|
||||
{/* Sidebar */}
|
||||
<div className="w-full lg:w-[260px] flex-shrink-0">
|
||||
{/* Sidebar - скрыт на мобильных, виден на десктопе */}
|
||||
<div className="hidden lg:block w-full lg:w-[260px] flex-shrink-0">
|
||||
<CatalogSidebar />
|
||||
</div>
|
||||
|
||||
{/* Мобильное модальное окно фильтров */}
|
||||
{isFiltersOpen && (
|
||||
<div className="fixed inset-0 z-50 lg:hidden">
|
||||
{/* Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50"
|
||||
onClick={() => setIsFiltersOpen(false)}
|
||||
/>
|
||||
{/* Модальное окно */}
|
||||
<div className="absolute inset-0 bg-white overflow-y-auto">
|
||||
<div className="sticky top-0 bg-white border-b border-gray-200 p-4 flex items-center justify-between z-10">
|
||||
<h2 className="text-lg font-bold text-[#333333]">
|
||||
Фильтры
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsFiltersOpen(false)}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<X size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<CatalogSidebar
|
||||
onApply={() => setIsFiltersOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end mb-6 gap-4">
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end mb-6">
|
||||
<div className="w-full lg:w-auto">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-[#333333] mb-4">
|
||||
Аренда яхт
|
||||
</h1>
|
||||
<p className="text-lg text-[#333333]">
|
||||
<p className="hidden lg:block text-lg text-[#333333]">
|
||||
Доступно яхт:{" "}
|
||||
<span className="text-[#b2b2b2]">
|
||||
{yachts.length}
|
||||
|
|
|
|||
|
|
@ -3,29 +3,207 @@
|
|||
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 (
|
||||
<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">
|
||||
Яхта Сеньорита
|
||||
</h2>
|
||||
<div className="flex justify-center gap-10 text-xs text-[#666666]">
|
||||
<span>09 авг.</span>
|
||||
<span>Гостей: 1</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]">
|
||||
9 Авг 00:00
|
||||
</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]">
|
||||
9 Авг 02:00
|
||||
</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]">
|
||||
1 гость
|
||||
</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>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-[#333333]">
|
||||
26 400₽ x 2ч
|
||||
</span>
|
||||
<span className="text-[#333333]">
|
||||
52 800 ₽
|
||||
</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>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[#333333]">
|
||||
Итого:
|
||||
</span>
|
||||
<span className="font-bold text-[#333333]">
|
||||
52 800 Р
|
||||
</span>
|
||||
</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)
|
||||
}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
Отправить заявку
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Десктопная версия */}
|
||||
<div className="hidden lg:block">
|
||||
<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-[16px]">
|
||||
{/* 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>
|
||||
<span className="text-[#333333]">
|
||||
Ваше бронирование
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Левая колонка - Информация о яхте и ценах */}
|
||||
<div className="w-full lg:w-[336px] flex-shrink-0 flex flex-col 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">
|
||||
{/* Изображение яхты */}
|
||||
|
|
@ -121,8 +299,7 @@ export default function ConfirmPage() {
|
|||
|
||||
{/* Правая колонка - Подтверждение бронирования */}
|
||||
<div className="flex-1">
|
||||
<div className="bg-white rounded-[16px]">
|
||||
<div className="p-8">
|
||||
<div className="bg-white rounded-[16px] p-8">
|
||||
{/* Заголовок */}
|
||||
<h1 className="text-2xl text-[#333333] mb-4">
|
||||
Проверьте данные
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default function Header() {
|
|||
const [isAuthDialogOpen, setIsAuthDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white/95 backdrop-blur-sm border-b border-gray-100">
|
||||
<header className="sticky top-0 z-50 bg-white backdrop-blur-sm border-b border-gray-100">
|
||||
<div className="w-full max-w-6xl mx-auto">
|
||||
<div className="flex justify-between items-center px-4 sm:px-6 lg:px-8 py-4">
|
||||
{/* Логотип */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue