Add design tokens, base components (page header, section, status chip, empty state, loading, announcer), a11y composables (text scaling, announcements), high-contrast theme and a typed HTTP client with ProblemDetails error mapping.
73 lines
2.0 KiB
TypeScript
73 lines
2.0 KiB
TypeScript
import { ApiError, type ProblemDetails } from './errors'
|
|
|
|
const baseUrl = import.meta.env.VITE_API_BASE_URL ?? ''
|
|
|
|
let tokenProvider: () => string | null = () => null
|
|
|
|
/** Wires the API client to the session store without creating a hard dependency. */
|
|
export function setAuthTokenProvider(provider: () => string | null): void {
|
|
tokenProvider = provider
|
|
}
|
|
|
|
export interface RequestOptions {
|
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
body?: unknown
|
|
headers?: Record<string, string>
|
|
signal?: AbortSignal
|
|
/** Attach the bearer token when available (default: true). */
|
|
auth?: boolean
|
|
}
|
|
|
|
async function parseBody(response: Response): Promise<unknown> {
|
|
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
return undefined
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') ?? ''
|
|
|
|
if (!contentType.includes('application/json')) {
|
|
return undefined
|
|
}
|
|
|
|
try {
|
|
return await response.json()
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Minimal typed fetch wrapper: JSON in/out, bearer auth, RFC 7807 error mapping.
|
|
* Throws `ApiError` on non-2xx responses.
|
|
*/
|
|
export async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
const { method = 'GET', body, headers = {}, signal, auth = true } = options
|
|
|
|
const requestHeaders: Record<string, string> = { Accept: 'application/json', ...headers }
|
|
const token = auth ? tokenProvider() : null
|
|
|
|
if (token) {
|
|
requestHeaders.Authorization = `Bearer ${token}`
|
|
}
|
|
|
|
if (body !== undefined) {
|
|
requestHeaders['Content-Type'] = 'application/json'
|
|
}
|
|
|
|
const response = await fetch(`${baseUrl}${path}`, {
|
|
method,
|
|
headers: requestHeaders,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
signal,
|
|
})
|
|
|
|
const payload = await parseBody(response)
|
|
|
|
if (!response.ok) {
|
|
const problem = (payload as ProblemDetails | undefined) ?? null
|
|
throw new ApiError(response.status, problem)
|
|
}
|
|
|
|
return payload as T
|
|
}
|