"use client"; import { useState, useMemo, Fragment } from "react"; import { useTranslations, useFormatter } from "next-intl"; import { ChevronLeft, ChevronRight, ChevronDown } from "lucide-react"; import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, addMonths, subMonths, addYears, subYears, setMonth, setYear, eachDayOfInterval, getMonth, getYear, getISOWeek, getWeek, isSameDay, isSameMonth, isToday, format, } from "date-fns"; import { cn } from "@/lib/utils"; import { getEventDayBounds } from "@/lib/calendar-utils"; import type { CalendarEvent } from "@/lib/jmap/types"; type PickerView = "days" | "months" | "years"; const MONTH_LABELS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; 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 intlFormatter = useFormatter(); const weekStart = (firstDayOfWeek === 0 ? 0 : 1) as 0 | 1; const [pickerView, setPickerView] = useState("days"); const days = useMemo(() => { const monthStart = startOfMonth(displayMonth); const monthEnd = endOfMonth(displayMonth); const gridStart = startOfWeek(monthStart, { weekStartsOn: weekStart }); const gridEnd = endOfWeek(monthEnd, { weekStartsOn: weekStart }); return eachDayOfInterval({ start: gridStart, end: gridEnd }); }, [displayMonth, weekStart]); 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]); const dayHeaders = firstDayOfWeek === 0 ? ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const : ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const; // 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(weekStart === 1 ? getISOWeek(days[i]) : getWeek(days[i], { weekStartsOn: 0 })); } return nums; }, [days, showWeekNumbers, weekStart]); 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" ? intlFormatter.dateTime(displayMonth, { month: "long", year: "numeric" }) : pickerView === "months" ? String(currentYear) : `${decadeStart}\u2013${decadeStart + 9}`; return (
{pickerView === "days" && (
{showWeekNumbers && (
)} {dayHeaders.map((d) => (
{t(`days.${d}`)}
))} {days.map((day, index) => { const inMonth = isSameMonth(day, displayMonth); const selected = isSameDay(day, selectedDate); const today = isToday(day); const hasEvent = eventDates.has(format(day, "yyyy-MM-dd")); const isFirstDayOfRow = index % 7 === 0; return ( {showWeekNumbers && isFirstDayOfRow && (
{weekNumbers[index / 7]}
)}
); })}
)} {pickerView === "months" && (
{MONTH_LABELS.map((label, 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 ( ); })}
)}
); }