import { addWeeks, buildCalendarMonth, DateTime, getWeekStart, toDateOnly, } from "@batch-cooking/date-tools"; import { WEEK_DAYS } from "@batch-cooking/shared"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import "./week-navigator.scss"; /** "17 au 23 août 2026" — collapses the month/year to just the end date when both ends of the week share it, spells it out on both ends otherwise (e.g. a week straddling two months). */ function formatWeekRange(weekStart: DateTime): string { const weekEnd = weekStart.plus({ days: 6 }); const sameMonth = weekStart.hasSame(weekEnd, "month"); const startLabel = weekStart.toLocaleString( sameMonth ? { day: "numeric" } : { day: "numeric", month: "long" }, { locale: "fr" }, ); const endLabel = weekEnd.toLocaleString( { day: "numeric", month: "long", year: "numeric" }, { locale: "fr" }, ); return `${startLabel} au ${endLabel}`; } /** * Arrows + clickable label opening {@link CalendarPopover} — week-selection * UI shared by any page organized around "one week at a time" (originally * `PlanningPage`'s own grid, now also `ShoppingListPage` — both just need a * `weekStart` in/out, neither cares how the other renders its own content * for that week). Copy comes from `common.weekNav.*`/`common.calendar.*`/ * `common.days.*` rather than `planning.*` — generic enough ("Semaine * précédente", day names) to not read as planning-specific from a page that * isn't the planning grid. */ export function WeekNavigator({ weekStart, onChangeWeek, }: { weekStart: DateTime; onChangeWeek: (weekStart: DateTime) => void; }) { const { t } = useTranslation(); const [isCalendarOpen, setIsCalendarOpen] = useState(false); const isThisWeek = weekStart.hasSame(getWeekStart(DateTime.utc()), "day"); return (
{isCalendarOpen && ( { onChangeWeek(getWeekStart(day)); setIsCalendarOpen(false); }} onClose={() => setIsCalendarOpen(false)} /> )}
); } /** Month calendar letting the visitor jump to any week at once — selecting a day selects its whole (Monday-first) week. Closes itself on an outside click. */ function CalendarPopover({ selectedWeekStart, onSelectDay, onClose, }: { selectedWeekStart: DateTime; onSelectDay: (day: DateTime) => void; onClose: () => void; }) { const { t } = useTranslation(); // Its own state: browsing to a different month to pick a week there // shouldn't jump back every render — only re-anchors when the popover is // first opened (`selectedWeekStart` at that point), not while it's open. const [visibleMonth, setVisibleMonth] = useState(() => selectedWeekStart.startOf("month")); const popoverRef = useRef(null); useEffect(() => { function handleClickOutside(e: MouseEvent) { if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { onClose(); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [onClose]); const today = toDateOnly(DateTime.utc()); const selectedWeekEnd = selectedWeekStart.plus({ days: 6 }); const weeks = buildCalendarMonth(visibleMonth); return (
{visibleMonth.toLocaleString({ month: "long", year: "numeric" }, { locale: "fr" })}
{WEEK_DAYS.map((weekDay) => ( {t(`common.days.${weekDay}`).charAt(0)} ))} {weeks.flat().map((day) => { const classNames = ["calendar-grid__day"]; if (!day.hasSame(visibleMonth, "month")) classNames.push("calendar-grid__day--muted"); if (day >= selectedWeekStart && day <= selectedWeekEnd) { classNames.push("calendar-grid__day--in-selected-week"); } if (day.hasSame(today, "day")) classNames.push("calendar-grid__day--today"); return ( ); })}
); }