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>
51 lines
1.5 KiB
Rust
51 lines
1.5 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::FromRow;
|
|
use uuid::Uuid;
|
|
|
|
use super::patch::Patch;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
|
pub struct Collection {
|
|
pub id: Uuid,
|
|
pub user_id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Shape returned by `GET /me/collections`. Enriched with the manga
|
|
/// count and up to three sample cover paths so a collection card can
|
|
/// render without extra round-trips.
|
|
#[derive(Debug, Clone, Serialize, FromRow)]
|
|
pub struct CollectionSummary {
|
|
pub id: Uuid,
|
|
pub user_id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub manga_count: i64,
|
|
/// Cover image keys of up to three sample mangas (newest-added
|
|
/// first). `Vec<String>` rather than `Option<...>` so an empty
|
|
/// collection renders as `[]` rather than `null`.
|
|
pub sample_covers: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct NewCollection {
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
pub struct CollectionPatch {
|
|
pub name: Option<String>,
|
|
/// Three-state: missing key leaves description alone; explicit
|
|
/// `null` clears it; a string sets it. See `Patch`.
|
|
#[serde(default)]
|
|
pub description: Patch<String>,
|
|
}
|