Public Access
114 lines
5.4 KiB
React
114 lines
5.4 KiB
React
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>
|
||
);
|
||
}
|