Public Access
Library rows: the status badge is a menu that saves at once. Game page: status and rating in the top card, saved on click. Status changes and hiding a game show an Undo toast. Other game actions moved into a "..." menu; split and unlink ask first. New sorts: name, last played, time to beat; "last synced" is gone. Page numbers and 44 px touch targets on phones.
82 lines
3.7 KiB
React
82 lines
3.7 KiB
React
import {useState} from 'react';
|
|
import {Link} from 'react-router-dom';
|
|
import {useTranslation} from 'react-i18next';
|
|
import {Layers, Link2Off, Unlink} from 'lucide-react';
|
|
import {toast} from 'sonner';
|
|
|
|
import {Button} from '@/components/ui/button';
|
|
import {Card} from '@/components/ui/card';
|
|
import GameStatusBadge from './GameStatusBadge.jsx';
|
|
import UnlinkDialog from './UnlinkDialog.jsx';
|
|
import FitText from './FitText.jsx';
|
|
import {splitGame, unlinkGame} from '../utils/links.js';
|
|
import {platformLabel} from '../utils/platforms.js';
|
|
|
|
/**
|
|
* The other editions of this game in the own library, and the buttons to ask for a link (everyone), to split the game
|
|
* off its set (everyone, for the own library) or to unlink (auditor). Shown on every game: a request must be possible
|
|
* from any game.
|
|
*/
|
|
export default function EditionsCard({game, auditor, onRequest, onChanged}) {
|
|
const {t} = useTranslation();
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const run = async (call, doneKey) => {
|
|
setBusy(true);
|
|
try {
|
|
const response = await call(game.appid);
|
|
if (response.status === 401) return;
|
|
if (response.ok) {
|
|
toast.success(t(doneKey));
|
|
onChanged();
|
|
} else {
|
|
toast.error(t('LINKS.ERROR'));
|
|
}
|
|
} catch (err) {
|
|
console.error('Edition change error:', err);
|
|
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="gap-3 p-4">
|
|
{game.editions?.length > 0 && (
|
|
<div className="space-y-2 text-sm">
|
|
<p className="font-medium">{t('LINKS.ALSO_IN_LIBRARY')}</p>
|
|
<ul className="space-y-1">
|
|
{game.editions.map((edition) => (
|
|
<li key={edition.appid} className="@container flex flex-wrap items-center gap-2">
|
|
<Link to={`/library/${edition.appid}`} className="min-w-0 max-w-full hover:text-primary hover:underline">
|
|
<FitText text={edition.name}/>
|
|
</Link>
|
|
{platformLabel(edition) && (
|
|
<span className="text-muted-foreground">({platformLabel(edition)})</span>
|
|
)}
|
|
<GameStatusBadge status={edition.status} compact/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className="text-muted-foreground">{t('LINKS.SHARED_STATUS')}</p>
|
|
</div>
|
|
)}
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button type="button" variant="outline" size="sm" onClick={onRequest}>
|
|
<Layers/>{t(auditor ? 'LINKS.LINK_WITH' : 'LINKS.SAME_AS')}
|
|
</Button>
|
|
{game.editions?.length > 0 && (
|
|
<UnlinkDialog size="sm" icon={Unlink} label={t('LINKS.SPLIT')} title={t('LINKS.SPLIT_CONFIRM_TITLE')}
|
|
description={t('LINKS.SPLIT_CONFIRM_TEXT')} confirmLabel={t('LINKS.SPLIT')} disabled={busy}
|
|
onConfirm={() => run(splitGame, 'LINKS.SPLIT_DONE')}/>
|
|
)}
|
|
{auditor && game.linked && (
|
|
<UnlinkDialog size="sm" icon={Link2Off} label={t('LINKS.UNLINK')} title={t('LINKS.UNLINK_CONFIRM_TITLE')}
|
|
description={t('LINKS.UNLINK_CONFIRM_TEXT')} confirmLabel={t('LINKS.UNLINK')} disabled={busy}
|
|
onConfirm={() => run(unlinkGame, 'LINKS.UNLINKED')}/>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|