Files
ToBePlayed-Frontend/src/components/AchievementsCard.jsx
T
lobo 8aa3181c37
Build, Test & Push Frontend / quality-check (push) Successful in 35s
Build, Test & Push Frontend / docker (push) Successful in 22s
feat(stats): status donut, meters and fixed chart colors
Pie and donut charts use one fixed color-blind safe palette in every
skin and accent. The overview and the profile status tile show a status
donut with the total in the middle. Habits and achievements get meter
bars, top games get share bars, the pile of shame bar uses the palette.
2026-10-04 12:03:52 +02:00

97 lines
4.9 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {Link} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {Trophy} from 'lucide-react';
import {Card} from '@/components/ui/card';
import GameCover from './GameCover.jsx';
import FitText from './FitText.jsx';
import {StatCount} from './stats/StatDrill.jsx';
import StatMeter from './stats/StatMeter.jsx';
/**
* @param summary the answer of GET /api/library/achievements/summary
*/
export default function AchievementsCard({summary}) {
const {t, i18n} = useTranslation();
const closest = summary.closest ?? [];
const started = summary.startedGames ?? 0;
const pending = summary.pendingGames ?? 0;
const hidden = summary.hiddenGames ?? 0;
const perfect = summary.perfectGames ?? 0;
// A private profile keeps games pending; the privacy hint explains that better than the pending count.
const steamHidesAll = hidden > 0 && started === 0;
const average = summary.averageCompletion != null ? `${summary.averageCompletion.toLocaleString(i18n.language)} %` : '–';
const stats = [
[(summary.unlocked ?? 0).toLocaleString(i18n.language), t('DASHBOARD.ACHIEVEMENTS.UNLOCKED')],
[perfect.toLocaleString(i18n.language), t('DASHBOARD.ACHIEVEMENTS.PERFECT'),
{params: {key: 'PERFECT'}, count: perfect}],
[average, t('DASHBOARD.ACHIEVEMENTS.AVERAGE'), undefined,
summary.averageCompletion != null ? {percent: summary.averageCompletion, valueText: average} : null],
];
return (
<Card className="gap-5 p-5">
<h2 className="flex items-center gap-2 font-semibold">
<Trophy className="size-4 text-amber-500"/>
{t('DASHBOARD.ACHIEVEMENTS.TITLE')}
</h2>
<div className="grid grid-cols-3 gap-3">
{stats.map(([value, label, drill, meter]) => (
<div key={label} className="rounded-lg bg-muted/50 p-3 text-center">
<div className="text-xl md:text-2xl font-bold tabular-nums">
<StatCount {...drill} title={label} label={`${label}: ${value}`}>{value}</StatCount>
</div>
{meter && <StatMeter {...meter} label={label}/>}
<div className="text-xs text-muted-foreground">{label}</div>
</div>
))}
</div>
{started === 0 && pending === 0 && hidden === 0 && (
<p className="text-sm text-muted-foreground">{t('DASHBOARD.ACHIEVEMENTS.NONE')}</p>
)}
{closest.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-medium">{t('DASHBOARD.ACHIEVEMENTS.CLOSEST')}</h3>
<ul className="space-y-2">
{closest.map((game) => (
<li key={game.appid}>
<Link
to={`/library/${game.appid}`}
className="group flex items-center gap-3 rounded-lg p-1 transition-colors hover:bg-muted/50"
>
<GameCover appid={game.appid} imageUrl={game.imageUrl} className="w-16 shrink-0"/>
<div className="@container min-w-0 flex-1 space-y-1">
<div className="flex items-baseline justify-between gap-2">
<span className="min-w-0 text-sm font-medium group-hover:text-primary transition-colors">
<FitText text={game.name ?? `App ${game.appid}`}/>
</span>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{game.unlocked} / {game.total}
</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
<div className="h-full rounded-full bg-amber-500"
style={{width: `${(game.unlocked / game.total) * 100}%`}}/>
</div>
</div>
</Link>
</li>
))}
</ul>
</div>
)}
{pending > 0 && !steamHidesAll && (
<p className="text-xs text-muted-foreground">{t('DASHBOARD.ACHIEVEMENTS.PENDING', {count: pending})}</p>
)}
{steamHidesAll && (
<p className="text-xs text-muted-foreground">{t('DASHBOARD.ACHIEVEMENTS.HIDDEN')}</p>
)}
</Card>
);
}