"use client"; import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { X, Trash2, Check, Users, CalendarDays } from "lucide-react"; import { format, parseISO, addHours } from "date-fns"; import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types"; import { parseDuration } from "./event-card"; import { ParticipantInput } from "./participant-input"; import { isOrganizer, getUserParticipantId, getUserStatus, getParticipantList, getStatusCounts, buildParticipantMap, } from "@/lib/calendar-participants"; interface EventModalProps { event?: CalendarEvent | null; calendars: Calendar[]; defaultDate?: Date; onSave: (data: Partial, sendSchedulingMessages?: boolean) => void; onDelete?: (id: string, sendSchedulingMessages?: boolean) => void; onRsvp?: (eventId: string, participantId: string, status: CalendarParticipant['participationStatus']) => void; onClose: () => void; currentUserEmails?: string[]; } function formatDateInput(d: Date): string { return format(d, "yyyy-MM-dd"); } function formatTimeInput(d: Date): string { return format(d, "HH:mm"); } function buildDuration(startDate: Date, endDate: Date): string { const diffMs = endDate.getTime() - startDate.getTime(); const totalMinutes = Math.max(0, Math.floor(diffMs / 60000)); const days = Math.floor(totalMinutes / (24 * 60)); const hours = Math.floor((totalMinutes % (24 * 60)) / 60); const minutes = totalMinutes % 60; let dur = "P"; if (days > 0) dur += `${days}D`; dur += "T"; if (hours > 0) dur += `${hours}H`; if (minutes > 0) dur += `${minutes}M`; if (dur === "PT") dur = "PT0M"; return dur; } type RecurrenceOption = "none" | "daily" | "weekly" | "monthly" | "yearly"; type AlertOption = "none" | "at_time" | "5" | "15" | "30" | "60" | "1440"; export function EventModal({ event, calendars, defaultDate, onSave, onDelete, onRsvp, onClose, currentUserEmails = [], }: EventModalProps) { const t = useTranslations("calendar"); const isEdit = !!event; const userIsOrganizer = useMemo(() => { if (!event) return true; if (!event.participants) return true; return isOrganizer(event, currentUserEmails); }, [event, currentUserEmails]); const isAttendeeMode = useMemo(() => { if (!event || !event.participants) return false; return !event.isOrigin && !userIsOrganizer; }, [event, userIsOrganizer]); const userParticipantId = useMemo(() => { if (!event) return null; return getUserParticipantId(event, currentUserEmails); }, [event, currentUserEmails]); const userCurrentStatus = useMemo(() => { if (!event) return null; return getUserStatus(event, currentUserEmails); }, [event, currentUserEmails]); const existingParticipants = useMemo(() => { if (!event) return []; return getParticipantList(event); }, [event]); const organizerInfo = useMemo(() => { if (!event?.participants) return null; const organizer = existingParticipants.find(p => p.isOrganizer); return organizer ? { name: organizer.name, email: organizer.email } : null; }, [event, existingParticipants]); const getInitialStart = (): Date => { if (event?.start) return parseISO(event.start); if (defaultDate) { const d = new Date(defaultDate); const now = new Date(); d.setHours(now.getHours() + 1, 0, 0, 0); return d; } const d = new Date(); d.setHours(d.getHours() + 1, 0, 0, 0); return d; }; const getInitialEnd = (): Date => { if (event?.start) { const s = parseISO(event.start); const dur = parseDuration(event.duration); return new Date(s.getTime() + dur * 60000); } return addHours(getInitialStart(), 1); }; const [title, setTitle] = useState(event?.title || ""); const [description, setDescription] = useState(event?.description || ""); const [location, setLocation] = useState( event?.locations ? Object.values(event.locations)[0]?.name || "" : "" ); const [startDate, setStartDate] = useState(formatDateInput(getInitialStart())); const [startTime, setStartTime] = useState(formatTimeInput(getInitialStart())); const [endDate, setEndDate] = useState(formatDateInput(getInitialEnd())); const [endTime, setEndTime] = useState(formatTimeInput(getInitialEnd())); const [allDay, setAllDay] = useState(event?.showWithoutTime || false); const [calendarId, setCalendarId] = useState(() => { if (event?.calendarIds) return Object.keys(event.calendarIds)[0] || calendars[0]?.id || ""; const defaultCal = calendars.find(c => c.isDefault); return defaultCal?.id || calendars[0]?.id || ""; }); const [recurrence, setRecurrence] = useState(() => { if (!event?.recurrenceRules?.length) return "none"; return event.recurrenceRules[0].frequency as RecurrenceOption; }); const [alert, setAlert] = useState(() => { if (!event?.alerts) return "none"; const first = Object.values(event.alerts)[0]; if (!first) return "none"; if (first.trigger["@type"] === "OffsetTrigger") { const offset = first.trigger.offset; if (offset === "PT0S") return "at_time"; const minMatch = offset.match(/-?PT?(\d+)M$/); if (minMatch) return minMatch[1] as AlertOption; const hourMatch = offset.match(/-?PT?(\d+)H$/); if (hourMatch) return String(parseInt(hourMatch[1]) * 60) as AlertOption; const dayMatch = offset.match(/-?P(\d+)D/); if (dayMatch) return String(parseInt(dayMatch[1]) * 1440) as AlertOption; } return "none"; }); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [attendees, setAttendees] = useState<{ name: string; email: string }[]>(() => { if (!event?.participants) return []; return existingParticipants .filter(p => !p.isOrganizer) .map(p => ({ name: p.name, email: p.email })); }); const [sendInvitations, setSendInvitations] = useState(true); const statusCounts = useMemo(() => { if (!event?.participants) return null; return getStatusCounts(event); }, [event]); const handleAddAttendee = useCallback((p: { name: string; email: string }) => { setAttendees(prev => [...prev, p]); }, []); const handleRemoveAttendee = useCallback((email: string) => { setAttendees(prev => prev.filter(a => a.email.toLowerCase() !== email.toLowerCase())); }, []); const handleSave = useCallback(() => { const trimmedTitle = title.trim(); if (!trimmedTitle) return; if (trimmedTitle.length > 500 || description.trim().length > 10000 || location.trim().length > 500) return; const startStr = allDay ? `${startDate}T00:00:00` : `${startDate}T${startTime}:00`; const endStr = allDay ? `${endDate}T23:59:59` : `${endDate}T${endTime}:00`; const start = new Date(startStr); let end = new Date(endStr); if (end <= start) { end = new Date(start.getTime() + 3600000); } const duration = allDay ? `P${Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 86400000))}D` : buildDuration(start, end); const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const data: Partial = { title: trimmedTitle, description: description.trim(), start: startStr, duration, timeZone, showWithoutTime: allDay, calendarIds: { [calendarId]: true }, status: "confirmed", freeBusyStatus: "busy", privacy: "public", }; if (location.trim()) { data.locations = { loc1: { "@type": "Location", name: location.trim(), description: null, locationTypes: null, coordinates: null, timeZone: null, links: null, relativeTo: null, }, }; } if (recurrence !== "none") { data.recurrenceRules = [{ "@type": "RecurrenceRule", frequency: recurrence, interval: 1, rscale: "gregorian", skip: "omit", firstDayOfWeek: "mo", byDay: null, byMonthDay: null, byMonth: null, byYearDay: null, byWeekNo: null, byHour: null, byMinute: null, bySecond: null, bySetPosition: null, count: null, until: null, }]; } if (alert !== "none") { const offset = alert === "at_time" ? "PT0S" : `-PT${alert}M`; data.alerts = { alert1: { "@type": "Alert", trigger: { "@type": "OffsetTrigger", offset, relativeTo: "start" }, action: "display", acknowledged: null, relatedTo: null, }, }; } if (attendees.length > 0 && currentUserEmails.length > 0) { const organizerEmail = currentUserEmails[0]; const organizerName = existingParticipants.find(p => p.isOrganizer)?.name || ""; data.participants = buildParticipantMap( { name: organizerName, email: organizerEmail }, attendees ) as Record; } else if (attendees.length === 0 && event?.participants) { data.participants = null; } const shouldSendScheduling = attendees.length > 0 && sendInvitations; onSave(data, shouldSendScheduling); }, [title, description, location, startDate, startTime, endDate, endTime, allDay, calendarId, recurrence, alert, attendees, sendInvitations, currentUserEmails, existingParticipants, event, onSave]); const handleRsvp = useCallback((status: CalendarParticipant['participationStatus']) => { if (!event || !userParticipantId || !onRsvp) return; onRsvp(event.id, userParticipantId, status); onClose(); }, [event, userParticipantId, onRsvp, onClose]); const modalRef = useRef(null); useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); if (!isAttendeeMode) handleSave(); } }; window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [onClose, handleSave, isAttendeeMode]); useEffect(() => { const modal = modalRef.current; if (!modal) return; const focusableEls = modal.querySelectorAll( 'input, select, textarea, button, [tabindex]:not([tabindex="-1"])' ); const firstEl = focusableEls[0]; const lastEl = focusableEls[focusableEls.length - 1]; const handler = (e: KeyboardEvent) => { if (e.key !== "Tab") return; if (e.shiftKey && document.activeElement === firstEl) { e.preventDefault(); lastEl?.focus(); } else if (!e.shiftKey && document.activeElement === lastEl) { e.preventDefault(); firstEl?.focus(); } }; modal.addEventListener("keydown", handler); firstEl?.focus(); return () => modal.removeEventListener("keydown", handler); }, []); const hasParticipants = attendees.length > 0 || (event?.participants && Object.keys(event.participants).length > 0); if (isAttendeeMode && event) { const startD = parseISO(event.start); const durMin = parseDuration(event.duration); const endD = new Date(startD.getTime() + durMin * 60000); const locationName = event.locations ? Object.values(event.locations)[0]?.name : null; const participants = getParticipantList(event); return (
); } return (