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.
This commit is contained in:
lobo committed 2026-10-21 11:09:44 +02:00
1 parent f9f0d94e2c
commit 45f5087227
32 files changed
+1091 -352

No files matched your search

+46
View File
@@ -157,4 +157,50 @@ describe('useSetStatus', () => {
expect(result.current.busy).toBe(false);
});
it('showsUndoOnlyWithAPreviousStatusThatDiffers', async () => {
fetchRoutes({[PUT]: () => json(200, {})});
const {result} = render();
await act(() => result.current.setStatus('PLAYING'));
await act(() => result.current.setStatus('PLAYING', {previous: 'PLAYING'}));
expect(toast.success.mock.calls.every(([, options]) => options === undefined)).toBe(true);
await act(() => result.current.setStatus('PLAYING', {previous: 'BACKLOG'}));
const [message, options] = toast.success.mock.calls.at(-1);
expect(message).toBe('BACKLOG.SAVED');
expect(options.action.label).toBe('MANUAL_GAME.UNDO');
expect(options.duration).toBe(8000);
});
it('undoSavesThePreviousStatusAndTellsOnChanged', async () => {
const fetchMock = fetchRoutes({[PUT]: (url, init) => json(200, JSON.parse(init.body))});
const onChanged = vi.fn();
const {result} = render({onChanged, checkPoints: true});
await act(() => result.current.setStatus('PLAYING', {previous: 'BACKLOG'}));
onChanged.mockClear();
pointsCheck.mockClear();
await act(async () => toast.success.mock.calls[0][1].action.onClick());
expect(JSON.parse(fetchMock.mock.calls[1][1].body)).toEqual({status: 'BACKLOG'});
expect(onChanged).toHaveBeenCalledWith({status: 'BACKLOG'});
expect(pointsCheck).toHaveBeenCalledWith(220, {status: 'BACKLOG', tell: true});
// The undo itself has no Undo.
expect(toast.success.mock.calls.at(-1)).toEqual(['BACKLOG.SAVED']);
});
it('undoShowsSaveFailedOnAServerError', async () => {
let calls = 0;
fetchRoutes({[PUT]: () => json(++calls === 1 ? 200 : 500, {})});
const onChanged = vi.fn();
const {result} = render({onChanged});
await act(() => result.current.setStatus('PLAYING', {previous: 'BACKLOG'}));
onChanged.mockClear();
await act(async () => toast.success.mock.calls[0][1].action.onClick());
expect(toast.error).toHaveBeenCalledWith('BACKLOG.SAVE_FAILED');
expect(onChanged).not.toHaveBeenCalled();
});
});