- 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
81 lines
2.2 KiB
TypeScript
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;
|