Return

All tests / src/components/GithubStars index.tsx

95.91% Statements 47/49
95.45% Branches 21/22
100% Functions 9/9
97.61% Lines 41/42

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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 1594x 4x 4x 4x 4x 4x 4x 4x 4x   4x             4x 13x 13x   13x 13x         13x 13x                           4x 32x 32x 32x 32x 32x   32x   32x 22x             6x 1x 1x 1x     6x 1x 1x     6x 6x 6x 6x 6x       32x                     6x                                                                                                                                       19x  
import React from 'react';
import { AiFillStar } from 'react-icons/ai';
import { GoRepoForked, GoEye, GoIssueOpened } from 'react-icons/go';
import { useIntl, type IntlShape } from 'react-intl';
import useGithubStars from '@/hooks/useGithubStars';
import RenderManager from '@/components/RenderManager';
import { REPOSITORY_URL } from '@/constants/site';
import { clx } from '@/helpers';
import styles from './githubStars.module.css';
 
const PANEL_ID = 'github-stars-panel';
 
/**
 * How long ago the last push was, in the coarsest unit that is still true. react-intl does the
 * wording per locale, so this never assembles a sentence by hand. Its formatter rather than a bare
 * `Intl.RelativeTimeFormat`, which has no Galician data in Chromium (`src/intl/galician.ts`).
 */
const sincePush = (pushedAt: string, format: IntlShape['formatRelativeTime']): string | null => {
    const at = Date.parse(pushedAt);
    Iif (Number.isNaN(at)) return null;
 
    const seconds = Math.round((at - Date.now()) / 1000);
    const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
        ['day', 86400],
        ['hour', 3600],
        ['minute', 60],
    ];
    for (const [unit, size] of units) {
        if (Math.abs(seconds) >= size) return format(Math.round(seconds / size), unit, { numeric: 'auto' });
    }
    return format(0, 'minute', { numeric: 'auto' });
};
 
/**
 * A menu bar extra for the repository, of the kind macOS puts to the left of the clock: the count
 * is always on the bar, and clicking it opens a panel that reports the rest and offers the one
 * action worth offering.
 *
 * It deliberately does not link anywhere on the bar itself. The count is a status, and a status
 * that navigates away on a stray click is a trap — the `<a>` lives inside the panel, where the
 * reader has already said they are interested.
 */
const GithubStars = () => {
    const { data, error, loading } = useGithubStars();
    const { formatMessage: f, formatNumber, formatRelativeTime } = useIntl();
    const [open, setOpen] = React.useState(false);
    const triggerRef = React.useRef<HTMLButtonElement>(null);
    const panelRef = React.useRef<HTMLDivElement>(null);
 
    const close = React.useCallback(() => setOpen(false), []);
 
    React.useEffect(() => {
        if (!open) return undefined;
 
        /**
         * Escape returns focus to the trigger, which is what a menu bar extra does and what a
         * keyboard reader needs: dismissing the panel must not drop the caret at the top of the
         * document.
         */
        const onKeyDown = (event: KeyboardEvent) => {
            if (event.key === 'Escape') {
                close();
                triggerRef.current?.focus();
            }
        };
        const onPointerDown = (event: MouseEvent) => {
            const target = event.target as Node;
            if (!panelRef.current?.contains(target) && !triggerRef.current?.contains(target)) close();
        };
 
        document.addEventListener('keydown', onKeyDown);
        document.addEventListener('mousedown', onPointerDown);
        return () => {
            document.removeEventListener('keydown', onKeyDown);
            document.removeEventListener('mousedown', onPointerDown);
        };
    }, [open, close]);
 
    const updated = data?.pushedAt ? sincePush(data.pushedAt, formatRelativeTime) : null;
 
    return (
        <div className={styles.extra}>
            <button
                ref={triggerRef}
                type="button"
                className={styles.trigger}
                aria-expanded={open}
                aria-controls={PANEL_ID}
                aria-label={f({ id: 'githubStars.open' })}
                onClick={() => setOpen((wasOpen) => !wasOpen)}
            >
                <AiFillStar className={styles.star} aria-hidden="true" />
                <RenderManager
                    loading={loading}
                    error={error}
                    errorTitle={f({ id: 'starCounter.error' })}
                    loadingTitle={f({ id: 'starCounter.loading' })}
                >
                    <span data-testid="github-stars-count">{formatNumber(data?.stars ?? 0)}</span>
                </RenderManager>
            </button>
 
            {/*
             * Always rendered, hidden by the attribute. A panel that only exists while open cannot
             * be the target of `aria-controls`, and mounting it on first click is a layout jump
             * inside a 24px bar.
             */}
            <div
                ref={panelRef}
                id={PANEL_ID}
                data-testid="github-stars-panel"
                className={clx(styles.panel, open ? styles.opened : undefined)}
                hidden={!open}
            >
                <p className={styles.repo}>{f({ id: 'githubStars.repo' })}</p>
                <p className={styles.tagline}>{f({ id: 'githubStars.tagline' })}</p>
 
                <ul className={styles.counters}>
                    <li>
                        <AiFillStar className={styles.star} aria-hidden="true" />
                        <span className={styles.value}>{formatNumber(data?.stars ?? 0)}</span>
                        <span className={styles.caption}>{f({ id: 'githubStars.stars' })}</span>
                    </li>
                    <li>
                        <GoRepoForked aria-hidden="true" />
                        <span className={styles.value}>{formatNumber(data?.forks ?? 0)}</span>
                        <span className={styles.caption}>{f({ id: 'githubStars.forks' })}</span>
                    </li>
                    <li>
                        <GoEye aria-hidden="true" />
                        <span className={styles.value}>{formatNumber(data?.watchers ?? 0)}</span>
                        <span className={styles.caption}>{f({ id: 'githubStars.watchers' })}</span>
                    </li>
                    <li>
                        <GoIssueOpened aria-hidden="true" />
                        <span className={styles.value}>{formatNumber(data?.issues ?? 0)}</span>
                        <span className={styles.caption}>{f({ id: 'githubStars.issues' })}</span>
                    </li>
                </ul>
 
                {updated && <p className={styles.updated}>{f({ id: 'githubStars.updated' }, { when: updated })}</p>}
 
                <a
                    className={styles.cta}
                    href={REPOSITORY_URL}
                    target="_blank"
                    rel="noopener noreferrer"
                    onClick={close}
                >
                    <AiFillStar aria-hidden="true" />
                    {f({ id: 'githubStars.cta' })}
                </a>
            </div>
        </div>
    );
};
 
export default GithubStars;