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>
41 lines
1.4 KiB
TypeScript
41 lines
1.4 KiB
TypeScript
import { error, redirect } from '@sveltejs/kit';
|
|
import { ApiError } from '$lib/api/client';
|
|
import {
|
|
getCollection,
|
|
listCollectionMangas
|
|
} from '$lib/api/collections';
|
|
import type { PageLoad } from './$types';
|
|
|
|
export const ssr = false;
|
|
|
|
export const load: PageLoad = async ({ params, url }) => {
|
|
try {
|
|
const [collection, mangas] = await Promise.all([
|
|
getCollection(params.id),
|
|
listCollectionMangas(params.id, { limit: 200 })
|
|
]);
|
|
return {
|
|
collection,
|
|
mangas: mangas.items,
|
|
total: mangas.page.total
|
|
};
|
|
} catch (e) {
|
|
if (e instanceof ApiError) {
|
|
// 401 means the user's session is gone — bounce to login
|
|
// and preserve where they wanted to go.
|
|
if (e.status === 401) {
|
|
const next = encodeURIComponent(url.pathname);
|
|
redirect(302, `/login?next=${next}`);
|
|
}
|
|
// 403 (post-Phase-3-polish the backend collapses this to
|
|
// 404 already, but keep the branch for defense-in-depth)
|
|
// and 404 both render the standard not-found page so the
|
|
// URL doesn't disclose collection existence to non-owners.
|
|
if (e.status === 404 || e.status === 403) {
|
|
error(404, 'Collection not found');
|
|
}
|
|
}
|
|
throw e;
|
|
}
|
|
};
|