Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 1x 13x 13x 13x 3x 10x | /**
* CountdownTimer Component (Story 4.1.3)
* Displays countdown to upcoming event if within 30 days
*/
import { differenceInCalendarDays } from 'date-fns';
import { useTranslation } from 'react-i18next';
interface CountdownTimerProps {
eventDate: Date;
}
export const CountdownTimer = ({ eventDate }: CountdownTimerProps) => {
const { t } = useTranslation('common');
// differenceInCalendarDays (not differenceInDays): "today / tomorrow / N days" is a
// calendar-boundary question, not a 24h-period one. differenceInDays truncates whole
// 24h spans, so an event at 00:00 two calendar days out read as 1 → "Tomorrow!" (the
// BATbern59 2026-06-19 bug seen on 2026-06-17). Calendar-day diff is timezone-local,
// matching how a visitor reads the date on the page.
const daysUntil = differenceInCalendarDays(eventDate, new Date());
// Only show if event is within 30 days and in the future
if (daysUntil < 0 || daysUntil > 30) {
return null;
}
return (
<div className="flex items-center gap-3 animate-in fade-in slide-in-from-top-2 duration-500">
{/* Pulsing blue dot */}
<div className="relative">
<div className="h-3 w-3 rounded-full bg-primary animate-pulse" />
<div className="absolute inset-0 h-3 w-3 rounded-full bg-primary opacity-75 animate-ping" />
</div>
{/* Badge */}
<div className="text-sm font-light text-zinc-300">
<span className="text-primary font-medium">{t('countdown.nextEvent')}</span>
{' • '}
{daysUntil === 0 ? (
<span className="text-green-400 font-medium">{t('countdown.today')}</span>
) : daysUntil === 1 ? (
<span className="text-orange-400 font-medium">{t('countdown.tomorrow')}</span>
) : (
<span>{t('countdown.daysUntil', { count: daysUntil })}</span>
)}
</div>
</div>
);
};
|