Files
nahodka-front/src/components/DateCalendarPopup.jsx
2026-08-12 23:59:16 +03:00

115 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/components/DateCalendarPopup.jsx
import { useState, useEffect, useRef, useLayoutEffect } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from 'react-i18next';
export default function DateCalendarPopup({ date, onClose, anchorRef }) {
const { t } = useTranslation();
const months = t('calendar.months', { returnObjects: true });
const dayNames = t('calendar.days', { returnObjects: true });
const ref = useRef(null);
const [coords, setCoords] = useState(null);
const selectedDate = new Date(date);
const isValidDate = !Number.isNaN(selectedDate.getTime());
const [year, setYear] = useState(isValidDate ? selectedDate.getFullYear() : new Date().getFullYear());
const [month, setMonth] = useState(isValidDate ? selectedDate.getMonth() : new Date().getMonth());
// Двухпроходное позиционирование:
// 1) сразу после монтирования попап невидим (opacity: 0), но уже в DOM —
// нужно, чтобы измерить его реальные width/height.
// 2) как только размеры известны — пересчитываем координаты так, чтобы
// попап открывался влево-вверх от якоря, и показываем (opacity: 1).
useLayoutEffect(() => {
if (!anchorRef?.current || !ref.current) return;
const anchorRect = anchorRef.current.getBoundingClientRect();
const popupRect = ref.current.getBoundingClientRect();
setCoords({
top: anchorRect.top + window.scrollY - popupRect.height - 4,
left: anchorRect.right + window.scrollX - popupRect.width,
});
}, [anchorRef, month, year]); // пересчитываем и при смене месяца — высота грид может немного плавать
useEffect(() => {
const handler = (e) => {
const clickedAnchor = anchorRef?.current?.contains(e.target);
const clickedPopup = ref.current?.contains(e.target);
if (!clickedAnchor && !clickedPopup) onClose();
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [onClose, anchorRef]);
const goToPrevMonth = () => {
if (month === 0) {
setMonth(11);
setYear(y => y - 1);
} else {
setMonth(m => m - 1);
}
};
const goToNextMonth = () => {
if (month === 11) {
setMonth(0);
setYear(y => y + 1);
} else {
setMonth(m => m + 1);
}
};
const firstDay = new Date(year, month, 1).getDay();
const offset = firstDay === 0 ? 6 : firstDay - 1;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const cells = [];
for (let i = 0; i < offset; i++) cells.push(null);
for (let i = 1; i <= daysInMonth; i++) cells.push(i);
const isSameDay = (day) =>
isValidDate &&
selectedDate.getFullYear() === year &&
selectedDate.getMonth() === month &&
selectedDate.getDate() === day;
return createPortal(
<div
className="f-date-calendar"
ref={ref}
style={{
position: "absolute",
top: coords?.top ?? 0,
left: coords?.left ?? 0,
zIndex: 1000,
// Пока координаты не вычислены (первый проход) — прячем, чтобы
// не мелькал в неправильном месте до пересчёта.
visibility: coords ? "visible" : "hidden",
}}
>
<div className="f-cal-header">
<button type="button" onClick={goToPrevMonth}>‹</button>
<span>{months[month]} {year}</span>
<button type="button" onClick={goToNextMonth}>›</button>
</div>
<div className="f-cal-grid">
{dayNames.map(dayName => (
<div key={dayName} className="f-cal-day-name">{dayName}</div>
))}
{cells.map((day, i) =>
day ? (
<div
key={i}
className={`f-cal-day${isSameDay(day) ? " f-cal-selected" : ""}`}
>
{day}
</div>
) : <div key={i} />
)}
</div>
</div>,
document.body
);
}