/** * Browser-side client for the user's roaming reading preferences. * * The BFF stores prefs as opaque JSONB keyed off the user. This module * just round-trips that blob — it makes no claim about its shape (that * contract is owned by components/reading-settings.tsx). All calls run * with `credentials: "include"` so the auth cookie comes along. */ const BFF_BASE = process.env.NEXT_PUBLIC_BFF_URL || "http://localhost:8080"; type Envelope = { success?: boolean; data?: T; error?: { code: number; message: string }; }; async function call(path: string, init?: RequestInit): Promise { const res = await fetch(`${BFF_BASE}/api/v1${path}`, { credentials: "include", headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) }, ...init, }); if (res.status === 401 || res.status === 403) return null; if (!res.ok) { throw new Error(`reading-prefs ${path}: ${res.status}`); } const body = (await res.json().catch(() => ({}))) as Envelope; return (body.data ?? null) as T | null; } export const readingPrefsApi = { /** Fetches the caller's reading prefs blob. Returns `null` if the * request is unauthenticated (treat as "no remote prefs"). The blob * is the raw JSON object the frontend wrote last; an empty object * on first load. */ async get(): Promise | null> { const data = await call>("/users/me/reading-prefs"); return data ?? null; }, /** Overwrites the caller's reading prefs blob. The server stores the * payload verbatim (after canonicalising via re-marshal) — schema is * client-owned. Errors are swallowed by the caller; reading prefs * should never block a UI interaction. */ async put(prefs: Record): Promise { await call>("/users/me/reading-prefs", { method: "PUT", body: JSON.stringify(prefs), }); }, };