"use client"; import { useMemo, useState, useCallback, type DragEvent } from "react"; import { useTranslations, useFormatter } from "next-intl"; import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, isSameDay, isSameMonth, isToday, format, parseISO, } from "date-fns"; import { cn } from "@/lib/utils"; import { EventCard } from "./event-card"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; import { useAuthStore } from "@/stores/auth-store"; import { useCalendarStore } from "@/stores/calendar-store"; import { toast } from "@/stores/toast-store"; interface CalendarMonthViewProps { selectedDate: Date; events: CalendarEvent[]; calendars: Calendar[]; onSelectDate: (date: Date) => void; onSelectEvent: (event: CalendarEvent) => void; firstDayOfWeek?: number; } function getEventEndDate(event: CalendarEvent): Date { const start = new Date(event.start); if (!event.duration) return start; const days = parseInt(event.duration.match(/(\d+)D/)?.[1] || "0"); const hours = parseInt(event.duration.match(/(\d+)H/)?.[1] || "0"); const minutes = parseInt(event.duration.match(/(\d+)M/)?.[1] || "0"); const weeks = parseInt(event.duration.match(/(\d+)W/)?.[1] || "0"); const totalMs = ((weeks * 7 + days) * 24 * 60 + hours * 60 + minutes) * 60000; return new Date(start.getTime() + totalMs); } export function CalendarMonthView({ selectedDate, events, calendars, onSelectDate, onSelectEvent, firstDayOfWeek = 1, }: CalendarMonthViewProps) { const t = useTranslations("calendar"); const intlFormatter = useFormatter(); const weekStart = (firstDayOfWeek === 0 ? 0 : 1) as 0 | 1; const days = useMemo(() => { const monthStart = startOfMonth(selectedDate); const monthEnd = endOfMonth(selectedDate); const gridStart = startOfWeek(monthStart, { weekStartsOn: weekStart }); const gridEnd = endOfWeek(monthEnd, { weekStartsOn: weekStart }); return eachDayOfInterval({ start: gridStart, end: gridEnd }); }, [selectedDate, weekStart]); const calendarMap = useMemo(() => { const map = new Map(); calendars.forEach((c) => map.set(c.id, c)); return map; }, [calendars]); const eventsByDate = useMemo(() => { const map = new Map(); events.forEach((e) => { try { const start = new Date(e.start); const end = getEventEndDate(e); const startDay = new Date(start); startDay.setHours(0, 0, 0, 0); const endDay = new Date(end); endDay.setHours(0, 0, 0, 0); const cursor = new Date(startDay); while (cursor <= endDay) { const key = format(cursor, "yyyy-MM-dd"); const arr = map.get(key) || []; arr.push(e); map.set(key, arr); cursor.setDate(cursor.getDate() + 1); } } catch { /* skip invalid dates */ } }); return map; }, [events]); const dayHeaders = firstDayOfWeek === 0 ? ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const : ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const; const weeks = useMemo(() => { const result: Date[][] = []; for (let i = 0; i < days.length; i += 7) { result.push(days.slice(i, i + 7)); } return result; }, [days]); const [dropDayKey, setDropDayKey] = useState(null); const handleCellDragOver = useCallback((e: DragEvent, dayKey: string) => { if (!e.dataTransfer.types.includes("application/x-calendar-event")) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDropDayKey((prev) => prev === dayKey ? prev : dayKey); }, []); const handleCellDragLeave = useCallback((e: DragEvent) => { const related = e.relatedTarget as Node | null; if (!e.currentTarget.contains(related)) setDropDayKey(null); }, []); const handleCellDrop = useCallback(async (e: DragEvent, day: Date) => { e.preventDefault(); setDropDayKey(null); const json = e.dataTransfer.getData("application/x-calendar-event"); if (!json) return; try { const data = JSON.parse(json); const originalStart = parseISO(data.originalStart); const newStart = new Date(day); newStart.setHours(originalStart.getHours(), originalStart.getMinutes(), originalStart.getSeconds(), 0); const newStartISO = format(newStart, "yyyy-MM-dd'T'HH:mm:ss"); if (newStartISO === data.originalStart) return; const client = useAuthStore.getState().client; if (!client) return; await useCalendarStore.getState().updateEvent(client, data.eventId, { start: newStartISO }); } catch { toast.error(t("notifications.event_move_error")); } }, [t]); return (
{dayHeaders.map((d) => (
{t(`days.${d}`)}
))}
{weeks.map((week, wi) => (
{week.map((day) => { const inMonth = isSameMonth(day, selectedDate); const selected = isSameDay(day, selectedDate); const today = isToday(day); const key = format(day, "yyyy-MM-dd"); const dayEvents = eventsByDate.get(key) || []; const maxVisible = 3; const fullDateLabel = intlFormatter.dateTime(day, { weekday: "long", month: "long", day: "numeric", year: "numeric" }); return (
onSelectDate(day)} onDragOver={(e) => handleCellDragOver(e, key)} onDragLeave={handleCellDragLeave} onDrop={(e) => handleCellDrop(e, day)} className={cn( "border-r border-border last:border-r-0 p-1 cursor-pointer transition-colors", !inMonth && "bg-muted/30", "hover:bg-muted/50", dropDayKey === key && "ring-2 ring-inset ring-primary bg-primary/10" )} >
{format(day, "d")}
{dayEvents.slice(0, maxVisible).map((ev) => { const calId = Object.keys(ev.calendarIds)[0]; return ( onSelectEvent(ev)} draggable /> ); })} {dayEvents.length > maxVisible && (
{t("events.more", { count: dayEvents.length - maxVisible })}
)}
); })}
))}
); }