"use client"; import { useMemo, useRef, useEffect, useCallback } from "react"; import { useTranslations, useFormatter } from "next-intl"; import { format, isToday, isTomorrow, startOfDay } from "date-fns"; import { MapPin, Users } from "lucide-react"; import { cn } from "@/lib/utils"; import { parseDuration, getEventColor } from "./event-card"; import { getEventDayBounds, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils"; import { getParticipantCount } from "@/lib/calendar-participants"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; interface CalendarAgendaViewProps { 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; timeFormat?: "12h" | "24h"; } interface DayGroup { date: Date; dateKey: string; events: CalendarEvent[]; } export function CalendarAgendaView({ selectedDate, events, calendars, onSelectEvent, onHoverEvent, onHoverLeave, onContextMenuEvent, timeFormat = "24h", }: CalendarAgendaViewProps) { const t = useTranslations("calendar"); const intlFormatter = useFormatter(); const calendarMap = useMemo(() => { const map = new Map(); calendars.forEach((c) => map.set(c.id, c)); return map; }, [calendars]); const todayRef = useRef(null); const scrollContainerRef = useRef(null); const grouped = useMemo(() => { const sorted = [...events].sort((a, b) => getEventStartDate(a).getTime() - getEventStartDate(b).getTime() ); const groups: DayGroup[] = []; const groupMap = new Map(); sorted.forEach((ev) => { try { const { startDay, endDay } = getEventDayBounds(ev); const cursor = new Date(startDay); while (cursor <= endDay) { const key = format(cursor, "yyyy-MM-dd"); let group = groupMap.get(key); if (!group) { group = { date: new Date(cursor), dateKey: key, events: [] }; groupMap.set(key, group); groups.push(group); } group.events.push(ev); cursor.setDate(cursor.getDate() + 1); } } catch { /* skip invalid dates */ } }); // Always include today's date in the groups so the view has a "Today" anchor const todayKey = format(new Date(), "yyyy-MM-dd"); if (!groupMap.has(todayKey)) { const todayGroup = { date: startOfDay(new Date()), dateKey: todayKey, events: [] as CalendarEvent[] }; groupMap.set(todayKey, todayGroup); groups.push(todayGroup); } groups.sort((a, b) => a.date.getTime() - b.date.getTime()); return groups; }, [events]); // Auto-scroll to today's section on mount and when selectedDate changes to today const scrollToToday = useCallback(() => { if (todayRef.current) { todayRef.current.scrollIntoView({ block: "start" }); } }, []); useEffect(() => { // Scroll to today on mount const frame = requestAnimationFrame(scrollToToday); return () => cancelAnimationFrame(frame); }, [scrollToToday]); useEffect(() => { // Scroll to today when selectedDate changes to today if (isToday(selectedDate)) { scrollToToday(); } }, [selectedDate, scrollToToday]); const formatDateHeader = (date: Date): string => { if (isToday(date)) return t("events.today_header"); if (isTomorrow(date)) return t("events.tomorrow_header"); return intlFormatter.dateTime(date, { weekday: "long", month: "long", day: "numeric" }); }; const formatTime = (date: Date): string => { if (timeFormat === "12h") { return intlFormatter.dateTime(date, { hour: "numeric", minute: "2-digit", hour12: true }); } return format(date, "HH:mm"); }; return (
{grouped.map((group) => (
{formatDateHeader(group.date)} {intlFormatter.dateTime(group.date, { month: "short", day: "numeric", year: "numeric" })}
{group.events.length === 0 ? (
{t("events.no_events")}
) : (
{group.events.map((ev) => { const calId = getPrimaryCalendarId(ev); const calendar = calId ? calendarMap.get(calId) : undefined; const color = getEventColor(ev, calendar); const start = getEventStartDate(ev); const durMin = parseDuration(ev.duration); const end = getEventEndDate(ev); const locationName = ev.locations ? Object.values(ev.locations)[0]?.name : null; return ( ); })}
)}
))}
); }