Files
ToBePlayed-Frontend/src/components/StatCard.jsx
T
lobo a60010e055 fix(ux): apply UX quick wins (#54)
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.
2026-10-21 08:21:39 +02:00

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;
}