feat: manga collections (0.17.0)

User-owned named lists of mangas with an add-to-collection modal on
the manga page and dedicated /collections and /collections/:id pages.

- Schema (0010): `collections` (per-user case-insensitive name
  uniqueness) + `collection_mangas` join with cascade FKs.
- Endpoints: full CRUD on `/v1/collections`, idempotent add/remove
  for `/v1/collections/:id/mangas`, and `/v1/mangas/:id/my-collections`
  for the modal's pre-checked state. Owner-mismatch surfaces as 404
  (not 403) so the API doesn't disclose collection existence to
  non-owners; the frontend funnels 401 to /login. Three-state PATCH
  via a new shared `domain::patch::Patch<T>` lets clients distinguish
  "leave alone", "clear", and "set" for description.
- Frontend: reusable `Modal` component (focus trap, opt-in
  backdrop close, ESC) and `AddToCollectionModal` with optimistic
  toggling that's race-safe under fast clicks. /collections page
  renders cover-collage cards; /collections/:id is editable with
  per-card remove. Top nav gets a Collections link.

155 backend tests (incl. 21 collection tests covering ownership,
idempotence, sample-cover enrichment, three-state PATCH, FK race);
88 frontend tests; svelte-check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-17 17:43:06 +02:00
parent 5e92a2c450
commit 274cc819ca
24 changed files with 2689 additions and 100 deletions

View File

@@ -0,0 +1,31 @@
-- User-owned manga collections. Each user can curate any number of
-- named lists (e.g., "Favorites", "Reading list"); mangas can belong
-- to many collections of many users without restriction.
CREATE TABLE collections (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name text NOT NULL,
description text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Per-user case-insensitive name uniqueness so "Favorites" and
-- "favorites" don't both end up in someone's sidebar.
CREATE UNIQUE INDEX collections_user_name_lower_uniq
ON collections (user_id, lower(name));
CREATE INDEX collections_user_idx ON collections (user_id, created_at DESC);
CREATE TABLE collection_mangas (
collection_id uuid NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
manga_id uuid NOT NULL REFERENCES mangas(id) ON DELETE CASCADE,
added_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (collection_id, manga_id)
);
-- Reverse lookup: which collections contain this manga? Used by the
-- "Add to collection" modal to pre-check the boxes for the user's
-- collections this manga is already in.
CREATE INDEX collection_mangas_manga_idx ON collection_mangas (manga_id);