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' };