Add Next.js web app

Feed with live updates and pinning, profiles with follow/pin, featured song
picker, settings for all music services, and account deletion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-03 09:59:02 -04:00
co-authored by Claude Opus 5.5
parent 6791b62d87
commit 5004beb10d
22 changed files with 1127 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
'use client';
import useSWR from 'swr';
import 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;
}
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 };
}