feat(mangas): add tag-similarity "Similar" section on manga detail

Add GET /v1/mangas/:id/similar returning the top 5 mangas ranked by
Jaccard tag overlap (shared / union), excluding self, as MangaCard items
in a plain { items } object. Surface them in a hidden-when-empty
"Similar" section on the manga detail page, reusing MangaCard.

Backend: new repo::manga::list_similar with a manga_tags self-join; a
shared cards_from_rows helper (also used by list_cards) that loads
authors+genres concurrently; single-sourced column list via MANGA_COLS +
manga_cols(alias) to replace the fragile SELECT_COLS string-splitting.

Frontend: getSimilarMangas client fn (guards a malformed body), loader
fetch with non-critical .catch fallback, and the catalog grid hoisted to
a shared global .manga-grid (lib/styles/tokens.css), de-duplicating the
per-route copies.

Bump version 0.62.0 -> 0.63.0 (backend + frontend in lockstep).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-13 15:48:15 +02:00
parent 6c901e64c9
commit 33f684887d
15 changed files with 423 additions and 54 deletions

View File

@@ -68,6 +68,7 @@ async function mockDetail(
page: number;
} | null;
authed?: boolean;
similar?: Array<Record<string, unknown>>;
} = {}
) {
const authed = opts.authed ?? false;
@@ -128,6 +129,13 @@ async function mockDetail(
})
})
);
await page.route(`**/api/v1/mangas/${mangaId}/similar`, (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: opts.similar ?? [] })
})
);
await page.route(`**/api/v1/me/read-progress/${mangaId}`, (route) =>
route.fulfill({
status: opts.readProgress ? 200 : 404,
@@ -241,4 +249,39 @@ test.describe('mobile manga detail', () => {
await expect(page.getByTestId('manga-title')).toBeVisible();
await expect(page.getByTestId('bookmark-toggle')).toBeVisible();
});
test('renders the Similar section with recommended cards when present', async ({
page
}) => {
await mockDetail(page, {
similar: [
{
id: 'b2222222-2222-2222-2222-222222222222',
title: 'Vinland Saga',
status: 'ongoing',
alt_titles: [],
description: null,
cover_image_path: null,
created_at: '2026-01-01T00:00:00Z',
updated_at: '2026-01-01T00:00:00Z',
authors: [{ id: 'au2', name: 'Makoto Yukimura' }],
genres: []
}
]
});
await page.goto(`/manga/${mangaId}`);
const section = page.getByTestId('similar-section');
await expect(section).toBeVisible();
await expect(section.getByText('Vinland Saga')).toBeVisible();
});
test('omits the Similar section when there are no recommendations', async ({ page }) => {
await mockDetail(page, { similar: [] });
await page.goto(`/manga/${mangaId}`);
// The chapter list is the anchor that the page has rendered.
await expect(page.getByTestId('manga-title')).toBeVisible();
await expect(page.getByTestId('similar-section')).toHaveCount(0);
});
});