Files
ToBePlayed-Frontend/src/components/stats/StatGamesDialog.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

114 lines
5.4 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 {useCallback, useEffect, useRef, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Button} from '@/components/ui/button';
import {Dialog, DialogContent, DialogDescription, DialogTitle} from '@/components/ui/dialog';
import {Skeleton} from '@/components/ui/skeleton';
import GameRow from './GameRow.jsx';
import ComingBadge from '../ComingBadge.jsx';
import GameStatusBadge from '../GameStatusBadge.jsx';
import {formatDate} from '../../utils/format.js';
import {formatPlaytime} from '../../utils/games.js';
import {fetchStatGames} from '../../utils/statGames.js';
const ERRORS = {403: 'STATS.DRILL.NOT_SHARED', 401: 'STATS.DRILL.EXPIRED'};
/**
* The games behind one number, 40 per page.
*
* @param request `{title, source, ticket, linked, params}` or null (closed)
*/
export default function StatGamesDialog({request, onClose, onCloseAutoFocus}) {
const {t, i18n} = useTranslation();
const [state, setState] = useState({games: [], page: 0, totalPages: 0, total: 0, untracked: 0});
const [status, setStatus] = useState('loading');
const openRequest = useRef(null);
const load = useCallback(async (page, current) => {
setStatus('loading');
const {ok, status: code, data} = await fetchStatGames(request.source, {...request.params, page}, request.ticket);
if (openRequest.current !== request) return;
if (!ok) {
setStatus(code);
return;
}
setState({
games: [...current, ...data.content], page, totalPages: data.totalPages, total: data.totalElements,
untracked: data.untracked ?? 0,
});
setStatus('ok');
}, [request]);
useEffect(() => {
openRequest.current = request;
if (!request) return;
setState({games: [], page: 0, totalPages: 0, total: 0, untracked: 0});
load(0, []);
}, [request, load]);
const value = (game) => {
if (request.source === 'community') {
return `${t('STATS.DRILL.IN_LIBRARIES', {count: game.libraries})} · ${formatPlaytime(game.minutes)}`;
}
const moved = game.fromStatus || game.toStatus;
return (
<>
<ComingBadge game={game} compact/>
{!moved && game.status && <GameStatusBadge status={game.status} compact/>}
{moved && (
<span>{game.fromStatus ? t(`BACKLOG.STATUS.${game.fromStatus}`) : t('STATS.DRILL.NEW_IN_LIBRARY')}
{' → '}{game.toStatus ? t(`BACKLOG.STATUS.${game.toStatus}`) : '–'}</span>
)}
<span>{game.at
? formatDate(game.at, i18n.language, {dateStyle: 'medium'})
: formatPlaytime(game.minutes ?? game.playtimeMinutes ?? 0)}</span>
</>
);
};
const failed = typeof status === 'number';
return (
<Dialog open={request != null} onOpenChange={(open) => !open && onClose()}>
<DialogContent onCloseAutoFocus={onCloseAutoFocus}
className="max-h-[85vh] overflow-y-auto overflow-x-hidden max-sm:top-auto max-sm:bottom-0 max-sm:max-w-full max-sm:translate-y-0 max-sm:rounded-b-none sm:max-w-xl">
<DialogTitle className="pr-6">{request?.title}</DialogTitle>
<DialogDescription>{status === 'ok' ? t('STATS.DRILL.GAMES', {count: state.total}) : ' '}</DialogDescription>
{request && (
<ul className="space-y-2">
{state.games.map((game, index) => (
<li key={`${game.appid}-${index}`}>
<GameRow game={game} value={value(game)} linked={request.source === 'own' || request.linked === true}/>
</li>
))}
</ul>
)}
{status === 'loading' && <Skeleton className="h-24 rounded-xl"/>}
{status === 'ok' && state.games.length === 0 && (
<p className="text-sm text-muted-foreground">{t('STATS.DRILL.EMPTY')}</p>
)}
{status === 'ok' && state.untracked > 0 && (
<p className="text-xs text-muted-foreground">{t('STATS.FINISHED.BEFORE', {count: state.untracked})}</p>
)}
{failed && (
<div className="flex flex-col items-start gap-2 text-sm text-muted-foreground">
<p>{t(ERRORS[status] ?? 'STATS.DRILL.ERROR')}</p>
{status === 401
? <Button variant="outline" size="sm" className="min-h-11" onClick={() => globalThis.location.reload()}>
{t('STATS.DRILL.RELOAD')}</Button>
: status !== 403 && (
<Button variant="outline" size="sm" className="min-h-11"
onClick={() => load(state.page + (state.games.length > 0 ? 1 : 0), state.games)}>
{t('DASHBOARD.RETRY')}</Button>
)}
</div>
)}
{status === 'ok' && state.page + 1 < state.totalPages && (
<Button variant="outline" className="min-h-11" onClick={() => load(state.page + 1, state.games)}>
{t('STATS.DRILL.LOAD_MORE')}
</Button>
)}
</DialogContent>
</Dialog>
);
}