feat(stats): add stats page and pile trend
Build, Test & Push Frontend / quality-check (push) Successful in 15s
Build, Test & Push Frontend / docker (push) Successful in 22s

Show the stat groups on /stats, the pile trend on the dashboard card,
one switch per group in the settings, and the shared groups on the
public profile.
This commit is contained in:
lobo committed 2026-09-27 07:16:10 +02:00
1 parent 83bbc2730d
commit 9d1c7bb40d
31 files changed
+978 -11

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
import {useTranslation} from 'react-i18next';
import {Inbox} from 'lucide-react';
import StatsSection from './StatsSection.jsx';
import StatTile from './StatTile.jsx';
import GameRow from './GameRow.jsx';
import {formatPlaytime} from '../../utils/games.js';
import {formatDecimal} from '../../utils/stats.js';
export default function PileStats({pile, linked = true}) {
const {t, i18n} = useTranslation();
const since = pile.oldestUnplayed
? new Date(pile.oldestUnplayed.inLibrarySince).toLocaleDateString(i18n.language, {dateStyle: 'medium'})
: null;
return (
<StatsSection title={t('STATS.PILE.TITLE')} icon={Inbox}>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<StatTile label={t('STATS.PILE.GAMES')} value={pile.backlogGames.toLocaleString(i18n.language)}/>
<StatTile label={t('STATS.PILE.NORMAL')} value={formatPlaytime(pile.normalMinutes)}
hint={t('STATS.PILE.KNOWN', {count: pile.gamesWithTimeToBeat})}/>
<StatTile label={t('STATS.PILE.COMPLETIONIST')} value={formatPlaytime(pile.completionistMinutes)}/>
</div>
{pile.normalMinutes > 0 && (
<ul className="space-y-1 text-sm">
<li>
{pile.paceWeeks == null
? t('STATS.PILE.PACE_NONE')
: t('STATS.PILE.PACE', {
weeks: formatDecimal(pile.paceWeeks, i18n.language),
perWeek: formatPlaytime(pile.minutesPerWeek),
})}
</li>
<li>{t('STATS.PILE.WORK_YEARS', {years: formatDecimal(pile.workYears, i18n.language, 2)})}</li>
</ul>
)}
{pile.oldestUnplayed && (
<div className="space-y-2">
<span className="text-sm font-medium">{t('STATS.PILE.OLDEST')}</span>
<GameRow game={pile.oldestUnplayed} value={t('STATS.PILE.SINCE', {date: since})} linked={linked}/>
</div>
)}
</StatsSection>
);
}