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,
|
Check,
|
||||||
X
|
X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { differenceInDays, parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
||||||
import { useMutationWithToast } from '../../hooks';
|
import { useMutationWithToast } from '../../hooks';
|
||||||
@@ -237,7 +237,8 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{expiringEvents.map((event) => {
|
{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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { differenceInDays } from 'date-fns';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
||||||
|
|
||||||
@@ -290,9 +289,14 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expiresAtDate = safeParseDate(event.expires_at);
|
const expiresAtDate = safeParseDate(event.expires_at);
|
||||||
const daysUntilExpiration = expiresAtDate ? differenceInDays(expiresAtDate, new Date()) : null;
|
// Timestamp comparison, not truncated whole days (#909): the old
|
||||||
const isExpired = daysUntilExpiration !== null && daysUntilExpiration <= 0;
|
// differenceInDays <= 0 marked events "expired" up to 24h early.
|
||||||
const isExpiring = daysUntilExpiration !== null && daysUntilExpiration > 0 && daysUntilExpiration <= 7;
|
// 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 = () => {
|
const handleStartEdit = () => {
|
||||||
setEditForm({
|
setEditForm({
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight
|
ChevronRight
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { parseISO, differenceInDays } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useModal, useMutationWithToast } from '../../hooks';
|
import { useModal, useMutationWithToast } from '../../hooks';
|
||||||
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
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' };
|
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());
|
// Expired means the timestamp has actually passed (#909):
|
||||||
if (days <= 0) return { label: t('events.expired'), color: 'text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/40' };
|
// 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' };
|
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' };
|
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