Files
flxn-app/src/components/countdown.tsx
T
kyle 96d5535293 fix: countdown hydration mismatch; rate-limit sse-error session refreshes
- suppressHydrationWarning on countdown time text (server second differs
  from client second; first tick corrects)
- sse reconnects attempt a session refresh at most once per minute so a
  flaky stream cannot hammer the refresh endpoint
2026-08-26 22:41:05 -07:00

81 lines
2.2 KiB
TypeScript

import useNow from '@/hooks/use-now';
import { Text, Group } from '@mantine/core';
import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import classes from './countdown.module.css';
interface CountdownProps {
date: Date;
label?: string;
color?: string;
}
interface TimeLeft {
days: number;
hours: number;
minutes: number;
seconds: number;
}
function calculateTimeLeft(targetDate: Date, currentTime = new Date()): TimeLeft {
const difference = targetDate.getTime() - currentTime.getTime();
if (difference <= 0) {
return { days: 0, hours: 0, minutes: 0, seconds: 0 };
}
return {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
minutes: Math.floor((difference / 1000 / 60) % 60),
seconds: Math.floor((difference / 1000) % 60)
};
}
const pad = (num: number) => num.toString().padStart(2, '0');
export function Countdown({ date, label, color }: CountdownProps) {
const { t } = useLingui();
const now = useNow();
const timeLeft = useMemo(() => calculateTimeLeft(date, now), [date, now]);
const totalSecondsLeft = Math.max(
0,
Math.floor((date.getTime() - now.getTime()) / 1000)
);
const isFinalStretch = totalSecondsLeft > 0 && totalSecondsLeft <= 10;
const prefix =
timeLeft.days > 0
? `${t`${timeLeft.days}d`} ${pad(timeLeft.hours)}:${pad(timeLeft.minutes)}:`
: `${pad(timeLeft.hours)}:${pad(timeLeft.minutes)}:`;
return (
<Group gap="xs">
{label && <Text size='sm' fw={500}>{label}:</Text>}
{/* Server and client render different seconds; the first client tick corrects it. */}
<Text
size='sm'
fw={600}
c={isFinalStretch ? 'red' : color}
ff="monospace"
className={classes.time}
suppressHydrationWarning
>
{prefix}
<span
key={timeLeft.seconds}
className={`${classes.seconds} ${
isFinalStretch ? classes.finalTick : classes.tick
}`}
suppressHydrationWarning
>
{pad(timeLeft.seconds)}
</span>
</Text>
</Group>
);
}
export default Countdown;