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>
80 lines
2.4 KiB
Rust
80 lines
2.4 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::FromRow;
|
|
use uuid::Uuid;
|
|
|
|
use super::author::AuthorRef;
|
|
use super::genre::GenreRef;
|
|
use super::patch::Patch;
|
|
use super::tag::TagRef;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
|
pub struct Manga {
|
|
pub id: Uuid,
|
|
pub title: String,
|
|
pub status: String,
|
|
pub alt_titles: Vec<String>,
|
|
pub description: Option<String>,
|
|
pub cover_image_path: Option<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Shape returned by list endpoints. Cards show the title, authors and
|
|
/// genres; tags are intentionally elided here to keep the response
|
|
/// proportional to what a card actually renders.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct MangaCard {
|
|
#[serde(flatten)]
|
|
pub manga: Manga,
|
|
pub authors: Vec<AuthorRef>,
|
|
pub genres: Vec<GenreRef>,
|
|
}
|
|
|
|
/// Shape returned by `GET /mangas/:id`. Adds user-added tags on top of
|
|
/// the card fields.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct MangaDetail {
|
|
#[serde(flatten)]
|
|
pub manga: Manga,
|
|
pub authors: Vec<AuthorRef>,
|
|
pub genres: Vec<GenreRef>,
|
|
pub tags: Vec<TagRef>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
pub struct NewManga {
|
|
pub title: String,
|
|
#[serde(default)]
|
|
pub status: Option<String>,
|
|
/// Author display names. Looked up case-insensitively; created on
|
|
/// the fly when unseen.
|
|
#[serde(default)]
|
|
pub authors: Vec<String>,
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
#[serde(default)]
|
|
pub alt_titles: Vec<String>,
|
|
#[serde(default)]
|
|
pub genre_ids: Vec<Uuid>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
pub struct MangaPatch {
|
|
pub title: Option<String>,
|
|
pub status: Option<String>,
|
|
/// Three-state: missing key leaves the description alone; explicit
|
|
/// `null` clears it; a string sets it. See `Patch` for details.
|
|
#[serde(default)]
|
|
pub description: Patch<String>,
|
|
pub alt_titles: Option<Vec<String>>,
|
|
/// When provided, replaces the manga's authors atomically.
|
|
pub authors: Option<Vec<String>>,
|
|
/// When provided, replaces the manga's genres atomically.
|
|
pub genre_ids: Option<Vec<Uuid>>,
|
|
}
|
|
|
|
// `Patch<T>` lives in `super::patch` so other resources (collections,
|
|
// future PATCH endpoints) can reuse the same three-state semantics
|
|
// without re-importing through `manga::`.
|