diff --git a/backend/Cargo.lock b/backend/Cargo.lock index dee5bbe..3561d6a 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mangalord" -version = "0.109.0" +version = "0.109.1" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 08360b1..e9973ba 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.109.0" +version = "0.109.1" edition = "2021" default-run = "mangalord" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 53826f9..9d348d7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mangalord-frontend", - "version": "0.109.0", + "version": "0.109.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mangalord-frontend", - "version": "0.109.0", + "version": "0.109.1", "devDependencies": { "@lucide/svelte": "^1.16.0", "@playwright/test": "^1.48.0", diff --git a/frontend/package.json b/frontend/package.json index 26f61ec..2da7b92 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.109.0", + "version": "0.109.1", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/components/ReactionButtons.svelte b/frontend/src/lib/components/ReactionButtons.svelte index 9da9173..30e243e 100644 --- a/frontend/src/lib/components/ReactionButtons.svelte +++ b/frontend/src/lib/components/ReactionButtons.svelte @@ -18,6 +18,16 @@ let current = $state(initial); let busy = $state(false); + // The detail page reuses this component across manga -> manga navigation + // (similar / recommendation cards), so `current` must re-seed when the + // loader hands us a new manga's reaction. `mangaId` is referenced so the + // effect also re-runs when navigating between two never-reacted mangas + // (both `initial === null`). + $effect(() => { + mangaId; + current = initial; + }); + // Click the active reaction to clear it; click the other to switch. async function apply(next: Reaction) { if (busy) return; diff --git a/frontend/src/lib/components/ReactionButtons.svelte.test.ts b/frontend/src/lib/components/ReactionButtons.svelte.test.ts index ee49c38..49e1f0c 100644 --- a/frontend/src/lib/components/ReactionButtons.svelte.test.ts +++ b/frontend/src/lib/components/ReactionButtons.svelte.test.ts @@ -51,6 +51,24 @@ describe('ReactionButtons', () => { expect(pressed('reaction-like')).toBe('false'); }); + it('resyncs when navigated to a different manga', async () => { + // The detail page component is reused across /manga/A -> /manga/B + // navigations (e.g. clicking a similar or recommendation card), so + // new props must overwrite the locally-held reaction state. + const { rerender } = render(ReactionButtons, { + props: { mangaId: 'm1', initial: 'like' } + }); + expect(pressed('reaction-like')).toBe('true'); + + await rerender({ mangaId: 'm2', initial: 'dislike' }); + expect(pressed('reaction-dislike')).toBe('true'); + expect(pressed('reaction-like')).toBe('false'); + + await rerender({ mangaId: 'm3', initial: null }); + expect(pressed('reaction-like')).toBe('false'); + expect(pressed('reaction-dislike')).toBe('false'); + }); + it('rolls back on failure', async () => { setReaction.mockRejectedValue(new Error('boom')); render(ReactionButtons, { props: { mangaId: 'm1', initial: null } }); diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 32712fd..0b6b2a6 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -311,24 +311,24 @@ } await hydrateFromUrl(); await load(); - // Fetch the "Continue reading" shelf after the catalogue so the - // public browse path stays unauthenticated and unblocked. Returns - // empty for guests (401 swallowed), which hides the shelf. - try { - const progress = await listMyReadProgressOrEmpty(); + // Fetch the personal shelves after the catalogue so the public browse + // path stays unauthenticated and unblocked. Both are independent + // `/me/*` calls, so fire them concurrently rather than in series. + // Each is isolated: a failure (or 401 for guests) hides its own shelf + // without touching the catalogue or the other shelf. + const [progressResult, recsResult] = await Promise.allSettled([ + listMyReadProgressOrEmpty(), + listMyRecommendations() + ]); + if (progressResult.status === 'fulfilled') { // Drop finished series (read to the end, nothing new) — a // "Continue reading" shelf is for what's still in progress. - continueEntries = progress.items.filter((e) => !isCaughtUp(e)); - } catch { - // Never let a history hiccup break the catalogue — leave the - // shelf hidden. + continueEntries = progressResult.value.items.filter((e) => !isCaughtUp(e)); } - try { + if (recsResult.status === 'fulfilled') { // Personal "Recommended for you" feed (empty for guests / no // taste signals yet → shelf hidden). - recommendations = await listMyRecommendations(); - } catch { - // Non-critical — leave the shelf hidden on failure. + recommendations = recsResult.value; } });