refactor(hooks): add useSetStatus for game status saves
Build, Test & Push Frontend / quality-check (push) Successful in 37s
Build, Test & Push Frontend / docker (push) Successful in 23s

Replaces 5 copies of the save code in hints, PlayNextPage and sync.
An empty 2xx body now counts as saved, not a network error.
Refs #46
This commit is contained in:
lobo committed 2026-10-11 17:55:30 +02:00
1 parent bbc707de87
commit 73ad4fd3b8
10 files changed
+239 -114

No files matched your search

+3 -24
View File
@@ -1,11 +1,9 @@
import {useState} from 'react';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
import {toast} from 'sonner';
import {Loader2, Trophy} from 'lucide-react'; import {Loader2, Trophy} from 'lucide-react';
import {Button} from '@/components/ui/button'; import {Button} from '@/components/ui/button';
import {Card} from '@/components/ui/card'; import {Card} from '@/components/ui/card';
import {saveGameStatus} from '../utils/backlog.js'; import useSetStatus from '../hooks/useSetStatus.js';
import {allAchievementsUnlocked} from '../utils/confetti.js'; import {allAchievementsUnlocked} from '../utils/confetti.js';
/** /**
@@ -15,36 +13,17 @@ import {allAchievementsUnlocked} from '../utils/confetti.js';
*/ */
export default function CompletedHint({game, onChanged}) { export default function CompletedHint({game, onChanged}) {
const {t} = useTranslation(); const {t} = useTranslation();
const [busy, setBusy] = useState(false); const {setStatus, busy} = useSetStatus(game.appid, {onChanged});
if (!allAchievementsUnlocked(game) || game.status === 'COMPLETED') return null; if (!allAchievementsUnlocked(game) || game.status === 'COMPLETED') return null;
const setCompleted = async () => {
setBusy(true);
try {
const response = await saveGameStatus(game.appid, 'COMPLETED');
if (response.status === 401) return;
if (!response.ok) {
toast.error(t('BACKLOG.SAVE_FAILED'));
return;
}
onChanged(await response.json());
toast.success(t('BACKLOG.SAVED'));
} catch (err) {
console.error('set completed error:', err);
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
} finally {
setBusy(false);
}
};
return ( return (
<Card className="gap-3 border-amber-500/40 bg-amber-500/5 p-4 sm:flex-row sm:items-center sm:gap-4"> <Card className="gap-3 border-amber-500/40 bg-amber-500/5 p-4 sm:flex-row sm:items-center sm:gap-4">
<div className="flex flex-1 items-start gap-3 sm:items-center"> <div className="flex flex-1 items-start gap-3 sm:items-center">
<Trophy className="size-5 shrink-0 text-amber-500"/> <Trophy className="size-5 shrink-0 text-amber-500"/>
<p className="text-sm">{t('GAME_DETAIL.ALL_UNLOCKED')}</p> <p className="text-sm">{t('GAME_DETAIL.ALL_UNLOCKED')}</p>
</div> </div>
<Button size="sm" className="self-end sm:self-auto" onClick={setCompleted} disabled={busy}> <Button size="sm" className="self-end sm:self-auto" onClick={() => setStatus('COMPLETED')} disabled={busy}>
{busy && <Loader2 className="animate-spin"/>} {busy && <Loader2 className="animate-spin"/>}
{t('GAME_DETAIL.SET_COMPLETED')} {t('GAME_DETAIL.SET_COMPLETED')}
</Button> </Button>
+1
View File
@@ -7,6 +7,7 @@ import CompletedHint from './CompletedHint.jsx';
vi.mock('react-i18next', () => import('../test/i18n-mock.js')); vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}})); vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
vi.mock('../hooks/usePointsCheck.js', () => ({default: () => vi.fn()}));
const game = {appid: 220, status: 'FINISHED', hasAchievements: true, statsVisible: true, achievementsTotal: 10, achievementsUnlocked: 10}; const game = {appid: 220, status: 'FINISHED', hasAchievements: true, statsVisible: true, achievementsTotal: 10, achievementsUnlocked: 10};
const renderHint = (props) => render(<MemoryRouter><CompletedHint game={game} onChanged={() => {}} {...props}/></MemoryRouter>); const renderHint = (props) => render(<MemoryRouter><CompletedHint game={game} onChanged={() => {}} {...props}/></MemoryRouter>);
+11 -15
View File
@@ -5,7 +5,8 @@ import {Flag, Loader2} from 'lucide-react';
import {Button} from '@/components/ui/button'; import {Button} from '@/components/ui/button';
import {Card} from '@/components/ui/card'; import {Card} from '@/components/ui/card';
import {dismissFinishedHint, saveGameStatus} from '../utils/backlog.js'; import useSetStatus from '../hooks/useSetStatus.js';
import {dismissFinishedHint} from '../utils/backlog.js';
import {formatPlaytime} from '../utils/games.js'; import {formatPlaytime} from '../utils/games.js';
/** /**
@@ -16,37 +17,32 @@ import {formatPlaytime} from '../utils/games.js';
*/ */
export default function FinishedHint({game, onChanged, onDismissed}) { export default function FinishedHint({game, onChanged, onDismissed}) {
const {t} = useTranslation(); const {t} = useTranslation();
const [busy, setBusy] = useState(false); const {setStatus, busy: saving} = useSetStatus(game.appid, {onChanged});
const [dismissing, setDismissing] = useState(false);
const busy = saving || dismissing;
const hint = game.finishedHint; const hint = game.finishedHint;
// A status saved elsewhere on the page answers the question too. // A status saved elsewhere on the page answers the question too.
if (!hint || (game.status !== 'BACKLOG' && game.status !== 'PLAYING')) return null; if (!hint || (game.status !== 'BACKLOG' && game.status !== 'PLAYING')) return null;
const run = async (request, onOk) => { const notYet = async () => {
setBusy(true); setDismissing(true);
try { try {
const response = await request(); const response = await dismissFinishedHint(game.appid, hint.type);
if (response.status === 401) return; if (response.status === 401) return;
if (!response.ok) { if (!response.ok) {
toast.error(t('BACKLOG.SAVE_FAILED')); toast.error(t('BACKLOG.SAVE_FAILED'));
return; return;
} }
await onOk(response); onDismissed();
} catch (err) { } catch (err) {
console.error('finished hint error:', err); console.error('finished hint error:', err);
toast.error(t('INFO.ERROR.NETWORK_FAILURE')); toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
} finally { } finally {
setBusy(false); setDismissing(false);
} }
}; };
const setFinished = () => run(() => saveGameStatus(game.appid, 'FINISHED'), async (response) => {
onChanged(await response.json());
toast.success(t('BACKLOG.SAVED'));
});
const notYet = () => run(() => dismissFinishedHint(game.appid, hint.type), () => onDismissed());
const text = hint.type === 'ENDING' const text = hint.type === 'ENDING'
? t('GAME_DETAIL.FINISHED_ENDING', {achievement: hint.achievement}) ? t('GAME_DETAIL.FINISHED_ENDING', {achievement: hint.achievement})
: t('GAME_DETAIL.FINISHED_PLAYTIME', { : t('GAME_DETAIL.FINISHED_PLAYTIME', {
@@ -62,7 +58,7 @@ export default function FinishedHint({game, onChanged, onDismissed}) {
</div> </div>
<div className="flex flex-wrap justify-end gap-2"> <div className="flex flex-wrap justify-end gap-2">
<Button size="sm" variant="outline" onClick={notYet} disabled={busy}>{t('GAME_DETAIL.NOT_YET')}</Button> <Button size="sm" variant="outline" onClick={notYet} disabled={busy}>{t('GAME_DETAIL.NOT_YET')}</Button>
<Button size="sm" onClick={setFinished} disabled={busy}> <Button size="sm" onClick={() => setStatus('FINISHED')} disabled={busy}>
{busy && <Loader2 className="animate-spin"/>} {busy && <Loader2 className="animate-spin"/>}
{t('GAME_DETAIL.SET_FINISHED')} {t('GAME_DETAIL.SET_FINISHED')}
</Button> </Button>
+1
View File
@@ -8,6 +8,7 @@ import {fetchRoutes, json} from '../test/fetchRoutes.js';
vi.mock('react-i18next', () => import('../test/i18n-mock.js')); vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}})); vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
vi.mock('../hooks/usePointsCheck.js', () => ({default: () => vi.fn()}));
const ending = {appid: 322110, status: 'PLAYING', playtimeMinutes: 600, timeToBeat: {normallyMinutes: 500}, const ending = {appid: 322110, status: 'PLAYING', playtimeMinutes: 600, timeToBeat: {normallyMinutes: 500},
finishedHint: {type: 'ENDING', achievement: 'Survivor'}}; finishedHint: {type: 'ENDING', achievement: 'Survivor'}};
+3 -24
View File
@@ -1,11 +1,9 @@
import {useState} from 'react';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
import {toast} from 'sonner';
import {Gamepad2, Loader2} from 'lucide-react'; import {Gamepad2, Loader2} from 'lucide-react';
import {Button} from '@/components/ui/button'; import {Button} from '@/components/ui/button';
import {Card} from '@/components/ui/card'; import {Card} from '@/components/ui/card';
import {saveGameStatus} from '../utils/backlog.js'; import useSetStatus from '../hooks/useSetStatus.js';
import {formatPlaytime} from '../utils/games.js'; import {formatPlaytime} from '../utils/games.js';
/** /**
@@ -15,29 +13,10 @@ import {formatPlaytime} from '../utils/games.js';
*/ */
export default function PlayingHint({game, onChanged}) { export default function PlayingHint({game, onChanged}) {
const {t} = useTranslation(); const {t} = useTranslation();
const [busy, setBusy] = useState(false); const {setStatus, busy} = useSetStatus(game.appid, {onChanged});
if (game.status !== 'BACKLOG' || !(game.playtime2Weeks > 0)) return null; if (game.status !== 'BACKLOG' || !(game.playtime2Weeks > 0)) return null;
const setPlaying = async () => {
setBusy(true);
try {
const response = await saveGameStatus(game.appid, 'PLAYING');
if (response.status === 401) return;
if (!response.ok) {
toast.error(t('BACKLOG.SAVE_FAILED'));
return;
}
onChanged(await response.json());
toast.success(t('BACKLOG.SAVED'));
} catch (err) {
console.error('set playing error:', err);
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
} finally {
setBusy(false);
}
};
return ( return (
<Card className="gap-3 border-primary/40 bg-primary/5 p-4 sm:flex-row sm:items-center sm:gap-4"> <Card className="gap-3 border-primary/40 bg-primary/5 p-4 sm:flex-row sm:items-center sm:gap-4">
<div className="flex flex-1 items-start gap-3 sm:items-center"> <div className="flex flex-1 items-start gap-3 sm:items-center">
@@ -46,7 +25,7 @@ export default function PlayingHint({game, onChanged}) {
{t('GAME_DETAIL.PLAYED_RECENTLY', {time: formatPlaytime(game.playtime2Weeks)})} {t('GAME_DETAIL.PLAYED_RECENTLY', {time: formatPlaytime(game.playtime2Weeks)})}
</p> </p>
</div> </div>
<Button size="sm" className="self-end sm:self-auto" onClick={setPlaying} disabled={busy}> <Button size="sm" className="self-end sm:self-auto" onClick={() => setStatus('PLAYING')} disabled={busy}>
{busy && <Loader2 className="animate-spin"/>} {busy && <Loader2 className="animate-spin"/>}
{t('GAME_DETAIL.SET_PLAYING')} {t('GAME_DETAIL.SET_PLAYING')}
</Button> </Button>
+1
View File
@@ -7,6 +7,7 @@ import PlayingHint from './PlayingHint.jsx';
vi.mock('react-i18next', () => import('../test/i18n-mock.js')); vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}})); vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
vi.mock('../hooks/usePointsCheck.js', () => ({default: () => vi.fn()}));
const game = {appid: 220, status: 'BACKLOG', rating: 3, notes: 'keep', priority: 'HIGH', playtime2Weeks: 90}; const game = {appid: 220, status: 'BACKLOG', rating: 3, notes: 'keep', priority: 'HIGH', playtime2Weeks: 90};
const renderHint = (props) => render(<MemoryRouter><PlayingHint game={game} onChanged={() => {}} {...props}/></MemoryRouter>); const renderHint = (props) => render(<MemoryRouter><PlayingHint game={game} onChanged={() => {}} {...props}/></MemoryRouter>);
+8 -26
View File
@@ -1,11 +1,9 @@
import {useState} from 'react'; import {useState} from 'react';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
import {toast} from 'sonner';
import {Loader2} from 'lucide-react'; import {Loader2} from 'lucide-react';
import {Button} from '@/components/ui/button'; import {Button} from '@/components/ui/button';
import usePointsCheck from '../hooks/usePointsCheck.js'; import useSetStatus from '../hooks/useSetStatus.js';
import {saveGameStatus} from '../utils/backlog.js';
/** /**
* Sets a game to COMPLETED from the sync details, the same call as {@link CompletedHint}. * Sets a game to COMPLETED from the sync details, the same call as {@link CompletedHint}.
@@ -14,34 +12,18 @@ import {saveGameStatus} from '../utils/backlog.js';
*/ */
export default function SyncCompletedButton({appid, onChanged}) { export default function SyncCompletedButton({appid, onChanged}) {
const {t} = useTranslation(); const {t} = useTranslation();
const check = usePointsCheck();
const [busy, setBusy] = useState(false);
const [done, setDone] = useState(false); const [done, setDone] = useState(false);
const {setStatus, busy} = useSetStatus(appid, {
const setCompleted = async () => { checkPoints: true,
setBusy(true); onChanged: (entry) => {
try {
const response = await saveGameStatus(appid, 'COMPLETED');
if (response.status === 401) return;
if (!response.ok) {
toast.error(t('BACKLOG.SAVE_FAILED'));
return;
}
setDone(true); setDone(true);
toast.success(t('BACKLOG.SAVED')); onChanged?.(entry);
check(appid, {status: 'COMPLETED', tell: true}); },
onChanged?.(); });
} catch (err) {
console.error('set completed error:', err);
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
} finally {
setBusy(false);
}
};
if (done) return <span className="text-sm text-muted-foreground">{t('SYNC_CHANGES.COMPLETED_DONE')}</span>; if (done) return <span className="text-sm text-muted-foreground">{t('SYNC_CHANGES.COMPLETED_DONE')}</span>;
return ( return (
<Button size="sm" className="min-h-11 shrink-0" onClick={setCompleted} disabled={busy}> <Button size="sm" className="min-h-11 shrink-0" onClick={() => setStatus('COMPLETED')} disabled={busy}>
{busy && <Loader2 className="animate-spin"/>} {busy && <Loader2 className="animate-spin"/>}
{t('GAME_DETAIL.SET_COMPLETED')} {t('GAME_DETAIL.SET_COMPLETED')}
</Button> </Button>
+48
View File
@@ -0,0 +1,48 @@
import {useRef, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {toast} from 'sonner';
import {saveGameStatus} from '../utils/backlog.js';
import usePointsCheck from './usePointsCheck.js';
/**
* Returns `{setStatus, busy}`. `setStatus(status)` saves the status (PUT .../status), shows the saved or error toast,
* with `checkPoints` asks the points check with its toast, then calls `onChanged(entry)`. `entry` is the saved backlog
* entry, or null when the body of a successful answer is empty or not JSON. A second call while one runs does nothing.
* Resolves to true when saved.
*
* GameDetail's GamePointsLine already tells the points; do not set `checkPoints` there.
*/
export default function useSetStatus(appid, {onChanged, checkPoints = false} = {}) {
const {t} = useTranslation();
const check = usePointsCheck();
const [busy, setBusy] = useState(false);
const running = useRef(false);
const setStatus = async (status) => {
if (running.current) return false;
running.current = true;
setBusy(true);
try {
const response = await saveGameStatus(appid, status);
if (response.status === 401) return false;
if (!response.ok) {
toast.error(t('BACKLOG.SAVE_FAILED'));
return false;
}
const entry = await response.json().catch(() => null);
toast.success(t('BACKLOG.SAVED'));
if (checkPoints) check(appid, {status, tell: true});
onChanged?.(entry);
return true;
} catch (err) {
console.error('set status error:', err);
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
return false;
} finally {
running.current = false;
setBusy(false);
}
};
return {setStatus, busy};
}
+160
View File
@@ -0,0 +1,160 @@
// @vitest-environment jsdom
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {act, cleanup, renderHook} from '@testing-library/react';
import {toast} from 'sonner';
import useSetStatus from './useSetStatus.js';
import {fetchRoutes, json} from '../test/fetchRoutes.js';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
const pointsCheck = vi.fn(() => Promise.resolve(null));
vi.mock('./usePointsCheck.js', () => ({default: () => pointsCheck}));
const PUT = 'PUT /api/library/games/220/status';
beforeEach(() => {
localStorage.setItem('tbp_session', '1');
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
localStorage.clear();
});
const render = (options) => renderHook(() => useSetStatus(220, options));
describe('useSetStatus', () => {
it('savesTheStatusAndShowsTheSavedToast', async () => {
const fetchMock = fetchRoutes({[PUT]: () => json(200, {status: 'PLAYING'})});
const {result} = render();
let saved;
await act(async () => {
saved = await result.current.setStatus('PLAYING');
});
expect(saved).toBe(true);
expect(toast.success).toHaveBeenCalledWith('BACKLOG.SAVED');
const [, init] = fetchMock.mock.calls[0];
expect(JSON.parse(init.body)).toEqual({status: 'PLAYING'});
});
it('passesTheSavedEntryToOnChanged', async () => {
fetchRoutes({[PUT]: () => json(200, {status: 'PLAYING', appid: 220})});
const onChanged = vi.fn();
const {result} = render({onChanged});
await act(() => result.current.setStatus('PLAYING'));
expect(onChanged).toHaveBeenCalledTimes(1);
expect(onChanged).toHaveBeenCalledWith({status: 'PLAYING', appid: 220});
});
it('countsAnEmptyBodyAsSavedAndPassesNull', async () => {
fetchRoutes({[PUT]: () => new Response(null, {status: 200})});
const onChanged = vi.fn();
const {result} = render({onChanged});
let saved;
await act(async () => {
saved = await result.current.setStatus('PLAYING');
});
expect(saved).toBe(true);
expect(toast.success).toHaveBeenCalledWith('BACKLOG.SAVED');
expect(toast.error).not.toHaveBeenCalled();
expect(onChanged).toHaveBeenCalledWith(null);
});
it('showsSaveFailedOnAServerError', async () => {
fetchRoutes({[PUT]: () => json(500, {})});
const onChanged = vi.fn();
const {result} = render({onChanged});
await act(() => result.current.setStatus('PLAYING'));
expect(toast.error).toHaveBeenCalledWith('BACKLOG.SAVE_FAILED');
expect(onChanged).not.toHaveBeenCalled();
expect(result.current.busy).toBe(false);
});
it('showsTheNetworkErrorWhenFetchRejects', async () => {
vi.spyOn(console, 'error').mockImplementation(() => {});
fetchRoutes({[PUT]: () => {
throw new Error('offline');
}});
const {result} = render();
await act(() => result.current.setStatus('PLAYING'));
expect(toast.error).toHaveBeenCalledWith('INFO.ERROR.NETWORK_FAILURE');
});
it('staysSilentOn401', async () => {
fetchRoutes({[PUT]: () => json(401, {}), 'POST /api/auth/refresh': () => json(401, {})});
const onChanged = vi.fn();
const {result} = render({onChanged});
let saved;
await act(async () => {
saved = await result.current.setStatus('PLAYING');
});
expect(saved).toBe(false);
expect(toast.error).not.toHaveBeenCalled();
expect(toast.success).not.toHaveBeenCalled();
expect(onChanged).not.toHaveBeenCalled();
});
it('sendsOneRequestWhenCalledTwice', async () => {
let release;
const held = new Promise((resolve) => {
release = resolve;
});
const fetchMock = fetchRoutes({[PUT]: () => held});
const {result} = render();
await act(async () => {
const first = result.current.setStatus('PLAYING');
const second = await result.current.setStatus('PLAYING');
expect(second).toBe(false);
release(json(200, {}));
await first;
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('checksPointsOnlyWhenAsked', async () => {
fetchRoutes({[PUT]: () => json(200, {})});
const plain = render();
await act(() => plain.result.current.setStatus('PLAYING'));
expect(pointsCheck).not.toHaveBeenCalled();
const asking = render({checkPoints: true});
await act(() => asking.result.current.setStatus('PLAYING'));
expect(pointsCheck).toHaveBeenCalledWith(220, {status: 'PLAYING', tell: true});
});
it('isBusyWhileSaving', async () => {
let release;
const held = new Promise((resolve) => {
release = resolve;
});
fetchRoutes({[PUT]: () => held});
const {result} = render();
let pending;
await act(async () => {
pending = result.current.setStatus('PLAYING');
});
expect(result.current.busy).toBe(true);
await act(async () => {
release(json(200, {}));
await pending;
});
expect(result.current.busy).toBe(false);
});
});
+3 -25
View File
@@ -11,8 +11,7 @@ import {Skeleton} from '@/components/ui/skeleton';
import GameCover from '../components/GameCover.jsx'; import GameCover from '../components/GameCover.jsx';
import FitText from '../components/FitText.jsx'; import FitText from '../components/FitText.jsx';
import PageContainer from '../components/PageContainer.jsx'; import PageContainer from '../components/PageContainer.jsx';
import usePointsCheck from '../hooks/usePointsCheck.js'; import useSetStatus from '../hooks/useSetStatus.js';
import {saveGameStatus} from '../utils/backlog.js';
import {formatDate} from '../utils/format.js'; import {formatDate} from '../utils/format.js';
import {formatPlaytime} from '../utils/games.js'; import {formatPlaytime} from '../utils/games.js';
import {request} from '../utils/api.js'; import {request} from '../utils/api.js';
@@ -280,31 +279,10 @@ const emptyKey = (budget, mood) => {
function PickCard({game}) { function PickCard({game}) {
const {t, i18n} = useTranslation(); const {t, i18n} = useTranslation();
const [busy, setBusy] = useState(false);
const [started, setStarted] = useState(false); const [started, setStarted] = useState(false);
const check = usePointsCheck(); const {setStatus, busy} = useSetStatus(game.appid, {checkPoints: true, onChanged: () => setStarted(true)});
const reason = reasonText(game.reason, t, i18n.language); const reason = reasonText(game.reason, t, i18n.language);
const startPlaying = async () => {
setBusy(true);
try {
const response = await saveGameStatus(game.appid, 'PLAYING');
if (response.status === 401) return;
if (!response.ok) {
toast.error(t('BACKLOG.SAVE_FAILED'));
return;
}
setStarted(true);
toast.success(t('BACKLOG.SAVED'));
check(game.appid, {status: 'PLAYING', tell: true});
} catch (err) {
console.error('Start playing error:', err);
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
} finally {
setBusy(false);
}
};
return ( return (
<Card className="gap-3 overflow-hidden p-0"> <Card className="gap-3 overflow-hidden p-0">
<Link to={`/library/${game.appid}`} className="block"> <Link to={`/library/${game.appid}`} className="block">
@@ -333,7 +311,7 @@ function PickCard({game}) {
<Check className="size-4"/>{t('PLAY_NEXT.STARTED')} <Check className="size-4"/>{t('PLAY_NEXT.STARTED')}
</p> </p>
) : ( ) : (
<Button size="sm" className="w-full" onClick={startPlaying} disabled={busy}> <Button size="sm" className="w-full" onClick={() => setStatus('PLAYING')} disabled={busy}>
{busy ? <Loader2 className="animate-spin"/> : <Play/>} {busy ? <Loader2 className="animate-spin"/> : <Play/>}
{t('PLAY_NEXT.START_PLAYING')} {t('PLAY_NEXT.START_PLAYING')}
</Button> </Button>