Public Access
Get started links to registration and says an admin approves accounts. Back navigation restores the library scroll; search no longer fills history. Steam and PSN unlink ask first. Any close marks news as seen. Confetti plays only when the level rises on the page. Count-ups respect reduced motion. Html lang follows the UI language. Add a 404 page, translated role and dialog close label.
59 lines
2.3 KiB
React
59 lines
2.3 KiB
React
import {useEffect, useState} from 'react';
|
|
import {useTranslation} from 'react-i18next';
|
|
|
|
import {Card} from '@/components/ui/card';
|
|
import useReducedMotion from '../hooks/useReducedMotion.js';
|
|
import {useDrillTarget} from './stats/StatDrill.jsx';
|
|
|
|
function useCountUp(target, instant, durationMs = 1600) {
|
|
const [value, setValue] = useState(0);
|
|
useEffect(() => {
|
|
const safeTarget = typeof target === 'number' && Number.isFinite(target) ? target : 0;
|
|
if (instant || safeTarget === 0) {
|
|
setValue(safeTarget);
|
|
return;
|
|
}
|
|
let raf;
|
|
let start;
|
|
const step = (ts) => {
|
|
if (start === undefined) start = ts;
|
|
const progress = Math.min((ts - start) / durationMs, 1);
|
|
setValue(Math.floor(progress * safeTarget));
|
|
if (progress < 1) raf = requestAnimationFrame(step);
|
|
};
|
|
raf = requestAnimationFrame(step);
|
|
return () => cancelAnimationFrame(raf);
|
|
}, [target, instant, durationMs]);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* @param drill `{params}` of StatCount: the card is a button that opens the games behind the number
|
|
* @param onClick used when there is no drill target
|
|
*/
|
|
export default function StatCard({label, value, icon: Icon, suffix, drill, onClick}) {
|
|
const {i18n} = useTranslation();
|
|
const reduce = useReducedMotion();
|
|
const animated = useCountUp(value, reduce);
|
|
const target = useDrillTarget({title: label, count: value, ...drill}) ?? (onClick && {onClick});
|
|
const card = (
|
|
<Card className="gap-3 p-5">
|
|
{Icon && (
|
|
<span className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
<Icon className="size-4"/>
|
|
</span>
|
|
)}
|
|
<div className="text-3xl font-bold tracking-tight tabular-nums">
|
|
{animated.toLocaleString(i18n.language)}{suffix ?? ''}
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">{label}</div>
|
|
</Card>
|
|
);
|
|
return target ? (
|
|
<button type="button" onClick={target.onClick}
|
|
className="block w-full rounded-xl text-left transition-colors hover:text-primary focus-visible:ring-2 focus-visible:ring-ring">
|
|
{card}
|
|
</button>
|
|
) : card;
|
|
}
|