Return

All tests / src/components/CountDown index.tsx

100% Statements 27/27
93.33% Branches 14/15
100% Functions 5/5
100% Lines 24/24

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 1354x 4x 4x 4x                                               4x         10x 10x 10x 10x 10x 10x 10x   10x 7x                   10x                 4x 20x 20x                                     20x 8x 8x   8x               20x                                                                               13x  
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;