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:04:43 +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
// 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<GalleryViewProps> = ({ slug, event }) => {
headerExtra={(() => {
const items = [];
if (daysUntilExpiration <= 1 && daysUntilExpiration > 0) {
if (daysUntilExpiration !== null && daysUntilExpiration <= 1 && daysUntilExpiration > 0) {
items.push(
<CountdownTimer key="countdown" expiresAt={event.expires_at} className="mr-2" />
);
+7 -5
View File
@@ -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 = () => {
<CardContent className="text-center py-12">
<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>
<p className="text-neutral-600 mb-4">
{t('gallery.expiredOn', { date: format(parseISO(galleryInfo.expires_at), 'PP') })}
</p>
{galleryInfo.expires_at && (
<p className="text-neutral-600 mb-4">
{t('gallery.expiredOn', { date: format(parseISO(galleryInfo.expires_at), 'PP') })}
</p>
)}
<p className="text-sm text-neutral-500">
{t('gallery.contactOrganizer')}
</p>
+6 -3
View File
@@ -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 = () => {
>
<div>
<h3 className="font-medium text-neutral-900">{event.event_name}</h3>
<p className="text-sm text-neutral-600">
{format(parseISO(event.event_date), 'PP')}
</p>
{event.event_date && (
<p className="text-sm text-neutral-600">
{format(parseISO(event.event_date), 'PP')}
</p>
)}
</div>
<div className="text-right">
<p className="text-sm font-medium text-orange-600">
+27 -18
View File
@@ -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 = () => {
<div>
<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">
<span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
{format(safeParseDate(event.event_date), 'PPP')}
</span>
{event.event_date && (
<span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
{format(safeParseDate(event.event_date)!, 'PPP')}
</span>
)}
<span className="capitalize">{event.event_type}</span>
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
@@ -1181,18 +1184,24 @@ export const EventDetailsPage: React.FC = () => {
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.created')}</dt>
<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>
</div>
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.expires')}</dt>
<dd className="mt-1 text-sm text-neutral-900">
{format(safeParseDate(event.expires_at), 'PP')}
{!event.is_archived && daysUntilExpiration > 0 && (
<span className="text-neutral-500 ml-1">
{t('events.daysLeft', { count: daysUntilExpiration })}
</span>
{event.expires_at ? (
<>
{format(safeParseDate(event.expires_at)!, 'PP')}
{!event.is_archived && daysUntilExpiration !== null && daysUntilExpiration > 0 && (
<span className="text-neutral-500 ml-1">
{t('events.daysLeft', { count: daysUntilExpiration })}
</span>
)}
</>
) : (
<span className="text-neutral-500">{t('events.neverExpires', 'Never')}</span>
)}
</dd>
</div>
@@ -1517,7 +1526,7 @@ export const EventDetailsPage: React.FC = () => {
<div>
<p className="text-sm font-medium text-neutral-500">{t('events.archivedOn')}</p>
<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>
</div>