differenceInDays truncates to whole days, so an event expiring in a few hours returned 0 and three admin surfaces treated it as gone: - EventsListPage: status chip said 'Expired' (days <= 0) while the public gallery — which compares real timestamps — correctly showed 'expires in X hours'. This is the reporter's exact symptom. - EventDetailsPage: same isExpired math on the detail view. - AdminDashboard: the expiring-soon card showed '0 days left' on the final day. Expired is now gated on the actual timestamp (expires_at <= now), and the countdown chips use ceiling days so the last day reads '1 day left' instead of flipping to Expired/0. Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
aca3c8e4bc
commit
487f55f2d9
@@ -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 = () => {
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{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 (
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
||||
|
||||
@@ -290,9 +289,14 @@ export const EventDetailsPage: React.FC = () => {
|
||||
}
|
||||
|
||||
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({
|
||||
|
||||
@@ -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' };
|
||||
|
||||
Reference in New Issue
Block a user