diff --git a/frontend/src/pages/admin/AdminDashboard.tsx b/frontend/src/pages/admin/AdminDashboard.tsx
index 8ca892bc..2240f867 100644
--- a/frontend/src/pages/admin/AdminDashboard.tsx
+++ b/frontend/src/pages/admin/AdminDashboard.tsx
@@ -15,7 +15,7 @@ import {
Check,
X
} from 'lucide-react';
-import { differenceInDays, parseISO } from 'date-fns';
+import { parseISO } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { useMutationWithToast } from '../../hooks';
@@ -237,7 +237,8 @@ export const AdminDashboard: React.FC = () => {
) : (
{expiringEvents.map((event) => {
- const daysLeft = differenceInDays(parseISO(event.expires_at!), new Date());
+ // Ceiling (#909): truncation showed "0 days" on the last day.
+ const daysLeft = Math.max(1, Math.ceil((parseISO(event.expires_at!).getTime() - Date.now()) / 86400000));
return (
{
}
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;
+ // Timestamp comparison, not truncated whole days (#909): the old
+ // differenceInDays <= 0 marked events "expired" up to 24h early.
+ // Ceiling keeps the countdown at "1 day" through the final day.
+ const isExpired = expiresAtDate !== null && expiresAtDate.getTime() <= Date.now();
+ const daysUntilExpiration = expiresAtDate
+ ? Math.ceil((expiresAtDate.getTime() - Date.now()) / 86400000)
+ : null;
+ const isExpiring = !isExpired && daysUntilExpiration !== null && daysUntilExpiration > 0 && daysUntilExpiration <= 7;
const handleStartEdit = () => {
setEditForm({
diff --git a/frontend/src/pages/admin/EventsListPage.tsx b/frontend/src/pages/admin/EventsListPage.tsx
index f8dd6bd0..cd1d3408 100644
--- a/frontend/src/pages/admin/EventsListPage.tsx
+++ b/frontend/src/pages/admin/EventsListPage.tsx
@@ -18,7 +18,7 @@ import {
ChevronLeft,
ChevronRight
} from 'lucide-react';
-import { parseISO, differenceInDays } from 'date-fns';
+import { parseISO } from 'date-fns';
import { toast } from 'react-toastify';
import { useModal, useMutationWithToast } from '../../hooks';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
@@ -258,8 +258,14 @@ export const EventsListPage: React.FC = () => {
if (!event.expires_at) return { label: t('events.active'), color: 'text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/40' };
- const days = differenceInDays(parseISO(event.expires_at), new Date());
- if (days <= 0) return { label: t('events.expired'), color: 'text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/40' };
+ // Expired means the timestamp has actually passed (#909):
+ // differenceInDays truncates to whole days, so an event expiring in a
+ // few hours returned 0 and showed "Expired" while the public gallery
+ // (which compares real timestamps) correctly showed it active.
+ const expiresAt = parseISO(event.expires_at);
+ if (expiresAt.getTime() <= Date.now()) return { label: t('events.expired'), color: 'text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/40' };
+ // Ceiling so the last day reads "1 day left", never "0 days".
+ const days = Math.ceil((expiresAt.getTime() - Date.now()) / 86400000);
if (days <= 7) return { label: t('events.daysLeft', { count: days }), color: 'text-orange-600 dark:text-orange-400 bg-orange-100 dark:bg-orange-900/40' };
return { label: t('events.active'), color: 'text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/40' };