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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | 3x 3x 3x 3x 3x 8x 8x 8x 8x 8x 8x 8x 8x 5x 8x 3x 13x 13x 13x 6x 6x 6x 13x 8x | import React from 'react';
import styles from './countdown.module.css';
import Tooltip from '@/components/Tooltip';
import { useIntl } from 'react-intl';
type Props = {
date: string;
caption?: string;
};
type TimeLeft = {
years: string | number;
months: string | number;
days: string | number;
hours: string | number;
minutes: string | number;
seconds: string | number;
};
/**
* @description This function is used to calculate the time left until the date
* @param {string} date - The date to calculate the time left
* @param {Function} setTime - The function to set the time left
* @param {TimeLeft} timeLeft - The time left object
* @returns {TimeLeft} - The time left object
* @example calculateTimeLeft('2021-12-31', setTime)
*/
const calculateTimeLeft = (
date: string,
setTime: (time: TimeLeft) => void,
timeLeft: TimeLeft = {} as TimeLeft
) => {
const difference = new Date(date).getTime() - Date.now();
const years = Math.floor(difference / (1000 * 60 * 60 * 24 * 30 * 12));
const months = Math.floor((difference / (1000 * 60 * 60 * 24 * 30)) % 12);
const days = Math.floor((difference / (1000 * 60 * 60 * 24)) % 30);
const hours = Math.floor((difference / (1000 * 60 * 60)) % 24);
const minutes = Math.floor((difference / (1000 * 60)) % 60);
const seconds = Math.floor((difference / 1000) % 60);
if (difference > 0) {
setTime({
years: years < 10 ? `0${years}` : years,
months: months < 10 ? `0${months}` : months,
days: days < 10 ? `0${days}` : days,
hours: hours < 10 ? `0${hours}` : hours,
minutes: minutes < 10 ? `0${minutes}` : minutes,
seconds: seconds < 10 ? `0${seconds}` : seconds,
});
}
return timeLeft;
};
/**
* @description Countdown to date provided in months, days, hours, minutes and seconds
* @param {string} date - date to countdown to
* @param {string} caption - caption to display
* @returns {JSX.Element}
*/
const CountDown = ({ date: dateProvided, caption }: Props) => {
const { formatMessage: f, formatDate } = useIntl();
const [time, setTime] = React.useState<TimeLeft>({
years: 0,
months: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
});
/**
* SDD-L09-T10. This was a `setTimeout` with `time` in its dependency array, and the callback
* called `setTime` with a **fresh object every tick**. So each tick changed `time` by identity,
* which re-ran the effect, which cleared and re-armed the timeout — a re-render loop that ran on
* every page, because the header mounts this site-wide. Each pass also dragged the Floating UI
* `Tooltip` wrapping it through a full render.
*
* One interval keyed on the date does the same job: the tick is what should drive the update,
* not the state the tick produced.
*/
React.useEffect(() => {
calculateTimeLeft(dateProvided, setTime);
const timer = setInterval(() => calculateTimeLeft(dateProvided, setTime), 1000);
return () => clearInterval(timer);
}, [dateProvided]);
/*
* SDD-L08-T18: the countdown was six bare numbers whose only explanation was a tooltip reading
* "Important date". This at least says what the number counts towards, in the reader's locale
* and calendar. What the date signifies is the owner's copy to write, not something to invent.
*/
const label = f(
{ id: 'countdown.label' },
{ date: formatDate(dateProvided, { dateStyle: 'long', timeZone: 'UTC' }) }
);
return (
<Tooltip>
<Tooltip.Trigger>
<div className={styles.countdown} data-testid="countdown" role="group" aria-label={label}>
<div className={styles.countdown__item}>
<div suppressHydrationWarning>{time.years}</div>
<div className={styles.countdown__item__text}>{f({ id: 'countdown.years' })}</div>
</div>
<div className={styles.countdown__item}>
<div suppressHydrationWarning>{time.months}</div>
<div className={styles.countdown__item__text}>{f({ id: 'countdown.months' })}</div>
</div>
<div className={styles.countdown__item}>
<div suppressHydrationWarning>{time.days}</div>
<div className={styles.countdown__item__text}>{f({ id: 'countdown.days' })}</div>
</div>
<div className={styles.countdown__item}>
<div suppressHydrationWarning>{time.hours}</div>
<div className={styles.countdown__item__text}>{f({ id: 'countdown.hours' })}</div>
</div>
<div className={styles.countdown__item}>
<div suppressHydrationWarning>{time.minutes}</div>
<div className={styles.countdown__item__text}>{f({ id: 'countdown.minutes' })}</div>
</div>
<div className={styles.countdown__item}>
<div suppressHydrationWarning>{time.seconds}</div>
<div className={styles.countdown__item__text}>{f({ id: 'countdown.seconds' })}</div>
</div>
</div>
</Tooltip.Trigger>
{caption && <Tooltip.Content>{caption}</Tooltip.Content>}
</Tooltip>
);
};
export default CountDown;
|