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
This commit is contained in:
Paul Nothaft
2026-01-22 13:54:23 +01:00
parent d4a15dbe74
commit c5a8ffc08c
4 changed files with 47 additions and 31 deletions
@@ -310,10 +310,12 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
} }
}, [settingsData, data, setTheme]); // Use data instead of event prop }, [settingsData, data, setTheme]); // Use data instead of event prop
// Calculate days until expiration // Calculate days until expiration (null means never expires)
const daysUntilExpiration = differenceInDays(parseISO(event.expires_at), new Date()); const daysUntilExpiration = event.expires_at
const showUrgentWarning = daysUntilExpiration <= 7; ? differenceInDays(parseISO(event.expires_at), new Date())
const isExpired = daysUntilExpiration < 0; : null;
const showUrgentWarning = daysUntilExpiration !== null && daysUntilExpiration <= 7;
const isExpired = daysUntilExpiration !== null && daysUntilExpiration < 0;
// Filter and sort photos // Filter and sort photos
const filteredPhotos = useMemo(() => { const filteredPhotos = useMemo(() => {
@@ -602,7 +604,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
headerExtra={(() => { headerExtra={(() => {
const items = []; const items = [];
if (daysUntilExpiration <= 1 && daysUntilExpiration > 0) { if (daysUntilExpiration !== null && daysUntilExpiration <= 1 && daysUntilExpiration > 0) {
items.push( items.push(
<CountdownTimer key="countdown" expiresAt={event.expires_at} className="mr-2" /> <CountdownTimer key="countdown" expiresAt={event.expires_at} className="mr-2" />
); );
+4 -2
View File
@@ -187,8 +187,8 @@ export const GalleryPage: React.FC = () => {
} }
}, [galleryInfo, isAuthenticated, autoLoginAttempted, login, resolvedSlug, isResolvingIdentifier]); }, [galleryInfo, isAuthenticated, autoLoginAttempted, login, resolvedSlug, isResolvingIdentifier]);
// Calculate days until expiration // Calculate days until expiration (null if no expiration set)
const daysUntilExpiration = galleryInfo const daysUntilExpiration = galleryInfo?.expires_at
? differenceInDays(parseISO(galleryInfo.expires_at), new Date()) ? differenceInDays(parseISO(galleryInfo.expires_at), new Date())
: null; : null;
@@ -394,9 +394,11 @@ export const GalleryPage: React.FC = () => {
<CardContent className="text-center py-12"> <CardContent className="text-center py-12">
<Clock className="w-16 h-16 text-amber-500 mx-auto mb-4" /> <Clock className="w-16 h-16 text-amber-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">{t('gallery.expired')}</h2> <h2 className="text-xl font-semibold mb-2">{t('gallery.expired')}</h2>
{galleryInfo.expires_at && (
<p className="text-neutral-600 mb-4"> <p className="text-neutral-600 mb-4">
{t('gallery.expiredOn', { date: format(parseISO(galleryInfo.expires_at), 'PP') })} {t('gallery.expiredOn', { date: format(parseISO(galleryInfo.expires_at), 'PP') })}
</p> </p>
)}
<p className="text-sm text-neutral-500"> <p className="text-sm text-neutral-500">
{t('gallery.contactOrganizer')} {t('gallery.contactOrganizer')}
</p> </p>
@@ -73,6 +73,7 @@ export const AdminDashboard: React.FC = () => {
// Calculate expiring events // Calculate expiring events
const activeEvents = eventsData?.events.filter(e => e.is_active && !e.is_archived) || []; const activeEvents = eventsData?.events.filter(e => e.is_active && !e.is_archived) || [];
const expiringEvents = activeEvents.filter(e => { const expiringEvents = activeEvents.filter(e => {
if (!e.expires_at) return false;
const days = differenceInDays(parseISO(e.expires_at), new Date()); const days = differenceInDays(parseISO(e.expires_at), new Date());
return days <= 7 && days > 0; return days <= 7 && days > 0;
}); });
@@ -204,9 +205,11 @@ export const AdminDashboard: React.FC = () => {
> >
<div> <div>
<h3 className="font-medium text-neutral-900">{event.event_name}</h3> <h3 className="font-medium text-neutral-900">{event.event_name}</h3>
{event.event_date && (
<p className="text-sm text-neutral-600"> <p className="text-sm text-neutral-600">
{format(parseISO(event.event_date), 'PP')} {format(parseISO(event.event_date), 'PP')}
</p> </p>
)}
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-medium text-orange-600"> <p className="text-sm font-medium text-orange-600">
+21 -12
View File
@@ -31,9 +31,9 @@ import {
import { parseISO, differenceInDays, isValid } from 'date-fns'; import { parseISO, differenceInDays, isValid } from 'date-fns';
// Helper to safely parse dates that might be strings, Date objects, or timestamps // 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) { if (!dateValue) {
return new Date(); return null;
} }
if (dateValue instanceof Date) { if (dateValue instanceof Date) {
return dateValue; return dateValue;
@@ -45,7 +45,7 @@ const safeParseDate = (dateValue: unknown): Date => {
const parsed = parseISO(dateValue); const parsed = parseISO(dateValue);
return isValid(parsed) ? parsed : new Date(dateValue); return isValid(parsed) ? parsed : new Date(dateValue);
} }
return new Date(); return null;
}; };
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useLocalizedDate } from '../../hooks/useLocalizedDate'; import { useLocalizedDate } from '../../hooks/useLocalizedDate';
@@ -389,16 +389,17 @@ export const EventDetailsPage: React.FC = () => {
); );
} }
const daysUntilExpiration = differenceInDays(safeParseDate(event.expires_at), new Date()); const expiresAtDate = safeParseDate(event.expires_at);
const isExpired = daysUntilExpiration <= 0; const daysUntilExpiration = expiresAtDate ? differenceInDays(expiresAtDate, new Date()) : null;
const isExpiring = daysUntilExpiration > 0 && daysUntilExpiration <= 7; const isExpired = daysUntilExpiration !== null && daysUntilExpiration <= 0;
const isExpiring = daysUntilExpiration !== null && daysUntilExpiration > 0 && daysUntilExpiration <= 7;
const handleStartEdit = () => { const handleStartEdit = () => {
setEditForm({ setEditForm({
welcome_message: event.welcome_message || '', welcome_message: event.welcome_message || '',
color_theme: event.color_theme || '', color_theme: event.color_theme || '',
css_template_id: event.css_template_id || null, 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, allow_user_uploads: event.allow_user_uploads || false,
upload_category_id: event.upload_category_id || null, upload_category_id: event.upload_category_id || null,
hero_photo_id: event.hero_photo_id || null, hero_photo_id: event.hero_photo_id || null,
@@ -616,10 +617,12 @@ export const EventDetailsPage: React.FC = () => {
<div> <div>
<h1 className="text-2xl font-bold text-neutral-900">{event.event_name}</h1> <h1 className="text-2xl font-bold text-neutral-900">{event.event_name}</h1>
<div className="flex items-center gap-4 mt-2 text-sm text-neutral-600"> <div className="flex items-center gap-4 mt-2 text-sm text-neutral-600">
{event.event_date && (
<span className="flex items-center"> <span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" /> <Calendar className="w-4 h-4 mr-1" />
{format(safeParseDate(event.event_date), 'PPP')} {format(safeParseDate(event.event_date)!, 'PPP')}
</span> </span>
)}
<span className="capitalize">{event.event_type}</span> <span className="capitalize">{event.event_type}</span>
<span <span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
@@ -1181,19 +1184,25 @@ export const EventDetailsPage: React.FC = () => {
<div> <div>
<dt className="text-sm font-medium text-neutral-500">{t('events.created')}</dt> <dt className="text-sm font-medium text-neutral-500">{t('events.created')}</dt>
<dd className="mt-1 text-sm text-neutral-900"> <dd className="mt-1 text-sm text-neutral-900">
{format(safeParseDate(event.created_at), 'PP')} {event.created_at && format(safeParseDate(event.created_at)!, 'PP')}
</dd> </dd>
</div> </div>
<div> <div>
<dt className="text-sm font-medium text-neutral-500">{t('events.expires')}</dt> <dt className="text-sm font-medium text-neutral-500">{t('events.expires')}</dt>
<dd className="mt-1 text-sm text-neutral-900"> <dd className="mt-1 text-sm text-neutral-900">
{format(safeParseDate(event.expires_at), 'PP')} {event.expires_at ? (
{!event.is_archived && daysUntilExpiration > 0 && ( <>
{format(safeParseDate(event.expires_at)!, 'PP')}
{!event.is_archived && daysUntilExpiration !== null && daysUntilExpiration > 0 && (
<span className="text-neutral-500 ml-1"> <span className="text-neutral-500 ml-1">
{t('events.daysLeft', { count: daysUntilExpiration })} {t('events.daysLeft', { count: daysUntilExpiration })}
</span> </span>
)} )}
</>
) : (
<span className="text-neutral-500">{t('events.neverExpires', 'Never')}</span>
)}
</dd> </dd>
</div> </div>
</div> </div>
@@ -1517,7 +1526,7 @@ export const EventDetailsPage: React.FC = () => {
<div> <div>
<p className="text-sm font-medium text-neutral-500">{t('events.archivedOn')}</p> <p className="text-sm font-medium text-neutral-500">{t('events.archivedOn')}</p>
<p className="text-sm text-neutral-900"> <p className="text-sm text-neutral-900">
{event.archived_at && format(safeParseDate(event.archived_at), 'PPp')} {event.archived_at && format(safeParseDate(event.archived_at)!, 'PPp')}
</p> </p>
</div> </div>