import { request, type Manga, type MangaStatus, type Page } from './client'; export type MangaSort = 'recent' | 'title'; export type AuthorRef = { id: string; name: string }; export type GenreRef = { id: string; name: string }; export type TagRef = { id: string; name: string; added_by: string | null }; /** Card shape returned by `GET /v1/mangas` — authors + genres, no tags. */ export type MangaCard = Manga & { authors: AuthorRef[]; genres: GenreRef[]; }; /** Detail shape returned by `GET /v1/mangas/:id`. Includes user tags. */ export type MangaDetail = Manga & { authors: AuthorRef[]; genres: GenreRef[]; tags: TagRef[]; }; export type ListOptions = { search?: string; status?: MangaStatus; /** AND across the list — every id must be attached to the manga. */ authorIds?: string[]; genreIds?: string[]; tagIds?: string[]; limit?: number; offset?: number; sort?: MangaSort; }; export type MangasPage = { items: MangaCard[]; page: Page; }; export async function listMangas(opts: ListOptions = {}): Promise { const params = new URLSearchParams(); if (opts.search) params.set('search', opts.search); if (opts.status) params.set('status', opts.status); if (opts.authorIds && opts.authorIds.length) { params.set('author_id', opts.authorIds.join(',')); } if (opts.genreIds && opts.genreIds.length) { params.set('genre_id', opts.genreIds.join(',')); } if (opts.tagIds && opts.tagIds.length) { params.set('tag_id', opts.tagIds.join(',')); } if (opts.limit != null) params.set('limit', String(opts.limit)); if (opts.offset != null) params.set('offset', String(opts.offset)); if (opts.sort) params.set('sort', opts.sort); const qs = params.toString(); return request(`/v1/mangas${qs ? `?${qs}` : ''}`); } export async function getManga(id: string): Promise { return request(`/v1/mangas/${encodeURIComponent(id)}`); } export type NewManga = { title: string; status?: MangaStatus; /** Author display names; resolved server-side, case-insensitive. */ authors?: string[]; description?: string | null; alt_titles?: string[]; genre_ids?: string[]; }; /** * POST /api/v1/mangas is multipart. The metadata part is JSON; the cover * part is the raw image bytes. The browser fills in the multipart boundary * automatically when `body` is a FormData, so we deliberately do not set * Content-Type ourselves. */ export async function createManga( input: NewManga, cover?: Blob ): Promise { const form = new FormData(); form.append( 'metadata', new Blob([JSON.stringify(input)], { type: 'application/json' }) ); if (cover) form.append('cover', cover); return request('/v1/mangas', { method: 'POST', body: form }); } export type MangaPatch = { title?: string; status?: MangaStatus; description?: string | null; alt_titles?: string[]; authors?: string[]; genre_ids?: string[]; }; export async function updateManga( id: string, patch: MangaPatch ): Promise { return request(`/v1/mangas/${encodeURIComponent(id)}`, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify(patch) }); } /** * PUT /api/v1/mangas/:id/cover (multipart). Replaces the cover image and * returns the refreshed detail. As with createManga the browser fills in * the multipart boundary automatically, so we must NOT set Content-Type. */ export async function updateMangaCover( id: string, cover: Blob ): Promise { const form = new FormData(); form.append('cover', cover); return request( `/v1/mangas/${encodeURIComponent(id)}/cover`, { method: 'PUT', body: form } ); } /** DELETE /api/v1/mangas/:id/cover. Returns the refreshed detail. */ export async function deleteMangaCover(id: string): Promise { return request( `/v1/mangas/${encodeURIComponent(id)}/cover`, { method: 'DELETE' } ); } export async function attachTag( mangaId: string, name: string ): Promise { return request(`/v1/mangas/${encodeURIComponent(mangaId)}/tags`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name }) }); } export async function detachTag(mangaId: string, tagId: string): Promise { await request( `/v1/mangas/${encodeURIComponent(mangaId)}/tags/${encodeURIComponent(tagId)}`, { method: 'DELETE' } ); } export type { Manga, MangaStatus, Page };