Files
ToBePlayed-Frontend/src/components/EditionsCard.jsx
T
lobo 45f5087227 feat(library): set status and rating in one step (#56)
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.
2026-10-21 11:09:44 +02:00

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>
);
}