diff --git a/components/calendar/event-card.tsx b/components/calendar/event-card.tsx index 902a6e5c..f797f0ea 100644 --- a/components/calendar/event-card.tsx +++ b/components/calendar/event-card.tsx @@ -78,7 +78,11 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM const calendarName = calendar?.name || ""; const durationMinutes = parseDuration(event.duration); const endTime = getEventEndDate(event); - const timeString = `${format(startDate, timeFmt)} – ${format(endTime, timeFmt)}`; + const safeFormat = (d: Date, fmt: string) => { + if (isNaN(d.getTime())) return "--:--"; + try { return format(d, fmt); } catch { return "--:--"; } + }; + const timeString = `${safeFormat(startDate, timeFmt)} – ${safeFormat(endTime, timeFmt)}`; const ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}`; const handleDragStart = useCallback((e: DragEvent) => { diff --git a/lib/calendar-utils.ts b/lib/calendar-utils.ts index 64179fb3..a6bcac0a 100644 --- a/lib/calendar-utils.ts +++ b/lib/calendar-utils.ts @@ -24,8 +24,14 @@ export interface TimedEventLayout { export function getEventStartDate( event: Pick, ): Date { - const source = !event.showWithoutTime && event.utcStart ? event.utcStart : event.start; - return parseISO(source); + // Prefer utcStart for timed events but fall back to start if utcStart is + // missing or unparseable - a malformed utcStart used to surface as an + // Invalid Date that crashed downstream format() calls (#316). + if (!event.showWithoutTime && event.utcStart) { + const utc = parseISO(event.utcStart); + if (!isNaN(utc.getTime())) return utc; + } + return parseISO(event.start); } export function packWeekSegments(rawSegments: CalendarWeekSegment[]): CalendarWeekSegment[] { @@ -56,7 +62,8 @@ export function packWeekSegments(rawSegments: CalendarWeekSegment[]): CalendarWe export function getEventEndDate(event: CalendarEvent): Date { if (!event.showWithoutTime && event.utcEnd) { - return parseISO(event.utcEnd); + const utc = parseISO(event.utcEnd); + if (!isNaN(utc.getTime())) return utc; } const start = getEventStartDate(event); diff --git a/stores/calendar-store.ts b/stores/calendar-store.ts index 1fb94a40..3d21a554 100644 --- a/stores/calendar-store.ts +++ b/stores/calendar-store.ts @@ -7,6 +7,7 @@ import { normalizeAllDayDuration } from '@/lib/calendar-utils'; import { parseDuration } from '@/components/calendar/event-card'; import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization'; import { expandRecurringEvents } from '@/lib/recurrence-expansion'; +import { parseISO } from 'date-fns'; import { generateUUID } from '@/lib/utils'; import { apiFetch } from '@/lib/browser-navigation'; import { BIRTHDAY_CALENDAR_ID } from '@/lib/birthday-calendar'; @@ -302,8 +303,12 @@ export const useCalendarStore = create()( after: start, before: end, }); - // Filter out malformed events missing required 'start' field - const validEvents = rawEvents.filter(e => typeof e.start === 'string' && e.start); + // Filter out malformed events missing required 'start' field, or + // whose start string fails to parse (would otherwise crash format() + // calls in the rendering path - #316). + const validEvents = rawEvents.filter(e => + typeof e.start === 'string' && e.start && !isNaN(parseISO(e.start).getTime()) + ); const droppedEvents = rawEvents.length - validEvents.length; // Expand recurring events client-side (Stalwart doesn't support // mutations on synthetic IDs from server-side expandRecurrences) @@ -366,7 +371,9 @@ export const useCalendarStore = create()( accounts.map(async ({ client, localAccountId }) => { try { const raw = await client.queryAllCalendarEvents({ after: start, before: end }); - const valid = raw.filter(e => typeof e.start === 'string' && e.start); + const valid = raw.filter(e => + typeof e.start === 'string' && e.start && !isNaN(parseISO(e.start).getTime()) + ); const expanded = expandRecurringEvents(valid, start, end); return prefixEventsWithLocalAccount( expanded,