import type { ConnectionDTO, FeedItemDTO, MeDTO, ProfileDTO, TrackSearchResultDTO, UserSummaryDTO, } from './dto'; import type { LoginInput, RegisterInput, SetFeaturedInput, UpdateMeInput } from './schemas'; export class ApiError extends Error { constructor( public status: number, message: string, ) { super(message); } } export interface ApiClientOptions { baseUrl: string; /** Bearer token (mobile). Web relies on the session cookie instead. */ getToken?: () => string | null | undefined; /** 'include' for browsers talking to the API cross-origin. */ credentials?: 'include' | 'same-origin' | 'omit'; } export type AuthResponse = { user: MeDTO; token: string }; /** Typed client for the musicshare API, shared by web and mobile. */ export function createApiClient({ baseUrl, getToken, credentials }: ApiClientOptions) { async function request(path: string, init: { method?: string; body?: unknown } = {}): Promise { const headers: Record = {}; const token = getToken?.(); if (token) headers.Authorization = `Bearer ${token}`; if (init.body !== undefined) headers['Content-Type'] = 'application/json'; const res = await fetch(baseUrl + path, { method: init.method ?? (init.body !== undefined ? 'POST' : 'GET'), headers, credentials, body: init.body !== undefined ? JSON.stringify(init.body) : undefined, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new ApiError(res.status, (data as { error?: string }).error ?? `Request failed (${res.status})`); return data as T; } const q = encodeURIComponent; return { request, /** For SWR-style fetchers keyed by path. */ get: (path: string) => request(path), register: (input: RegisterInput) => request('/auth/register', { body: input }), login: (input: LoginInput) => request('/auth/login', { body: input }), logout: () => request('/auth/logout', { method: 'POST', body: {} }), me: () => request('/me'), updateMe: (input: UpdateMeInput) => request('/me', { method: 'PATCH', body: input }), deleteAccount: () => request('/me', { method: 'DELETE' }), setFeatured: (input: SetFeaturedInput) => request('/me/featured', { method: 'PUT', body: input }), clearFeatured: () => request('/me/featured', { method: 'DELETE' }), feed: () => request('/feed'), profile: (username: string) => request(`/users/${q(username)}`), searchUsers: (query: string) => request(`/users/search?q=${q(query)}`), follow: (username: string) => request(`/users/${q(username)}/follow`, { method: 'POST', body: {} }), unfollow: (username: string) => request(`/users/${q(username)}/follow`, { method: 'DELETE' }), pin: (username: string) => request(`/users/${q(username)}/pin`, { method: 'PUT', body: {} }), unpin: (username: string) => request(`/users/${q(username)}/pin`, { method: 'DELETE' }), searchTracks: (query: string) => request(`/tracks/search?q=${q(query)}`), connections: () => request('/connections'), authorizeUrl: (provider: 'spotify' | 'plex', body: { client: 'web' | 'mobile'; returnTo?: string }) => request<{ url: string }>(`/connections/${provider}/authorize`, { body }), connectUsername: (provider: 'lastfm' | 'listenbrainz', username: string) => request(`/connections/${provider}`, { body: { username } }), connectJellyfin: (input: { serverUrl: string; username: string; password: string }) => request('/connections/jellyfin', { body: input }), disconnect: (provider: string) => request(`/connections/${q(provider)}`, { method: 'DELETE' }), }; } export type ApiClient = ReturnType;