import type { CalendarEvent } from './shared'; import { useState } from 'react'; import { CalendarDays, MapPin, Repeat, Pencil, Trash2, Check, X } from 'lucide-react'; import { useCalendarEvents, useSelectedCollection } from './useDavData'; import { useDavEdit, setProperty } from './useDavEdit'; import { useTranslation } from 'officerdev'; // Right panel of /calendar. Deliberately a grouped agenda list, not a month grid: the sidecar returns // events as they are stored, with RRULE unexpanded, so a grid would have to invent occurrences the server // never claimed existed. A list can show a repeating event honestly, as one entry with a "repeats" badge. const dayKey = (ev: CalendarEvent) => (ev.start ? ev.start.slice(0, 10) : ''); const formatDay = (iso: string) => { if (!iso) return 'No date'; const date = new Date(`${iso}T12:00:00`); if (Number.isNaN(date.getTime())) return iso; return date.toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); }; const formatTime = (ev: CalendarEvent) => { if (ev.allDay || !ev.start) return 'All day'; const start = new Date(ev.start); if (Number.isNaN(start.getTime())) return ''; const opts: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }; const end = ev.end ? new Date(ev.end) : null; if (!end || Number.isNaN(end.getTime())) return start.toLocaleTimeString(undefined, opts); return `${start.toLocaleTimeString(undefined, opts)} – ${end.toLocaleTimeString(undefined, opts)}`; }; export const CalendarView = () => { const { t } = useTranslation(); const { selected, isLoading: loadingCollections } = useSelectedCollection('calendar'); const { events, isLoading, error } = useCalendarEvents(selected?.path ?? null); // Keyed on the COLLECTION, so a save refreshes the list this event came from. const edit = useDavEdit(['caldav', 'events', selected?.path ?? null]); const [editing, setEditing] = useState(null); const [draft, setDraft] = useState({ summary: '', location: '' }); const [confirming, setConfirming] = useState(null); if (loadingCollections) return ; if (!selected) return ; if (error) return ; if (isLoading) return ; if (events.length === 0) return ; const sorted = [...events].sort((a, b) => (a.start ?? '').localeCompare(b.start ?? '')); const days: Array<{ key: string; events: CalendarEvent[] }> = []; for (const ev of sorted) { const key = dayKey(ev); const last = days[days.length - 1]; if (last && last.key === key) last.events.push(ev); else days.push({ key, events: [ev] }); } return (
{days.map((day) => (

{formatDay(day.key)}

{day.events.map((ev) => (
{ev.summary || '(no title)'} {ev.rrule && ( repeats )}
{formatTime(ev)}
{/* The editor is the title and the place, and nothing else. Those are the two fields a person actually corrects; times and recurrence are where an editor does damage, and a half-editor that silently rewrites an RRULE is worse than none. */} {editing === ev.href && (
{ e.preventDefault(); if (!ev.href) return; void edit .save(ev.href, (raw) => setProperty(setProperty(raw, 'SUMMARY', draft.summary), 'LOCATION', draft.location || null), ) .then(() => setEditing(null)); }} > setDraft((d) => ({ ...d, summary: e.target.value }))} placeholder="Title" className="rounded-md border bg-background px-2 py-1 text-sm" /> setDraft((d) => ({ ...d, location: e.target.value }))} placeholder="Where" className="rounded-md border bg-background px-2 py-1 text-xs" />
)} {ev.location && (
{ev.location}
)} {ev.description && (

{ev.description}

)}
{/* Only for an event we can actually address. An item with no href came from somewhere that did not give us one, and offering a delete that cannot work is worse than not offering it. */} {ev.href && editing !== ev.href && (
{confirming === ev.href ? ( <> ) : ( <> )}
)}
))}
))}
); }; const Placeholder = ({ text }: { text: string }) => (
{text}
);