import { Calendar as CalIcon, CalendarDays, Copy, ExternalLink, Palette, Pencil, Plus, Tag, Trash2, X } from "lucide-react"; import { useLocation } from "wouter"; import type { CalendarEvent } from "@/jmap/types"; import { useCalendar, isRecurring, type EventInstance } from "@/store/calendar"; import { useSettings } from "@/store/settings"; import { formatDayMonth } from "@/lib/datetime"; import { MenuItem, MenuSep, MenuTitle, Popover, type Anchor } from "@/ui/popover"; import { CALENDAR_COLORS } from "@/ui/misc"; import { confirmDialog } from "@/ui/dialog"; import { toast } from "@/ui/toast"; import { toLocalDateOnly } from "@/lib/dates"; import { formatTime } from "@/lib/format"; export type CalendarContext = | { kind: "event"; inst: EventInstance; anchor: Anchor } | { kind: "slot"; start: Date; end: Date; allDay: boolean; anchor: Anchor }; interface Props { ctx: CalendarContext; onClose: () => void; onOpen: (inst: EventInstance, anchor: Anchor) => void; onEdit: (inst: EventInstance) => void; onCreate: (start: Date, end: Date, allDay: boolean) => void; } /** Resolve the display colour of an event: explicit colour → category colour → calendar colour. */ export function eventColor(ev: CalendarEvent, calendarColor: string | null | undefined, categories: Array<{ name: string; color: string }>): string { if (ev.color) return ev.color; const cat = categoryOf(ev, categories); if (cat) return cat.color; return calendarColor ?? "var(--accent)"; } export function categoryOf(ev: CalendarEvent, categories: Array<{ name: string; color: string }>): { name: string; color: string } | undefined { const names = Object.keys(ev.categories ?? {}); for (const n of names) { const c = categories.find((x) => x.name.toLowerCase() === n.toLowerCase()); if (c) return c; } return undefined; } export function CalendarContextMenu({ ctx, onClose, onOpen, onEdit, onCreate }: Props) { const cal = useCalendar(); const [, navigate] = useLocation(); const categories = useSettings((s) => s.settings.eventCategories); if (ctx.kind === "slot") { const { start, end, allDay } = ctx; return ( } label={allDay ? `New all-day event on ${formatDayMonth(start)}` : `New event at ${formatTime(start)}`} onClick={() => onCreate(start, end, allDay)} /> {!allDay && } label="New all-day event" onClick={() => { const d = new Date(start); d.setHours(0, 0, 0, 0); onCreate(d, new Date(d.getTime() + 86400000), true); }} />} } label="Go to day" onClick={() => navigate(`/calendar/day/${toLocalDateOnly(start)}`)} /> } label="Go to week" onClick={() => navigate(`/calendar/week/${toLocalDateOnly(start)}`)} /> ); } const { inst } = ctx; const ev = inst.event; const baseId = ev.baseEventId ?? ev.id; const canEdit = inst.calendar?.myRights.mayWriteAll || inst.calendar?.myRights.mayWriteOwn || !inst.calendar; const currentCat = categoryOf(ev, categories); const participants = Object.keys(ev.participants ?? {}).length; const patch = async (p: Record, msg: string) => { try { await cal.updateEvent(baseId, p, false); toast.success(msg); } catch (err) { toast.error((err as Error).message); } }; const setColor = (color: string | null) => void patch({ color }, color ? "Colour updated" : "Colour reset"); const setCategory = (cat: { name: string; color: string } | null) => { const categoriesPatch = cat ? { [cat.name]: true } : null; void patch({ categories: categoriesPatch, color: cat ? cat.color : null }, cat ? `Categorised as ${cat.name}` : "Category cleared"); }; const duplicate = async () => { const { id: _i, baseEventId: _b, uid: _u, utcStart: _s, utcEnd: _e, isOrigin: _o, calendarIds, created: _c, updated: _up, sequence: _sq, recurrenceId: _ri, recurrenceIdTimeZone: _rt, ...rest } = ev as CalendarEvent & Record; try { await cal.createEvent({ ...rest, title: `Copy of ${ev.title ?? "event"}`, participants: undefined, replyTo: undefined, organizerCalendarAddress: undefined } as Partial, Object.keys(calendarIds)[0] ?? Object.keys(cal.calendars)[0]!, false); toast.success("Event duplicated"); } catch (err) { toast.error((err as Error).message); } }; const del = async () => { onClose(); const recurring = isRecurring(ev); if (!(await confirmDialog({ title: recurring ? "Delete all occurrences?" : "Delete this event?", confirmLabel: "Delete", danger: true }))) return; try { await cal.destroyEvent(baseId, participants > 1); toast.success("Event deleted"); } catch (err) { toast.error((err as Error).message); } }; return ( } label="Open" onClick={() => { onClose(); onOpen(inst, ctx.anchor); }} /> {canEdit && } label="Edit…" onClick={() => { onClose(); onEdit(inst); }} />} {canEdit && } label="Duplicate" onClick={() => { onClose(); void duplicate(); }} />} {canEdit && ( <> Category {categories.map((c) => ( {c.name}} checked={currentCat?.name === c.name} onClick={() => { onClose(); setCategory(currentCat?.name === c.name ? null : c); }} /> ))} } label="No category" disabled={!currentCat} onClick={() => { onClose(); setCategory(null); }} /> } label="Manage categories…" onClick={() => { onClose(); navigate("/settings/calendar"); }} /> Colour {CALENDAR_COLORS.map((c) => ( { onClose(); setColor(c); }} /> ))} {ev.color && } label="Use calendar colour" onClick={() => { onClose(); setColor(null); }} />} } label="Delete" onClick={() => void del()} /> > )} ); }