Files
ToBePlayed-Frontend/src/components/SyncChangesDialog.jsx
T
lobo bbc707de87
Build, Test & Push Frontend / quality-check (push) Successful in 40s
Build, Test & Push Frontend / docker (push) Successful in 23s
feat(sync): add Set to 100% button for all-unlocked games
The sync details show a Set to 100% button for ALL_UNLOCKED games.
The button uses the existing status PUT, checks points, and reloads
the Dashboard. Refs #21
2026-10-11 16:22:49 +02:00

75 lines
3.5 KiB
React

import {useEffect, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Dialog, DialogContent, DialogDescription, DialogTitle} from '@/components/ui/dialog';
import {Skeleton} from '@/components/ui/skeleton';
import SyncCompletedButton from './SyncCompletedButton.jsx';
import GameRow from './stats/GameRow.jsx';
import {formatPlaytime} from '../utils/games.js';
import {fetchSyncChanges, groupByKind} from '../utils/syncChanges.js';
/**
* The changes of the last sync, grouped by kind. Loads the result again when it opens: the achievements of a sync
* arrive after its games.
*
* @param onChanged called after a game was set to COMPLETED from here
*/
export default function SyncChangesDialog({onClose, onChanged}) {
const {t} = useTranslation();
const [changes, setChanges] = useState(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
let current = true;
fetchSyncChanges()
.then((body) => {
if (!current) return;
if (body) setChanges(body.changes ?? []);
else setFailed(true);
})
.catch(() => current && setFailed(true));
return () => {
current = false;
};
}, []);
const value = (change) => {
switch (change.kind) {
case 'PLAYTIME': return `+${formatPlaytime(change.amount)}`;
case 'ACHIEVEMENTS': return `+${change.amount}`;
case 'STATUS': return `${t(`BACKLOG.STATUS.${change.fromStatus}`)} → ${t(`BACKLOG.STATUS.${change.toStatus}`)}`;
default: return null;
}
};
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent
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">{t('SYNC_CHANGES.TITLE')}</DialogTitle>
<DialogDescription className="sr-only">{t('SYNC_CHANGES.TITLE')}</DialogDescription>
{changes === null && !failed && <Skeleton className="h-24 rounded-xl"/>}
{failed && <p className="text-sm text-muted-foreground">{t('SYNC_CHANGES.ERROR')}</p>}
{changes && changes.length === 0 && <p className="text-sm text-muted-foreground">{t('SYNC_CHANGES.EMPTY')}</p>}
{changes && groupByKind(changes).map(({kind, changes: rows}) => (
<section key={kind} className="space-y-2">
<h3 className="text-sm font-semibold">{t(`SYNC_CHANGES.HEADING.${kind}`)}</h3>
<ul className="space-y-2">
{rows.map((change, index) => kind === 'ALL_UNLOCKED' ? (
<li key={change.appid} className="flex items-center gap-3">
<div className="min-w-0 flex-1"><GameRow game={change} linked/></div>
<SyncCompletedButton appid={change.appid} onChanged={onChanged}/>
</li>
) : (
<li key={`${change.appid}-${change.fromStatus}-${index}`}>
<GameRow game={change} value={value(change)} linked/>
</li>
))}
</ul>
</section>
))}
</DialogContent>
</Dialog>
);
}