340 lines
16 KiB
TypeScript
340 lines
16 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { Calendar } from "@/components/ui/calendar";
|
||
import {
|
||
isSameMonth,
|
||
isBefore,
|
||
startOfDay,
|
||
format,
|
||
eachDayOfInterval,
|
||
startOfMonth,
|
||
endOfMonth,
|
||
} from "date-fns";
|
||
import { ru } from "date-fns/locale";
|
||
import { ChevronLeftIcon, ChevronRightIcon, Clock } from "lucide-react";
|
||
|
||
interface YachtAvailabilityProps {
|
||
price: string;
|
||
mobile?: boolean;
|
||
}
|
||
|
||
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);
|
||
});
|
||
|
||
const isDateUnavailable = (date: Date) => {
|
||
return unavailableDates.some(
|
||
(d) =>
|
||
d.getDate() === date.getDate() &&
|
||
d.getMonth() === date.getMonth() &&
|
||
d.getFullYear() === date.getFullYear()
|
||
);
|
||
};
|
||
|
||
const isDateInPast = (date: Date) => {
|
||
return isBefore(startOfDay(date), today);
|
||
};
|
||
|
||
const shouldBeCrossedOut = (date: Date) => {
|
||
// Перечеркиваем если день занят или находится до текущего дня
|
||
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)
|
||
);
|
||
};
|
||
|
||
const goToNextMonth = () => {
|
||
setCurrentMonth(
|
||
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">
|
||
<h2 className="text-base font-bold text-[#333333]">
|
||
Доступность яхты
|
||
</h2>
|
||
</div>
|
||
|
||
<div className="bg-white w-full">
|
||
<div className="w-full flex justify-end mb-8">
|
||
<div className="flex items-center gap-5">
|
||
<button
|
||
onClick={goToPreviousMonth}
|
||
className="cursor-pointer rounded-full border border-[#dfdfdf] h-12 w-12 flex items-center justify-center"
|
||
>
|
||
<ChevronLeftIcon className="size-4" />
|
||
</button>
|
||
<span className="text-2xl text-[#333333]">
|
||
{format(currentMonth, "LLLL yyyy", { locale: ru })}
|
||
</span>
|
||
<button
|
||
onClick={goToNextMonth}
|
||
className="cursor-pointer rounded-full border border-[#dfdfdf] h-12 w-12 flex items-center justify-center"
|
||
>
|
||
<ChevronRightIcon className="size-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<Calendar
|
||
mode="single"
|
||
month={currentMonth}
|
||
onMonthChange={setCurrentMonth}
|
||
showOutsideDays={false}
|
||
className="w-full p-0"
|
||
locale={ru}
|
||
classNames={{
|
||
root: "w-full",
|
||
month: "flex w-full flex-col gap-4",
|
||
nav: "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-2",
|
||
month_caption: "hidden",
|
||
caption_label: "text-2xl",
|
||
button_previous: "hidden",
|
||
button_next: "hidden",
|
||
table: "w-full border-collapse",
|
||
weekdays: "hidden",
|
||
weekday:
|
||
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
|
||
week: "flex w-full",
|
||
day: "relative flex-1",
|
||
}}
|
||
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 h-20 flex flex-col items-start justify-start px-2 py-[2px] border border-gray-200"
|
||
disabled={isCrossedOut}
|
||
>
|
||
{isCrossedOut ? (
|
||
// Перечеркнутая ячейка для недоступных дней
|
||
<>
|
||
<span className="text-sm font-medium text-[#333333] self-end">
|
||
{day.date.getDate()}
|
||
</span>
|
||
<div className="absolute inset-0 flex items-center justify-center">
|
||
<span className="text-gray-300 text-4xl font-light leading-none">
|
||
✕
|
||
</span>
|
||
</div>
|
||
</>
|
||
) : (
|
||
// Доступный день с информацией
|
||
<>
|
||
{/* Дата и "Доступно:" в одной строке */}
|
||
<div className="flex items-center justify-between w-full">
|
||
<span className="text-xs text-gray-400">
|
||
Доступно:
|
||
</span>
|
||
<span className="text-sm font-medium text-[#333333]">
|
||
{day.date.getDate()}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col gap-1.5 w-full mt-1">
|
||
<div className="w-fit bg-[#F6BD4D] text-white text-[10px] font-medium px-1 py-0 rounded-full inline-block">
|
||
08:00—20:00
|
||
</div>
|
||
</div>
|
||
{/* Цена в нижнем правом углу */}
|
||
<span className="absolute bottom-[2px] right-[4px] text-xs text-[#333333] font-medium">
|
||
{price} / час
|
||
</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
);
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|