Files
ToBePlayed-Frontend/src/components/PileOfShameCard.jsx
T
lobo c158909083
Build, Test & Push Frontend / quality-check (push) Successful in 32s
Build, Test & Push Frontend / docker (push) Successful in 22s
feat(games): add coming badge for unreleased games
2026-10-05 20:33:25 +02:00

129 lines
6.0 KiB
React

import {Link} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {Dices, Inbox, Shuffle} from 'lucide-react';
import {Button} from '@/components/ui/button';
import {Card} from '@/components/ui/card';
import {cn} from '@/lib/utils';
import GameCover from './GameCover.jsx';
import PileTrend from './stats/PileTrend.jsx';
import {StatCount} from './stats/StatDrill.jsx';
import {backlogShare, pileGames} from '../utils/backlog.js';
import {sliceColors, statusSlices} from '../utils/statCharts.js';
import {formatPlaytime} from '../utils/games.js';
import {libraryLink} from '../utils/statGames.js';
/**
* @param trend the TREND group of GET /api/library/stats; the card shows no trend while it is missing
*/
export default function PileOfShameCard({summary, trend, syncing, onShuffle, shuffling}) {
const {t, i18n} = useTranslation();
const backlog = pileGames(summary);
const suggestion = summary.suggestion;
const slices = statusSlices(summary.byStatus);
const colors = sliceColors(slices);
if (syncing && summary.total === 0) {
return (
<Card className="gap-5 p-5">
<Title/>
<p className="py-6 text-center text-sm text-muted-foreground">{t('LIBRARY.SYNCING_FIRST_TIME')}</p>
</Card>
);
}
return (
<Card className="gap-5 p-5">
<Title/>
<div>
<div className="flex items-baseline gap-2">
<StatCount to={libraryLink('BACKLOG')} title={t('DASHBOARD.PILE_OF_SHAME')} params={{key: 'PILE'}} count={backlog}
label={`${t('BACKLOG.STATUS.BACKLOG')}: ${backlog}`}>
<span className="text-4xl font-bold tabular-nums">{backlog.toLocaleString(i18n.language)}</span>
</StatCount>
<span className="text-sm text-muted-foreground">
{t('DASHBOARD.PILE_SHARE', {percent: backlogShare(summary)})}
</span>
</div>
<p className="mt-1 text-sm text-muted-foreground">
<StatCount params={summary.neverPlayed > 0 ? {key: 'NEVER_STARTED'} : null}
title={t('STATS.OVERVIEW.NEVER_STARTED')}>
{t('DASHBOARD.NEVER_PLAYED', {count: summary.neverPlayed})}
</StatCount>
</p>
{summary.backlogMinutes > 0 && (
<p className="text-sm text-muted-foreground">
<StatCount params={{key: 'PILE_TIME_TO_BEAT'}} title={t('STATS.PILE.NORMAL')}>
{t('DASHBOARD.BACKLOG_HOURS', {time: formatPlaytime(summary.backlogMinutes)})}
</StatCount>
</p>
)}
</div>
{trend && <PileTrend trend={trend}/>}
{summary.total > 0 && (
<div className="space-y-2">
<div className="flex h-2.5 overflow-hidden rounded-full bg-muted">
{slices.map((s, i) => s.value > 0 && (
<div key={s.name} className="border-card [&:not(:last-child)]:border-r-2"
style={{width: `${(s.value / summary.total) * 100}%`, background: colors[i]}}/>
))}
</div>
<ul className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
{slices.map(({name: s, value}, i) => (
<li key={s} className="flex items-center gap-1.5">
<span className={cn('size-2 rounded-full', !colors[i] && 'bg-muted')}
style={colors[i] ? {background: colors[i]} : undefined}/>
{t(`BACKLOG.STATUS.${s}`)}
<StatCount to={libraryLink(s)} count={value}
label={`${t(`BACKLOG.STATUS.${s}`)}: ${value}`}>
<span className="tabular-nums text-foreground">{value}</span>
</StatCount>
</li>
))}
</ul>
</div>
)}
{suggestion ? (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{t('DASHBOARD.SUGGESTION')}</span>
{backlog > 1 && (
<Button variant="ghost" size="sm" onClick={onShuffle} disabled={shuffling}>
<Shuffle/>{t('DASHBOARD.OTHER_SUGGESTION')}
</Button>
)}
</div>
<Link
to={`/library/${suggestion.appid}`}
className="group flex items-center gap-3 rounded-lg border p-2 transition-colors hover:bg-muted/50"
>
<GameCover appid={suggestion.appid} imageUrl={suggestion.imageUrl} className="w-24 shrink-0"/>
<span className="min-w-0 truncate font-medium group-hover:text-primary transition-colors">
{suggestion.name ?? `App ${suggestion.appid}`}
</span>
</Link>
<Button asChild variant="outline" size="sm" className="w-full">
<Link to="/play-next"><Dices/>{t('DASHBOARD.OPEN_PICKER')}</Link>
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground">{t('DASHBOARD.BACKLOG_EMPTY')}</p>
)}
</Card>
);
}
function Title() {
const {t} = useTranslation();
return (
<h2 className="flex items-center gap-2 font-semibold">
<Inbox className="size-4 text-rose-500"/>
{t('DASHBOARD.PILE_OF_SHAME')}
</h2>
);
}