From c5a8ffc08cd4c53c37fe4fb9cde8519a68f1f343 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 22 Jan 2026 13:04:43 +0100 Subject: [PATCH] fix: handle null dates in dashboard and gallery pages Add null checks for expires_at and event_date fields to prevent TypeError when calling parseISO() on null values. This fixes crashes that occurred after making event dates optional. - AdminDashboard: skip events with null expires_at in expiring filter - GalleryPage: handle null expires_at in expiration calculation - GalleryView: make daysUntilExpiration nullable with explicit checks - EventDetailsPage: return null from safeParseDate for null inputs --- .../src/components/gallery/GalleryView.tsx | 12 ++--- frontend/src/pages/GalleryPage.tsx | 12 ++--- frontend/src/pages/admin/AdminDashboard.tsx | 9 ++-- frontend/src/pages/admin/EventDetailsPage.tsx | 45 +++++++++++-------- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 5b8e5090..bd4d95ab 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -310,10 +310,12 @@ export const GalleryView: React.FC = ({ slug, event }) => { } }, [settingsData, data, setTheme]); // Use data instead of event prop - // Calculate days until expiration - const daysUntilExpiration = differenceInDays(parseISO(event.expires_at), new Date()); - const showUrgentWarning = daysUntilExpiration <= 7; - const isExpired = daysUntilExpiration < 0; + // Calculate days until expiration (null means never expires) + const daysUntilExpiration = event.expires_at + ? differenceInDays(parseISO(event.expires_at), new Date()) + : null; + const showUrgentWarning = daysUntilExpiration !== null && daysUntilExpiration <= 7; + const isExpired = daysUntilExpiration !== null && daysUntilExpiration < 0; // Filter and sort photos const filteredPhotos = useMemo(() => { @@ -602,7 +604,7 @@ export const GalleryView: React.FC = ({ slug, event }) => { headerExtra={(() => { const items = []; - if (daysUntilExpiration <= 1 && daysUntilExpiration > 0) { + if (daysUntilExpiration !== null && daysUntilExpiration <= 1 && daysUntilExpiration > 0) { items.push( ); diff --git a/frontend/src/pages/GalleryPage.tsx b/frontend/src/pages/GalleryPage.tsx index 12fd94b3..f4d1fafd 100644 --- a/frontend/src/pages/GalleryPage.tsx +++ b/frontend/src/pages/GalleryPage.tsx @@ -187,8 +187,8 @@ export const GalleryPage: React.FC = () => { } }, [galleryInfo, isAuthenticated, autoLoginAttempted, login, resolvedSlug, isResolvingIdentifier]); - // Calculate days until expiration - const daysUntilExpiration = galleryInfo + // Calculate days until expiration (null if no expiration set) + const daysUntilExpiration = galleryInfo?.expires_at ? differenceInDays(parseISO(galleryInfo.expires_at), new Date()) : null; @@ -394,9 +394,11 @@ export const GalleryPage: React.FC = () => {

{t('gallery.expired')}

-

- {t('gallery.expiredOn', { date: format(parseISO(galleryInfo.expires_at), 'PP') })} -

+ {galleryInfo.expires_at && ( +

+ {t('gallery.expiredOn', { date: format(parseISO(galleryInfo.expires_at), 'PP') })} +

+ )}

{t('gallery.contactOrganizer')}

diff --git a/frontend/src/pages/admin/AdminDashboard.tsx b/frontend/src/pages/admin/AdminDashboard.tsx index 92d8a24e..4a4495af 100644 --- a/frontend/src/pages/admin/AdminDashboard.tsx +++ b/frontend/src/pages/admin/AdminDashboard.tsx @@ -73,6 +73,7 @@ export const AdminDashboard: React.FC = () => { // Calculate expiring events const activeEvents = eventsData?.events.filter(e => e.is_active && !e.is_archived) || []; const expiringEvents = activeEvents.filter(e => { + if (!e.expires_at) return false; const days = differenceInDays(parseISO(e.expires_at), new Date()); return days <= 7 && days > 0; }); @@ -204,9 +205,11 @@ export const AdminDashboard: React.FC = () => { >

{event.event_name}

-

- {format(parseISO(event.event_date), 'PP')} -

+ {event.event_date && ( +

+ {format(parseISO(event.event_date), 'PP')} +

+ )}

diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 958519af..c2867881 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -31,9 +31,9 @@ import { import { parseISO, differenceInDays, isValid } from 'date-fns'; // Helper to safely parse dates that might be strings, Date objects, or timestamps -const safeParseDate = (dateValue: unknown): Date => { +const safeParseDate = (dateValue: unknown): Date | null => { if (!dateValue) { - return new Date(); + return null; } if (dateValue instanceof Date) { return dateValue; @@ -45,7 +45,7 @@ const safeParseDate = (dateValue: unknown): Date => { const parsed = parseISO(dateValue); return isValid(parsed) ? parsed : new Date(dateValue); } - return new Date(); + return null; }; import { toast } from 'react-toastify'; import { useLocalizedDate } from '../../hooks/useLocalizedDate'; @@ -389,16 +389,17 @@ export const EventDetailsPage: React.FC = () => { ); } - const daysUntilExpiration = differenceInDays(safeParseDate(event.expires_at), new Date()); - const isExpired = daysUntilExpiration <= 0; - const isExpiring = daysUntilExpiration > 0 && daysUntilExpiration <= 7; + const expiresAtDate = safeParseDate(event.expires_at); + const daysUntilExpiration = expiresAtDate ? differenceInDays(expiresAtDate, new Date()) : null; + const isExpired = daysUntilExpiration !== null && daysUntilExpiration <= 0; + const isExpiring = daysUntilExpiration !== null && daysUntilExpiration > 0 && daysUntilExpiration <= 7; const handleStartEdit = () => { setEditForm({ welcome_message: event.welcome_message || '', color_theme: event.color_theme || '', css_template_id: event.css_template_id || null, - expires_at: format(safeParseDate(event.expires_at), 'yyyy-MM-dd'), + expires_at: expiresAtDate ? format(expiresAtDate, 'yyyy-MM-dd') : '', allow_user_uploads: event.allow_user_uploads || false, upload_category_id: event.upload_category_id || null, hero_photo_id: event.hero_photo_id || null, @@ -616,10 +617,12 @@ export const EventDetailsPage: React.FC = () => {

{event.event_name}

- - - {format(safeParseDate(event.event_date), 'PPP')} - + {event.event_date && ( + + + {format(safeParseDate(event.event_date)!, 'PPP')} + + )} {event.event_type} {
{t('events.created')}
- {format(safeParseDate(event.created_at), 'PP')} + {event.created_at && format(safeParseDate(event.created_at)!, 'PP')}
{t('events.expires')}
- {format(safeParseDate(event.expires_at), 'PP')} - {!event.is_archived && daysUntilExpiration > 0 && ( - - {t('events.daysLeft', { count: daysUntilExpiration })} - + {event.expires_at ? ( + <> + {format(safeParseDate(event.expires_at)!, 'PP')} + {!event.is_archived && daysUntilExpiration !== null && daysUntilExpiration > 0 && ( + + {t('events.daysLeft', { count: daysUntilExpiration })} + + )} + + ) : ( + {t('events.neverExpires', 'Never')} )}
@@ -1517,7 +1526,7 @@ export const EventDetailsPage: React.FC = () => {

{t('events.archivedOn')}

- {event.archived_at && format(safeParseDate(event.archived_at), 'PPp')} + {event.archived_at && format(safeParseDate(event.archived_at)!, 'PPp')}