jedmund-svelte/src/lib/admin/api.ts
Justin Edmund 4782584a47 fix: replace any types in admin and utility files (11 errors)
Phase 1 Batch 8: Admin & Misc type safety improvements - PHASE 1 COMPLETE

Fixed 11 any-type errors across 7 files:

1. src/lib/admin/autoSave.svelte.ts (1 error)
   - Fixed catch block: e: unknown (line 98)

2. src/lib/admin/autoSave.ts (1 error)
   - Fixed catch block: e: unknown (line 85)

3. src/lib/admin/autoSaveLifecycle.ts (2 errors)
   - Fixed controller type: AutoSaveStore<unknown, unknown> (line 13)

4. src/lib/admin/api.ts (1 error)
   - Fixed body cast: body as FormData (line 61)

5. src/lib/server/api-utils.ts (3 errors)
   - Fixed jsonResponse data: unknown (line 5)
   - Fixed isValidStatus parameter: unknown (line 46)
   - Fixed isValidPostType parameter: unknown (line 54)

6. src/lib/stores/project-form.svelte.ts (1 error)
   - Fixed setField value: unknown (line 57)

7. src/lib/stores/toast.ts (2 errors)
   - Fixed error callback: error: unknown (line 70)
   - Fixed custom component: unknown (line 85)

Progress: 0 any-type errors remaining!
PHASE 1 TYPE SAFETY: COMPLETE (103 errors fixed)
2025-11-24 02:43:52 -08:00

94 lines
2.8 KiB
TypeScript

import { goto } from '$app/navigation'
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
export interface RequestOptions<TBody = unknown> {
method?: HttpMethod
body?: TBody
signal?: AbortSignal
headers?: Record<string, string>
}
export interface ApiError extends Error {
status: number
details?: unknown
}
function getAuthHeader() {
return {}
}
async function handleResponse(res: Response) {
if (res.status === 401) {
// Redirect to login for unauthorized requests
try {
goto('/admin/login')
} catch {
// Ignore navigation errors (e.g., if already on login page)
}
}
const contentType = res.headers.get('content-type') || ''
const isJson = contentType.includes('application/json')
const data = isJson ? await res.json().catch(() => undefined) : undefined
if (!res.ok) {
const err: ApiError = Object.assign(new Error('Request failed'), {
status: res.status,
details: data
})
throw err
}
return data
}
export async function request<TResponse = unknown, TBody = unknown>(
url: string,
opts: RequestOptions<TBody> = {}
): Promise<TResponse> {
const { method = 'GET', body, signal, headers } = opts
const isFormData = typeof FormData !== 'undefined' && body instanceof FormData
const mergedHeaders: Record<string, string> = {
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...getAuthHeader(),
...(headers || {})
}
const res = await fetch(url, {
method,
headers: mergedHeaders,
body: body ? (isFormData ? (body as FormData) : JSON.stringify(body)) : undefined,
signal,
credentials: 'same-origin'
})
return handleResponse(res) as Promise<TResponse>
}
export const api = {
get: <T = unknown>(url: string, opts: Omit<RequestOptions, 'method' | 'body'> = {}) =>
request<T>(url, { ...opts, method: 'GET' }),
post: <T = unknown, B = unknown>(url: string, body: B, opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}) =>
request<T, B>(url, { ...opts, method: 'POST', body }),
put: <T = unknown, B = unknown>(url: string, body: B, opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}) =>
request<T, B>(url, { ...opts, method: 'PUT', body }),
patch: <T = unknown, B = unknown>(url: string, body: B, opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}) =>
request<T, B>(url, { ...opts, method: 'PATCH', body }),
delete: <T = unknown>(url: string, opts: Omit<RequestOptions, 'method' | 'body'> = {}) =>
request<T>(url, { ...opts, method: 'DELETE' })
}
export function createAbortable() {
let controller: AbortController | null = null
return {
nextSignal() {
if (controller) controller.abort()
controller = new AbortController()
return controller.signal
},
abort() {
if (controller) controller.abort()
}
}
}