diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx index 8a84f295..748f0262 100644 --- a/components/calendar/event-modal.tsx +++ b/components/calendar/event-modal.tsx @@ -4,10 +4,10 @@ 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, Copy } from "lucide-react"; +import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Video, Repeat, Bell, AlignLeft } from "lucide-react"; import { format, parseISO, addHours, addDays } from "date-fns"; import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types"; -import { parseDuration } from "./event-card"; +import { parseDuration, getEventColor } from "./event-card"; import { buildAllDayDuration, getEventDisplayEndDate } from "@/lib/calendar-utils"; import { ParticipantInput } from "./participant-input"; import { @@ -59,6 +59,41 @@ function buildDuration(startDate: Date, endDate: Date): string { type RecurrenceOption = "none" | "daily" | "weekly" | "monthly" | "yearly"; type AlertOption = "none" | "at_time" | "5" | "15" | "30" | "60" | "1440"; +function formatDurationDisplay(minutes: number): string { + if (minutes < 60) return `${minutes}min`; + const h = Math.floor(minutes / 60); + const m = minutes % 60; + if (m === 0) return `${h}h`; + return `${h}h${m}min`; +} + +function getAlertLabel(event: CalendarEvent, t: ReturnType): string | null { + if (!event.alerts) return null; + const first = Object.values(event.alerts)[0]; + if (!first || first.trigger["@type"] !== "OffsetTrigger") return null; + const offset = first.trigger.offset; + if (offset === "PT0S") return t("alerts.at_time"); + const minMatch = offset.match(/-?PT?(\d+)M$/); + if (minMatch) return t("alerts.minutes_before", { count: parseInt(minMatch[1]) }); + const hourMatch = offset.match(/-?PT?(\d+)H$/); + if (hourMatch) return t("alerts.hours_before", { count: parseInt(hourMatch[1]) }); + const dayMatch = offset.match(/-?P(\d+)D/); + if (dayMatch) return t("alerts.days_before", { count: parseInt(dayMatch[1]) }); + return null; +} + +function getRecurrenceLabel(event: CalendarEvent, t: ReturnType): string | null { + if (!event.recurrenceRules?.length) return null; + const freq = event.recurrenceRules[0].frequency; + const labels: Record = { + daily: t("recurrence.daily"), + weekly: t("recurrence.weekly"), + monthly: t("recurrence.monthly"), + yearly: t("recurrence.yearly"), + }; + return labels[freq] || null; +} + export function EventModal({ event, calendars, @@ -74,6 +109,7 @@ export function EventModal({ }: EventModalProps) { const t = useTranslations("calendar"); const isEdit = !!event; + const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit"); const userIsOrganizer = useMemo(() => { if (!event) return true; @@ -340,7 +376,13 @@ export function EventModal({ useEffect(() => { const handleKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); + if (e.key === "Escape") { + if (mode === "edit" && isEdit) { + setMode("view"); + } else { + onClose(); + } + } if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); if (!isAttendeeMode) handleSave(); @@ -348,7 +390,7 @@ export function EventModal({ }; window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); - }, [onClose, handleSave, isAttendeeMode]); + }, [onClose, handleSave, isAttendeeMode, mode, isEdit]); useEffect(() => { const modal = modalRef.current; @@ -486,6 +528,175 @@ export function EventModal({ ); } + // View mode: read-only display of event details with Edit button + if (mode === "view" && 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 : null; + const virtualLoc = event.virtualLocations ? Object.values(event.virtualLocations)[0]?.uri || null : null; + const viewParticipants = getParticipantList(event); + const recurrenceLabel = getRecurrenceLabel(event, t); + const alertLabel = getAlertLabel(event, t); + const eventCalendar = calendars.find(c => event.calendarIds[c.id]); + const color = getEventColor(event, eventCalendar); + + return ( +
+ {/* Color accent bar */} +
+ + {/* Header */} +
+
+
+ +

{event.title || t("events.no_title")}

+
+ {eventCalendar && ( +

{eventCalendar.name}

+ )} +
+ +
+ + {/* Content */} +
+
+ {/* Date & Time */} +
+ +
+ + {format(startD, "EEE, MMM d, yyyy")} + + {event.showWithoutTime ? ( + {t("events.all_day")} + ) : ( +
+ {format(startD, "HH:mm")} – {format(endD, "HH:mm")} + ({formatDurationDisplay(durMin)}) +
+ )} +
+
+ + {/* Location */} + {locationName && ( +
+ + {/^https?:\/\//i.test(locationName) ? ( + + {(() => { try { return new URL(locationName).hostname; } catch { return locationName; } })()} + + ) : ( + {locationName} + )} +
+ )} + + {/* Virtual Location */} + {virtualLoc && ( + + )} + + {/* Participants */} + {viewParticipants.length > 0 && ( +
+ +
+ + {t("participants.count", { count: viewParticipants.length })} + +
+ {viewParticipants.map((p) => ( +
+ + {p.name || p.email} + {p.isOrganizer && ( + ({t("participants.organizer").toLowerCase()}) + )} + + +
+ ))} +
+
+
+ )} + + {/* Recurrence */} + {recurrenceLabel && ( +
+ + {recurrenceLabel} +
+ )} + + {/* Reminder */} + {alertLabel && ( +
+ + {alertLabel} +
+ )} + + {/* Description */} + {event.description && ( +
+ +

{event.description}

+
+ )} +
+
+ + {/* Action Bar */} +
+
+ {onDelete && ( + showDeleteConfirm ? ( +
+ {t("form.delete_confirm")} + + +
+ ) : ( + + ) + )} + {onDuplicate && !showDeleteConfirm && ( + + )} +
+ {!showDeleteConfirm && ( + + )} +
+
+ ); + } + return (
@@ -728,7 +939,7 @@ export function EventModal({
-
{/* Content */}