Add frontend API client and TanStack Query hooks

Install @tanstack/react-query, create a fetch-based API client with typed
functions for all endpoints, and add query/mutation hooks for games, pokemon,
runs, and encounters. Includes Vite dev proxy for /api and QueryClientProvider
setup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-05 15:09:14 +01:00
parent 13e90eb308
commit 7c65775c8b
15 changed files with 371 additions and 19 deletions

View File

@@ -0,0 +1,51 @@
const API_BASE = import.meta.env.VITE_API_URL ?? ''
export class ApiError extends Error {
status: number
constructor(status: number, message: string) {
super(message)
this.name = 'ApiError'
this.status = status
}
}
async function request<T>(
path: string,
options?: RequestInit,
): Promise<T> {
const res = await fetch(`${API_BASE}/api/v1${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new ApiError(res.status, body.detail ?? res.statusText)
}
if (res.status === 204) return undefined as T
return res.json()
}
export const api = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body: unknown) =>
request<T>(path, {
method: 'POST',
body: JSON.stringify(body),
}),
patch: <T>(path: string, body: unknown) =>
request<T>(path, {
method: 'PATCH',
body: JSON.stringify(body),
}),
del: <T = void>(path: string) =>
request<T>(path, { method: 'DELETE' }),
}

View File

@@ -0,0 +1,24 @@
import { api } from './client'
import type {
Encounter,
CreateEncounterInput,
UpdateEncounterInput,
} from '../types/game'
export function createEncounter(
runId: number,
data: CreateEncounterInput,
): Promise<Encounter> {
return api.post(`/runs/${runId}/encounters`, data)
}
export function updateEncounter(
id: number,
data: UpdateEncounterInput,
): Promise<Encounter> {
return api.patch(`/encounters/${id}`, data)
}
export function deleteEncounter(id: number): Promise<void> {
return api.del(`/encounters/${id}`)
}

22
frontend/src/api/games.ts Normal file
View File

@@ -0,0 +1,22 @@
import { api } from './client'
import type { Game, Route, RouteEncounter } from '../types/game'
export interface GameDetail extends Game {
routes: Route[]
}
export function getGames(): Promise<Game[]> {
return api.get('/games')
}
export function getGame(id: number): Promise<GameDetail> {
return api.get(`/games/${id}`)
}
export function getGameRoutes(gameId: number): Promise<Route[]> {
return api.get(`/games/${gameId}/routes`)
}
export function getRoutePokemon(routeId: number): Promise<RouteEncounter[]> {
return api.get(`/routes/${routeId}/pokemon`)
}

View File

@@ -0,0 +1,6 @@
import { api } from './client'
import type { Pokemon } from '../types/game'
export function getPokemon(id: number): Promise<Pokemon> {
return api.get(`/pokemon/${id}`)
}

30
frontend/src/api/runs.ts Normal file
View File

@@ -0,0 +1,30 @@
import { api } from './client'
import type {
NuzlockeRun,
RunDetail,
CreateRunInput,
UpdateRunInput,
} from '../types/game'
export function getRuns(): Promise<NuzlockeRun[]> {
return api.get('/runs')
}
export function getRun(id: number): Promise<RunDetail> {
return api.get(`/runs/${id}`)
}
export function createRun(data: CreateRunInput): Promise<NuzlockeRun> {
return api.post('/runs', data)
}
export function updateRun(
id: number,
data: UpdateRunInput,
): Promise<NuzlockeRun> {
return api.patch(`/runs/${id}`, data)
}
export function deleteRun(id: number): Promise<void> {
return api.del(`/runs/${id}`)
}