"use client"; import { useState, useMemo, useCallback, useEffect } from "react"; import { useTranslations } from "next-intl"; import { useRouter } from "@/i18n/navigation"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, format, isToday, isSameDay, addMonths, subMonths, isSameMonth, } from "date-fns"; import { cn } from "@/lib/utils"; import { useSettingsStore } from "@/stores/settings-store"; import { useCalendarStore } from "@/stores/calendar-store"; import { useAuthStore } from "@/stores/auth-store"; import { getEventDayBounds } from "@/lib/calendar-utils"; interface MiniCalendarDashletProps { events?: { date: string; color?: string }[]; onDayClick?: (date: Date) => void; selectedDate?: Date; } const ALL_DAY_KEYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const; export function MiniCalendarDashlet({ events: propEvents, onDayClick, selectedDate: propSelectedDate, }: MiniCalendarDashletProps) { const t = useTranslations("calendar"); const router = useRouter(); const firstDayOfWeek = useSettingsStore((s) => s.firstDayOfWeek); const storeSelectedDate = useCalendarStore((s) => s.selectedDate); const storeEvents = useCalendarStore((s) => s.events); const selectedDate = propSelectedDate ?? storeSelectedDate; const client = useAuthStore((s) => s.client); const [displayMonth, setDisplayMonth] = useState(() => new Date()); const weekStartsOn = useMemo(() => { if (firstDayOfWeek === 0) return 0 as const; if (firstDayOfWeek === 6) return 6 as const; return 1 as const; }, [firstDayOfWeek]); useEffect(() => { if (!client) return; const start = format(startOfMonth(displayMonth), "yyyy-MM-dd'T'00:00:00"); const end = format(endOfMonth(displayMonth), "yyyy-MM-dd'T'23:59:59"); const { dateRange } = useCalendarStore.getState(); if (dateRange?.start === start && dateRange?.end === end) return; // Imperative fetch via getState() is intentional: we only need to // trigger a data fetch, not react to its completion directly within // this component. The store handles loading / error states internally. useCalendarStore.getState().fetchEvents(client, start, end); }, [displayMonth, client]); const days = useMemo(() => { const monthStart = startOfMonth(displayMonth); const monthEnd = endOfMonth(displayMonth); const calStart = startOfWeek(monthStart, { weekStartsOn }); const calEnd = endOfWeek(monthEnd, { weekStartsOn }); return eachDayOfInterval({ start: calStart, end: calEnd }); }, [displayMonth, weekStartsOn]); const eventDates = useMemo(() => { const set = new Set(); for (const e of storeEvents) { 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 */ } } if (propEvents) { for (const e of propEvents) { set.add(e.date); } } return set; }, [storeEvents, propEvents]); const dayHeaders = useMemo( () => [...ALL_DAY_KEYS.slice(weekStartsOn), ...ALL_DAY_KEYS.slice(0, weekStartsOn)], [weekStartsOn], ); const handlePrevMonth = useCallback(() => { setDisplayMonth((prev) => subMonths(prev, 1)); }, []); const handleNextMonth = useCallback(() => { setDisplayMonth((prev) => addMonths(prev, 1)); }, []); const handleGoToToday = useCallback(() => { setDisplayMonth(new Date()); }, []); const handleDayClick = useCallback( (day: Date) => { useCalendarStore.getState().setSelectedDate(day); if (onDayClick) { onDayClick(day); } else { router.push("/calendar"); } }, [onDayClick, router], ); return (
{dayHeaders.map((dh) => (
{t(`days.${dh}`)}
))}
{days.map((day) => { const inMonth = isSameMonth(day, displayMonth); const selected = isSameDay(day, selectedDate); const today = isToday(day); const dateStr = format(day, "yyyy-MM-dd"); const hasEvent = eventDates.has(dateStr); const dotColor = propEvents?.find((e) => e.date === dateStr && e.color)?.color ?? undefined; return ( ); })}
); }