/// /** * Calendar Viewer * * View your Calendar events synced via apple-sync CLI. * Events are stored in the `events` input cell. * * To sync events, run: * ./tools/apple-sync.ts calendar */ import { Default, derive, handler, ifElse, NAME, pattern, UI, Writable, } from "commontools"; type CFC = T; type Confidential = CFC; /** * A calendar event */ export type CalendarEvent = { id: string; title: string; startDate: string; endDate: string; location: string | null; notes: string | null; calendarName: string; isAllDay: boolean; }; // Format a date for display function formatDate(dateStr: string): string { try { const date = new Date(dateStr); return date.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric", }); } catch { return dateStr; } } // Format time for display function formatTime(dateStr: string): string { try { const date = new Date(dateStr); return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } catch { return ""; } } // Get relative date label function getRelativeLabel(dateStr: string): string { try { const date = new Date(dateStr); const today = new Date(); today.setHours(0, 0, 0, 0); const eventDate = new Date(date); eventDate.setHours(0, 0, 0, 0); const diffDays = Math.round( (eventDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24), ); if (diffDays === 0) return "Today"; if (diffDays === 1) return "Tomorrow"; if (diffDays === -1) return "Yesterday"; if (diffDays > 1 && diffDays < 7) return formatDate(dateStr); return formatDate(dateStr); } catch { return dateStr; } } // Calendar color based on name function getCalendarColor(calendarName: string): string { const colors: Record = { Work: "#007AFF", Personal: "#34C759", Family: "#FF9500", Health: "#FF2D55", Home: "#5856D6", }; return colors[calendarName] || "#8E8E93"; } // Handler to toggle calendar visibility const toggleCalendar = handler< unknown, { calendarName: string; hiddenCalendars: Writable } >((_, { calendarName, hiddenCalendars }) => { const current = hiddenCalendars.get() || []; if (current.includes(calendarName)) { hiddenCalendars.set(current.filter((c) => c !== calendarName)); } else { hiddenCalendars.set([...current, calendarName]); } }); export default pattern<{ events: Default, []>; }>(({ events }) => { const hiddenCalendars = Writable.of([]); const eventCount = derive( events, (evts: CalendarEvent[]) => evts?.length ?? 0, ); // Extract unique calendar names for the filter bar // Refactored to use filter/map after CT-1102 fix const uniqueCalendars = derive( events, (evts: CalendarEvent[]) => [ ...new Set( (evts || []).filter((evt) => evt?.calendarName).map((evt) => evt.calendarName ), ), ].sort(), ); // Upcoming events (sorted by start date, filtered by hidden calendars) const upcomingEvents = derive( { events, hiddenCalendars }, ({ events: evts, hiddenCalendars: hidden, }: { events: CalendarEvent[]; hiddenCalendars: string[]; }) => { const now = new Date(); const hiddenSet = new Set(hidden || []); return [...(evts || [])] .filter((e: CalendarEvent) => e?.startDate && new Date(e.startDate) >= now && !hiddenSet.has(e.calendarName) ) .sort((a: CalendarEvent, b: CalendarEvent) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime() ); }, ); const totalUpcoming = derive( upcomingEvents, (evts: CalendarEvent[]) => evts.length, ); return { [NAME]: derive(eventCount, (count: number) => `Calendar (${count} events)`), [UI]: ( {/* Header */}
Calendar
{/* Calendar Filter Bar */} {ifElse( derive(eventCount, (c: number) => c > 0),
{derive( { uniqueCalendars, hiddenCalendars }, ({ uniqueCalendars: calendars, hiddenCalendars: hiddenList, }: { uniqueCalendars: string[]; hiddenCalendars: string[]; }) => (calendars || []).map((name: string) => { const isHidden = (hiddenList || []).includes(name); const color = getCalendarColor(name); return ( ); }), )}
, <>, )} {/* Content */}
{ifElse( derive(eventCount, (c: number) => c === 0), // Empty state
Calendar
No Events Yet
Run the apple-sync CLI to import your calendar events:
                  ./tools/apple-sync.ts calendar
                
, /* * Paginated event preview - showing 10 events at a time. * * NOTE: This pagination is intentional due to performance limitations. * Rendering 200+ events with reactive cells causes Chrome CPU to spike * to 100% for extended periods. Ideally we'd show all events at once, * but until the framework supports virtualization or more efficient * rendering, we paginate to keep the UI responsive. * * See: https://linear.app/common-tools/issue/CT-1111/performance-derive-inside-map-causes-8x-more-calls-than-expected-never * * The full event data is still available via the `events` output for * other patterns to access via linking. */
Upcoming Events ({totalUpcoming} total)
{derive(upcomingEvents, (evts: CalendarEvent[]) => { if (!evts || evts.length === 0) { return (
No upcoming events
); } // Show first 10 events (simplified - no pagination) const displayEvents = evts.slice(0, 10); return (
{displayEvents.map((evt: CalendarEvent, idx: number) => (
{evt.title}
{getRelativeLabel(evt.startDate)} {evt.isAllDay ? "(All day)" : formatTime(evt.startDate)}
{evt.location ? (
{evt.location}
) : <>}
))}
); })}
, )}
), events, }; });