CI / check (push) Canceled after 0s
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
51 lines
1.8 KiB
TypeScript
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' });
|