186 lines
7.6 KiB
JavaScript
186 lines
7.6 KiB
JavaScript
import React from "react";
|
||
import { Button } from "../UI/Button";
|
||
|
||
const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"];
|
||
|
||
const toDateKey = (date) => {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
return `${year}-${month}-${day}`;
|
||
};
|
||
|
||
const isWeekendDate = (date) => {
|
||
const day = date.getDay();
|
||
return day === 0 || day === 6;
|
||
};
|
||
|
||
const isSelectableCalendarDate = (date, minDateKey) => {
|
||
const dateKey = toDateKey(date);
|
||
return dateKey >= minDateKey && !isWeekendDate(date);
|
||
};
|
||
|
||
const formatDateForDisplay = (value) => {
|
||
if (!value) {
|
||
return "Выберите дату";
|
||
}
|
||
|
||
const [year, month, day] = value.split("-").map(Number);
|
||
if (!year || !month || !day) {
|
||
return value;
|
||
}
|
||
|
||
return new Date(year, month - 1, day).toLocaleDateString("ru-RU", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
});
|
||
};
|
||
|
||
const CalendarWidget = ({
|
||
label,
|
||
selectedDate,
|
||
onDateChange,
|
||
minDateKey,
|
||
isCalendarOpen,
|
||
setIsCalendarOpen,
|
||
currentMonth,
|
||
setCurrentMonth,
|
||
calendarDays,
|
||
monthLabel,
|
||
canGoBack,
|
||
timeOptions,
|
||
selectedTime,
|
||
onTimeChange,
|
||
layoutClassName,
|
||
calendarClassName,
|
||
timeClassName,
|
||
}) => {
|
||
return (
|
||
<div className={layoutClassName}>
|
||
<div className={calendarClassName}>
|
||
<Button
|
||
variant="ghost"
|
||
aria-label={label}
|
||
aria-expanded={isCalendarOpen}
|
||
className="flex min-h-[54px] w-full items-center justify-between rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-left text-sm font-medium !text-[var(--color-text)] transition hover:border-[var(--color-accent)] focus:border-[var(--color-accent)] focus:outline-none"
|
||
onClick={() => setIsCalendarOpen((current) => !current)}
|
||
>
|
||
<span>{selectedDate ? formatDateForDisplay(selectedDate) : "Выберите дату"}</span>
|
||
<span aria-hidden="true" className="text-[var(--color-text-muted)]">▾</span>
|
||
</Button>
|
||
{isCalendarOpen ? (
|
||
<div className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3 shadow-lg absolute left-0 top-full z-50 mt-2 w-[300px]">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||
{label}
|
||
</p>
|
||
<h4
|
||
className="mt-1 text-base font-semibold capitalize"
|
||
style={{ color: "var(--color-text)" }}
|
||
>
|
||
{monthLabel}
|
||
</h4>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
disabled={!canGoBack}
|
||
aria-label="Предыдущий месяц"
|
||
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-40"
|
||
onClick={() => setCurrentMonth((month) => new Date(month.getFullYear(), month.getMonth() - 1, 1))}
|
||
>
|
||
‹
|
||
</button>
|
||
<button
|
||
type="button"
|
||
aria-label="Следующий месяц"
|
||
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:text-[var(--color-text)]"
|
||
onClick={() => setCurrentMonth((month) => new Date(month.getFullYear(), month.getMonth() + 1, 1))}
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 grid grid-cols-7 gap-1 text-center text-[10px] font-semibold uppercase text-[var(--color-text-muted)]">
|
||
{WEEK_DAY_LABELS.map((day) => (
|
||
<div key={day} className="px-1 py-1">
|
||
{day}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="mt-1 grid grid-cols-7 gap-1">
|
||
{calendarDays.map((day, index) => {
|
||
if (!day) {
|
||
return <div key={`empty-${index}`} className="aspect-square" />;
|
||
}
|
||
|
||
const dateKey = toDateKey(day);
|
||
const isWeekend = isWeekendDate(day);
|
||
const isSelectable = isSelectableCalendarDate(day, minDateKey);
|
||
const isSelected = dateKey === selectedDate;
|
||
const isDisabled = !isSelectable;
|
||
const dayNumber = String(day.getDate()).padStart(2, "0");
|
||
|
||
return (
|
||
<Button
|
||
key={dateKey}
|
||
variant="ghost"
|
||
disabled={isDisabled}
|
||
title={isWeekend ? "Выходной, доставки нет" : isSelectable ? "Можно выбрать" : "Недоступно"}
|
||
className={[
|
||
"relative flex aspect-square items-center justify-center rounded-xl border text-sm font-semibold transition",
|
||
isSelected
|
||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]"
|
||
: isWeekend
|
||
? "border-dashed border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)]"
|
||
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]",
|
||
isDisabled ? "cursor-not-allowed opacity-45" : "",
|
||
].join(" ")}
|
||
onClick={() => {
|
||
if (isDisabled) return;
|
||
onDateChange(dateKey);
|
||
setIsCalendarOpen(false);
|
||
}}
|
||
>
|
||
<span>{dayNumber}</span>
|
||
{isWeekend ? (
|
||
<span
|
||
aria-hidden="true"
|
||
className="absolute inset-x-2 top-1/2 h-px -rotate-12 bg-[var(--color-text-muted)] opacity-70"
|
||
/>
|
||
) : null}
|
||
</Button>
|
||
);
|
||
})}
|
||
</div>
|
||
<p className="mt-2 text-xs text-[var(--color-text-muted)]">
|
||
Выходные отмечены пунктиром и недоступны.
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
<div className={timeClassName}>
|
||
{timeOptions.map((option) => (
|
||
<Button
|
||
key={option}
|
||
variant="ghost"
|
||
aria-pressed={selectedTime === option}
|
||
className={[
|
||
"min-h-[54px] rounded-2xl border px-4 text-left text-sm font-medium transition",
|
||
selectedTime === option
|
||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]"
|
||
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]",
|
||
].join(" ")}
|
||
onClick={() => onTimeChange(option)}
|
||
>
|
||
{option}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export { CalendarWidget, formatDateForDisplay }; |