Public Access
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.
97 lines
4.9 KiB
React
97 lines
4.9 KiB
React
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>
|
||
);
|
||
}
|