Files
Mangalord/frontend/src/routes/search/+page.ts
MechaCat02 6c901e64c9 feat(search): tag-based page search surface + per-page tags & collections
Add the /search surface (Pages / Chapters / Mangas tabs) backed by
per-user page tags and per-page collections: schema (migration 0023),
backend endpoints for page tags/collections and tagged-page aggregations
(with the OCR text-search param reserved at 501), plus the frontend API
clients, library Page-tags tab, collection page sections, page context
menu / AddTagsSheet, and reader long-press wiring. Includes the
continuous-reader navigation fixes (?page=N handling, chapter-reset
timing, back-button pops history) and tag-normalization hardening
accumulated on the branch.

Bump version 0.60.2 -> 0.62.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:51:38 +02:00

93 lines
2.9 KiB
TypeScript

import { ApiError } from '$lib/api/client';
import {
listMyDistinctPageTags,
listMyPageTags,
listTaggedChapters,
listTaggedMangas,
type PageTagSummary,
type TaggedChapterAggregate,
type TaggedMangaAggregate,
type TaggedPageItem
} from '$lib/api/page_tags';
import type { PageLoad } from './$types';
export const ssr = false;
type View = 'pages' | 'chapters' | 'mangas';
type Order = 'desc' | 'asc';
/**
* Loader for the /search page. URL is the source of truth — refresh
* or share a link lands the user on the same view.
*
* - `?tag=` exact-match tag filter. Empty → no results, just the
* chip cloud for browsing.
* - `?view=pages|chapters|mangas` — defaults to `pages` when omitted.
* - `?order=desc|asc` — only meaningful for chapters/mangas tabs.
* `desc` (most matches first) is the default.
*
* `?text=` is reserved for the planned OCR text-search input. The
* backend rejects it with 501 + stable code
* `text_search_not_yet_supported` today; the frontend never sets it.
*/
export const load: PageLoad = async ({ url }) => {
const tag = url.searchParams.get('tag');
const viewParam = url.searchParams.get('view');
const view: View =
viewParam === 'chapters' || viewParam === 'mangas' ? viewParam : 'pages';
const order: Order = url.searchParams.get('order') === 'asc' ? 'asc' : 'desc';
const empty = {
authenticated: true,
tag,
view,
order,
distinct: [] as PageTagSummary[],
pages: [] as TaggedPageItem[],
chapters: [] as TaggedChapterAggregate[],
mangas: [] as TaggedMangaAggregate[],
total: 0,
error: null as string | null
};
try {
const distinct = await listMyDistinctPageTags(undefined, 100);
// No tag selected → just the chip cloud.
if (!tag) return { ...empty, distinct };
if (view === 'chapters') {
const r = await listTaggedChapters({ tag, order, limit: 100 });
return {
...empty,
distinct,
chapters: r.items,
total: r.page.total ?? 0
};
}
if (view === 'mangas') {
const r = await listTaggedMangas({ tag, order, limit: 100 });
return {
...empty,
distinct,
mangas: r.items,
total: r.page.total ?? 0
};
}
const r = await listMyPageTags({ tag, limit: 100 });
return {
...empty,
distinct,
pages: r.items,
total: r.page.total ?? 0
};
} catch (e) {
if (e instanceof ApiError && e.status === 401) {
return { ...empty, authenticated: false };
}
if (e instanceof ApiError) {
return { ...empty, error: e.message };
}
throw e;
}
};