Files
musicshare/apps/web/src/lib/api.ts
T
chrisandClaude Opus 5.5 45a7bd190a
CI / check (push) Canceled after 0s
Safety screens: followers, blocking, reporting, admin
Web and mobile:
- Follower/following lists (tap the counts), with Follow buttons and
  "Remove" on your own followers.
- Profile ⋯ menu: block (with confirmation) / unblock, and report with a
  reason and optional note. "Follows you" label, "Follow back", and a
  banner on profiles you've blocked.
- Blocked accounts list in Settings, with unblock.
Web only: /admin for the admin account: stats, open/resolved reports,
resolve, suspend/unsuspend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
2026-10-06 12:53:28 -04:00

51 lines
1.8 KiB
TypeScript

'use client';
import useSWR from 'swr';
import { createApiClient, type MeDTO } from '@musicshare/shared';
export const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000';
export class ApiError extends Error {
constructor(
public status: number,
message: string,
) {
super(message);
}
}
export async function api<T = unknown>(path: string, init: { method?: string; body?: unknown } = {}): Promise<T> {
const res = await fetch(API_URL + path, {
method: init.method ?? (init.body ? 'POST' : 'GET'),
credentials: 'include',
headers: init.body !== undefined ? { 'content-type': 'application/json' } : undefined,
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 ?? res.statusText);
return data as T;
}
/** Uploads one file as multipart form data (e.g. the profile picture). */
export async function uploadFile<T = unknown>(path: string, file: File): Promise<T> {
const form = new FormData();
form.append('file', file);
const res = await fetch(API_URL + path, { method: 'POST', credentials: 'include', body: form });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new ApiError(res.status, (data as { error?: string }).error ?? res.statusText);
return data as T;
}
export const fetcher = <T,>(path: string) => api<T>(path);
export function useMe() {
const { data, error, isLoading, mutate } = useSWR<MeDTO, ApiError>('/me', fetcher, {
shouldRetryOnError: false,
revalidateOnFocus: false,
});
return { me: data, isLoading, loggedOut: error?.status === 401, mutate };
}
/** The shared typed client, for calls not covered by `api` above. */
export const client = createApiClient({ baseUrl: API_URL, credentials: 'include' });