Files
minstrel/web/src/lib/api/client.ts
T
bvandeusen 13e12d97ae feat(web): add /settings page with ListenBrainz section
Adds api.put helper, ListenBrainz API module with query/mutation
helpers, a /settings route with token + enable/disable UI, and 5
tests. Adds Settings to the Shell nav.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:45:42 -04:00

53 lines
1.6 KiB
TypeScript

export type ApiError = {
code: string;
message: string;
status: number;
};
export type User = {
id: string;
username: string;
is_admin: boolean;
};
export type LoginResponse = {
token: string;
user: User;
};
export async function apiFetch(path: string, init?: RequestInit): Promise<unknown> {
const res = await fetch(path, {
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
...init
});
const body = res.status === 204 ? null : await res.json().catch(() => null);
if (!res.ok) {
if (res.status === 401) {
// Lazy import: auth/store imports from this file, so a top-level
// import would be circular. By the time any 401 actually happens
// at runtime, both modules have finished loading.
const { logout } = await import('$lib/auth/store.svelte');
await logout({ silent: true });
}
const envelope = body && (body as { error?: { code?: string; message?: string } }).error;
const err: ApiError = {
code: envelope?.code ?? 'unknown',
message: envelope?.message ?? res.statusText,
status: res.status
};
throw err;
}
return body;
}
export const api = {
get: <T>(path: string): Promise<T> => apiFetch(path) as Promise<T>,
post: <T>(path: string, body: unknown): Promise<T> =>
apiFetch(path, { method: 'POST', body: JSON.stringify(body) }) as Promise<T>,
put: <T>(path: string, body: unknown): Promise<T> =>
apiFetch(path, { method: 'PUT', body: JSON.stringify(body) }) as Promise<T>,
del: (path: string): Promise<null> =>
apiFetch(path, { method: 'DELETE' }) as Promise<null>
};