"use client"; import { useMemo, useEffect, useRef, useState } from "react"; import { useTranslations, useFormatter } from "next-intl"; import { format, isSameDay, isToday, parseISO } from "date-fns"; import { cn } from "@/lib/utils"; import { Check } from "lucide-react"; import { EventCard } from "./event-card"; import { QuickEventInput } from "./quick-event-input"; import { formatSnapTime, getEventDayBounds, getPrimaryCalendarId, isTimedEventFullDayOnDate, layoutOverlappingEvents } from "@/lib/calendar-utils"; import type { CalendarEvent, Calendar, CalendarTask } from "@/lib/jmap/types"; import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions"; import type { PendingEventPreview } from "./event-modal"; interface CalendarDayViewProps { selectedDate: Date; events: CalendarEvent[]; calendars: Calendar[]; onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void; onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void; onHoverLeave?: () => void; onContextMenuEvent?: (e: React.MouseEvent, event: CalendarEvent) => void; onContextMenuEmpty?: (e: React.MouseEvent, date: Date, hour?: number, allDayArea?: boolean) => void; onCreateAtTime: (date: Date, endDate?: Date) => void; timeFormat?: "12h" | "24h"; isMobile?: boolean; pendingPreview?: PendingEventPreview | null; tasks?: CalendarTask[]; onToggleTaskComplete?: (task: CalendarTask) => void; } const HOUR_HEIGHT = 64; const HOURS = Array.from({ length: 24 }, (_, i) => i); export function CalendarDayView({ selectedDate, events, calendars, onSelectEvent, onHoverEvent, onHoverLeave, onContextMenuEvent, onContextMenuEmpty, onCreateAtTime, timeFormat = "24h", isMobile, pendingPreview, tasks, onToggleTaskComplete, }: CalendarDayViewProps) { const t = useTranslations("calendar"); const intlFormatter = useFormatter(); const scrollRef = useRef(null); const dayKey = format(selectedDate, "yyyy-MM-dd"); const calendarMap = useMemo(() => { const map = new Map(); calendars.forEach((c) => map.set(c.id, c)); return map; }, [calendars]); const { timedEvents, allDayEvents } = useMemo(() => { const timed: CalendarEvent[] = []; const allDay: CalendarEvent[] = []; events.forEach((ev) => { try { const { startDay, endDay } = getEventDayBounds(ev); const selDay = new Date(selectedDate); selDay.setHours(0, 0, 0, 0); const spansThisDay = startDay.getTime() <= selDay.getTime() && endDay.getTime() >= selDay.getTime(); if (!spansThisDay) return; if (ev.showWithoutTime || isTimedEventFullDayOnDate(ev, selectedDate)) allDay.push(ev); else timed.push(ev); } catch { /* skip invalid dates */ } }); return { timedEvents: timed, allDayEvents: allDay }; }, [events, selectedDate]); const dayTasks = useMemo(() => { if (!tasks?.length) return []; return tasks.filter(task => { if (!task.due) return false; try { return isSameDay(parseISO(task.due), selectedDate); } catch { return false; } }); }, [tasks, selectedDate]); useEffect(() => { if (scrollRef.current) { const now = new Date(); scrollRef.current.scrollTop = Math.max(0, (now.getHours() - 1) * HOUR_HEIGHT); } }, []); const today = isToday(selectedDate); const [nowMinutes, setNowMinutes] = useState(() => { const now = new Date(); return now.getHours() * 60 + now.getMinutes(); }); useEffect(() => { const interval = setInterval(() => { setNowMinutes(new Date().getHours() * 60 + new Date().getMinutes()); }, 60000); return () => clearInterval(interval); }, []); const { dragCreate, handleGridPointerDown, handleGridPointerMove, handleGridPointerUp, resizeVisual, handleResizePointerDown, handleResizePointerMove, handleResizePointerUp, quickCreate, handleSlotClick, handleSlotDoubleClick, handleQuickCreateSubmit, handleQuickCreateCancel, dropTarget, handleColumnDragOver, handleColumnDragLeave, handleColumnDrop, } = useTimeGridInteractions({ hourHeight: HOUR_HEIGHT, calendars, onCreateRange: onCreateAtTime, errorMessages: { resize: t("notifications.event_resize_error"), move: t("notifications.event_move_error"), created: t("notifications.event_created"), error: t("notifications.event_error"), }, isMobile, }); const formatHour = (h: number): string => { if (timeFormat === "12h") { const d = new Date(2000, 0, 1, h); return intlFormatter.dateTime(d, { hour: "numeric", minute: "2-digit", hour12: true }); } return format(new Date(2000, 0, 1, h), "HH:mm"); }; const layouted = useMemo(() => layoutOverlappingEvents(timedEvents, selectedDate), [timedEvents, selectedDate]); return (

{isMobile ? intlFormatter.dateTime(selectedDate, { weekday: "short", month: "short", day: "numeric" }) : intlFormatter.dateTime(selectedDate, { weekday: "long", month: "long", day: "numeric", year: "numeric" }) }

{(allDayEvents.length > 0 || dayTasks.length > 0) && (
{ if ((e.target as HTMLElement).closest("[data-calendar-event],button")) return; onContextMenuEmpty(e, selectedDate, undefined, true); } : undefined} > {allDayEvents.length > 0 && ( <>
{t("events.all_day")}
{allDayEvents.map((ev) => { const calId = getPrimaryCalendarId(ev); return ( onSelectEvent(ev, rect)} onMouseEnter={(rect) => onHoverEvent?.(ev, rect)} onMouseLeave={onHoverLeave} onContextMenu={onContextMenuEvent} /> ); })}
)} {dayTasks.length > 0 && ( <>
0 && "mt-2")}>{t("tasks.label")}
{dayTasks.map((task) => { const isCompleted = task.progress === "completed"; const cal = calendars.find(c => task.calendarIds[c.id]); const color = cal?.color || "#3b82f6"; return (
{task.title || t("tasks.no_title")}
); })}
)}
)}
{HOURS.map((h) => (
{h > 0 && ( {formatHour(h)} )}
))}
handleGridPointerDown(e, dayKey, selectedDate)} onPointerMove={handleGridPointerMove} onPointerUp={handleGridPointerUp} onDragOver={(e) => handleColumnDragOver(e, dayKey)} onDragLeave={handleColumnDragLeave} onDrop={(e) => handleColumnDrop(e, selectedDate)} > {HOURS.map((h) => (
handleSlotClick(selectedDate, h)} onDoubleClick={() => handleSlotDoubleClick(selectedDate, h)} onContextMenu={onContextMenuEmpty ? (e) => onContextMenuEmpty(e, selectedDate, h, false) : undefined} className="border-b border-border/50 hover:bg-muted/30 cursor-pointer transition-colors" style={{ height: HOUR_HEIGHT }} /> ))} {layouted.map(({ event: ev, column, totalColumns, startMinutes, endMinutes }) => { const durMin = Math.max(15, endMinutes - startMinutes); const baseTop = (startMinutes / 60) * HOUR_HEIGHT; const baseHeight = Math.max(24, (durMin / 60) * HOUR_HEIGHT); const isResizing = resizeVisual?.eventId === ev.id; const top = isResizing ? resizeVisual!.topPx : baseTop; const height = isResizing ? resizeVisual!.heightPx : baseHeight; const calId = getPrimaryCalendarId(ev); const leftPct = (column / totalColumns) * 100; const widthPct = (1 / totalColumns) * 100; return (
onSelectEvent(ev, rect)} onMouseEnter={(rect) => onHoverEvent?.(ev, rect)} onMouseLeave={onHoverLeave} onContextMenu={onContextMenuEvent} draggable />
handleResizePointerDown(ev.id, "top", startMinutes, durMin, e)} onPointerMove={handleResizePointerMove} onPointerUp={handleResizePointerUp} >
handleResizePointerDown(ev.id, "bottom", startMinutes, durMin, e)} onPointerMove={handleResizePointerMove} onPointerUp={handleResizePointerUp} >
); })} {today && (
)} {quickCreate?.dayKey === dayKey && ( )} {dragCreate && (
{formatSnapTime(dragCreate.startMinutes, timeFormat)} – {formatSnapTime(dragCreate.endMinutes, timeFormat)}
)} {dropTarget?.dayKey === dayKey && (
{formatSnapTime(dropTarget.minutes, timeFormat)}
)} {pendingPreview && !pendingPreview.allDay && isSameDay(pendingPreview.start, selectedDate) && ( (() => { const startMin = pendingPreview.start.getHours() * 60 + pendingPreview.start.getMinutes(); const endMin = pendingPreview.end.getHours() * 60 + pendingPreview.end.getMinutes(); const durationMin = Math.max(15, endMin - startMin); const cal = calendars.find(c => c.id === pendingPreview.calendarId); const color = cal?.color || "hsl(var(--primary))"; return (
{pendingPreview.title}
{formatSnapTime(startMin, timeFormat)} – {formatSnapTime(startMin + durationMin, timeFormat)}
); })() )}
); }