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

@@ -16,6 +16,7 @@
import { listTags, type Tag } from '$lib/api/tags';
import { session } from '$lib/session.svelte';
import Chip from '$lib/components/Chip.svelte';
import MangaCard from '$lib/components/MangaCard.svelte';
import Sheet from '$lib/components/Sheet.svelte';
import AddToCollectionModal from '$lib/components/AddToCollectionModal.svelte';
import Plus from '@lucide/svelte/icons/plus';
@@ -62,6 +63,9 @@
const authors = $derived<AuthorRef[]>(manga.authors);
const genres = $derived<GenreRef[]>(manga.genres);
/** Up to 5 mangas with the most similar tags; empty hides the section. */
const similar = $derived(data.similar);
// svelte-ignore state_referenced_locally
let tags = $state<TagRef[]>([...manga.tags]);
// svelte-ignore state_referenced_locally
@@ -605,6 +609,17 @@
{/if}
</section>
{#if similar.length > 0}
<section aria-label="similar mangas" class="similar" data-testid="similar-section">
<h2>Similar</h2>
<ul class="manga-grid" data-testid="similar-list">
{#each similar as m (m.id)}
<MangaCard manga={m} authors={m.authors} genres={m.genres} />
{/each}
</ul>
</section>
{/if}
<Sheet
open={overflowOpen}
title="Actions"
@@ -688,6 +703,12 @@
}
}
.similar {
margin-top: var(--space-6);
}
/* `.manga-grid` is a shared global (see lib/styles/tokens.css). */
.cover {
width: 100%;
height: auto;