Files
bookhoard/web/src/api.ts
T
John O'Keefe 77cd268f4e style: format web/src with oxfmt defaults (via vp fmt)
One-time mechanical sweep, isolated from any functional change.
- oxfmt defaults: double quotes, semicolons, 2-space indent, LF,
  printWidth 100, trailing commas, bracket spacing, arrow parens.
- vite.config.ts gains an fmt.ignorePatterns block for web/static:
  vendored/minified assets (htmx.min.js, compiled css) must never
  be reformatted.
- Verified: rebuild after the sweep produces byte-identical JS/CSS
  bundles; only sourcemaps changed (they embed sourcesContent).
2026-10-21 13:18:24 -04:00

140 lines
3.7 KiB
TypeScript

import { Alpine } from "./alpine";
import { showToast } from "./toast";
function getAuthHeader(): string {
const token = localStorage.getItem("token");
return token ? `Bearer ${token}` : "";
}
async function apiGet(url: string): Promise<Response> {
return fetch(`/api${url}`, {
headers: {
Authorization: getAuthHeader(),
"Content-Type": "application/json",
},
});
}
async function apiPost(url: string, data?: unknown): Promise<Response> {
return fetch(`/api${url}`, {
method: "POST",
headers: {
Authorization: getAuthHeader(),
"Content-Type": "application/json",
},
body: data ? JSON.stringify(data) : undefined,
});
}
async function apiPut(url: string, data?: unknown): Promise<Response> {
return fetch(`/api${url}`, {
method: "PUT",
headers: {
Authorization: getAuthHeader(),
"Content-Type": "application/json",
},
body: data ? JSON.stringify(data) : undefined,
});
}
async function apiDelete<T extends object>(url: string, data?: T): Promise<Response> {
return fetch(`/api${url}`, {
method: "DELETE",
headers: {
Authorization: getAuthHeader(),
"Content-Type": "application/json",
},
body: data ? JSON.stringify(data) : undefined,
});
}
async function apiPatch(url: string, data?: unknown): Promise<Response> {
return fetch(`/api${url}`, {
method: "PATCH",
headers: {
Authorization: getAuthHeader(),
"Content-Type": "application/json",
},
body: data ? JSON.stringify(data) : undefined,
});
}
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
return response.json();
}
async function handleVoidResponse(response: Response): Promise<void> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
}
function handleError(error: unknown, context: string): void {
console.error(`${context}:`, error);
const message = error instanceof Error ? error.message : "An unexpected error occurred";
showToast(message, "error");
}
// ============================================================
// Reader API Functions
// ============================================================
interface ReadingProgress {
current_page: number;
total_pages: number;
cfi?: string;
percentage?: number;
last_read_at: string;
}
async function getReaderMetadata(mediaItemId: string): Promise<ReaderMetadata> {
const response = await apiGet(`/media-items/${mediaItemId}`);
return handleResponse<ReaderMetadata>(response);
}
async function updateReadingProgress(
mediaItemId: string,
progress: ReadingProgress,
): Promise<void> {
const response = await apiPut(`/media-items/${mediaItemId}/progress`, progress);
await handleVoidResponse(response);
}
async function getReadingProgress(mediaItemId: string): Promise<ReadingProgress | null> {
const response = await apiGet(`/media-items/${mediaItemId}/progress`);
if (response.status === 404) {
return null;
}
return handleResponse<ReadingProgress>(response);
}
export {
apiGet,
apiPost,
apiPut,
apiDelete,
apiPatch,
handleResponse,
handleVoidResponse,
handleError,
getReaderMetadata,
getReadingProgress, // ADD THIS
updateReadingProgress,
ReadingProgress,
};
Alpine.data("api", () => ({
get: apiGet,
post: apiPost,
put: apiPut,
delete: apiDelete,
patch: apiPatch,
handleResponse: handleResponse,
handleVoidResponse: handleVoidResponse,
handleError: handleError,
}));