"use client"; import { useState, useMemo, Fragment } from "react"; import { useTranslations } from "next-intl"; import { ChevronLeft, ChevronRight, ChevronDown } from "lucide-react"; import { addMonths, subMonths, addYears, subYears, setMonth, setYear, getISOWeek, getWeek, format, } from "date-fns"; import { cn } from "@/lib/utils"; import { getEventDayBounds } from "@/lib/calendar-utils"; import type { CalendarEvent } from "@/lib/jmap/types"; import { useCalendarLocale } from "@/hooks/use-calendar-locale"; type PickerView = "days" | "months" | "years"; interface MiniCalendarProps { selectedDate: Date; displayMonth: Date; onSelectDate: (date: Date) => void; onChangeMonth: (date: Date) => void; events?: CalendarEvent[]; firstDayOfWeek?: number; showWeekNumbers?: boolean; } export function MiniCalendar({ selectedDate, displayMonth, onSelectDate, onChangeMonth, events = [], firstDayOfWeek = 1, showWeekNumbers = false, }: MiniCalendarProps) { const t = useTranslations("calendar"); const { weekStartsOn, dayHeaderKeys, getMonthGridDays, checkIsToday, checkIsSameMonth, checkIsSameDay, formatDayNumber, formatMonthYear, getMonth, getYear, monthLabelKeys, } = useCalendarLocale(); const [pickerView, setPickerView] = useState("days"); const days = useMemo( () => getMonthGridDays(displayMonth), [displayMonth, getMonthGridDays], ); const eventDates = useMemo(() => { const set = new Set(); events.forEach(e => { try { const { startDay, endDay } = getEventDayBounds(e); const cursor = new Date(startDay); while (cursor <= endDay) { set.add(format(cursor, "yyyy-MM-dd")); cursor.setDate(cursor.getDate() + 1); } } catch { /* skip */ } }); return set; }, [events]); // Compute week numbers for each row (one per 7-day chunk) const weekNumbers = useMemo(() => { if (!showWeekNumbers) return []; const nums: number[] = []; for (let i = 0; i < days.length; i += 7) { // Use the first day of each row to determine the week number nums.push(weekStartsOn === 1 ? getISOWeek(days[i]) : getWeek(days[i], { weekStartsOn: 0 })); } return nums; }, [days, showWeekNumbers, weekStartsOn]); const currentYear = getYear(displayMonth); const currentMonth = getMonth(displayMonth); const decadeStart = Math.floor(currentYear / 10) * 10; const years = Array.from({ length: 12 }, (_, i) => decadeStart - 1 + i); const handlePickMonth = (month: number) => { onChangeMonth(setMonth(displayMonth, month)); setPickerView("days"); }; const handlePickYear = (year: number) => { onChangeMonth(setYear(displayMonth, year)); setPickerView("months"); }; const handlePrev = () => { if (pickerView === "days") onChangeMonth(subMonths(displayMonth, 1)); else if (pickerView === "months") onChangeMonth(subYears(displayMonth, 1)); else onChangeMonth(setYear(displayMonth, decadeStart - 10)); }; const handleNext = () => { if (pickerView === "days") onChangeMonth(addMonths(displayMonth, 1)); else if (pickerView === "months") onChangeMonth(addYears(displayMonth, 1)); else onChangeMonth(setYear(displayMonth, decadeStart + 10)); }; const handleHeaderClick = () => { if (pickerView === "days") setPickerView("months"); else if (pickerView === "months") setPickerView("years"); }; const headerLabel = pickerView === "days" ? formatMonthYear(displayMonth) : pickerView === "months" ? String(currentYear) : `${decadeStart}\u2013${decadeStart + 9}`; return (
{pickerView === "days" && (
{showWeekNumbers && (
)} {dayHeaderKeys.map((d) => (
{t(`days.${d}`)}
))} {days.map((day, index) => { const inMonth = checkIsSameMonth(day, displayMonth); const selected = checkIsSameDay(day, selectedDate); const today = checkIsToday(day); const hasEvent = eventDates.has(format(day, "yyyy-MM-dd")); const isFirstDayOfRow = index % 7 === 0; return ( {showWeekNumbers && isFirstDayOfRow && (
{weekNumbers[index / 7]}
)}
); })}
)} {pickerView === "months" && (
{monthLabelKeys.map((labelKey, i) => { const isCurrentMonth = i === currentMonth && currentYear === getYear(new Date()); const isSelected = i === getMonth(selectedDate) && currentYear === getYear(selectedDate); return ( ); })}
)} {pickerView === "years" && (
{years.map((year) => { const inDecade = year >= decadeStart && year <= decadeStart + 9; const isCurrentYear = year === getYear(new Date()); const isSelected = year === getYear(selectedDate); return ( ); })}
)}
); }