import type { BookDto, BookQueryDto, BootstrapAdminDto, CreateLibraryDto, JobDto, LibraryDto, LoginDto, ProgressDto, UpdateProgressDto, UserDto } from "@readabook/shared"; import { mockBooks, mockContinue, mockJobs, mockLibraries, mockProgress, mockUser } from "./mockData"; import type { ContinueItem, Session } from "./types"; const API_BASE = import.meta.env.VITE_API_BASE_URL ?? ""; type RequestOptions = RequestInit & { fallback?: unknown; }; export class ApiFallbackError extends Error { constructor( message: string, public readonly fallback: unknown ) { super(message); } } export function getApiFallback(error: unknown): T | undefined { return error instanceof ApiFallbackError ? (error.fallback as T) : undefined; } async function request(path: string, options: RequestOptions = {}): Promise { try { const response = await fetch(`${API_BASE}${path}`, { ...options, credentials: "include", headers: { "Content-Type": "application/json", ...options.headers } }); if (!response.ok) { const detail = await response.text(); throw new Error(detail || `${response.status} ${response.statusText}`); } return (await response.json()) as T; } catch (error) { if (options.fallback !== undefined) { throw new ApiFallbackError(error instanceof Error ? error.message : String(error), options.fallback); } throw error; } } function queryString(query: Partial): string { const params = new URLSearchParams(); Object.entries(query).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== "") params.set(key, String(value)); }); const value = params.toString(); return value ? `?${value}` : ""; } export const api = { async session(): Promise { try { const result = await request<{ user: UserDto }>("/auth/me"); return { user: result.user, degraded: false }; } catch { return { user: null, degraded: false }; } }, async bootstrap(input: BootstrapAdminDto): Promise { const result = await request("/auth/bootstrap", { method: "POST", body: JSON.stringify(input) }); return result; }, async login(input: LoginDto): Promise { const result = await request<{ user: UserDto }>("/auth/login", { method: "POST", body: JSON.stringify(input) }); return result.user; }, async logout(): Promise { await request<{ ok: true }>("/auth/logout", { method: "POST" }); }, async books(query: Partial = {}): Promise { return request(`/books${queryString({ limit: 50, offset: 0, ...query })}`, { fallback: mockBooks }); }, async search(query: string): Promise { return request(`/books/search${queryString({ q: query, limit: 50, offset: 0 })}`, { fallback: mockBooks }); }, async book(id: number): Promise { const fallback = mockBooks.find((book) => book.id === id) ?? mockBooks[0]; return request(`/books/${id}`, { fallback }); }, bookFileUrl(id: number): string { return `${API_BASE}/books/${id}/file`; }, bookCoverUrl(id: number): string { return `${API_BASE}/books/${id}/cover`; }, async progress(bookId: number): Promise { try { return await request(`/progress/${bookId}`, { fallback: mockProgress.find((progress) => progress.bookId === bookId) ?? null }); } catch (error) { if (error instanceof ApiFallbackError) return error.fallback as ProgressDto | null; return null; } }, async saveProgress(bookId: number, input: UpdateProgressDto): Promise { return request(`/progress/${bookId}`, { method: "PUT", body: JSON.stringify(input), fallback: { bookId, ...input, updatedAt: new Date().toISOString() } }); }, async continueReading(): Promise { return request("/progress/continue", { fallback: mockContinue }); }, async libraries(): Promise { return request("/admin/libraries", { fallback: mockLibraries }); }, async createLibrary(input: CreateLibraryDto): Promise { return request("/admin/libraries", { method: "POST", body: JSON.stringify(input), fallback: { id: Date.now(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...input } }); }, async scanLibrary(id: number): Promise { return request(`/admin/libraries/${id}/scan`, { method: "POST", fallback: mockJobs[0] }); }, async jobs(): Promise { return request("/admin/jobs", { fallback: mockJobs }); }, async users(): Promise { return request("/admin/users", { fallback: [mockUser] }); } };