Files
History/src/lib/api.ts
T
kamilkrzywda 17d6cecab9
CI / build + unit + e2e (pull_request) Successful in 1m14s
feat: remove a saved game & redirect on missing/deleted game (#20)
- DELETE /api/games/:id route + Db.delete() (404 when absent)
- PresetPicker: per-saved-game trash button + confirm dialog (Cancel/Delete)
- App: runDeleteGame; useSse onError -> goHome on 'deleted'/'not found'
  (a deleted/missing game now returns the live tab and any deep link home)
- DeleteGameResponse in shared/protocol.ts; api.deleteGame()
- unit test (Db.delete) + 2 e2e tests (home-screen delete; in-flight delete
  returns the active tab home and a fresh deep link lands on home)
- version 0.7.0 -> 0.8.0 + changelog

Fixes #20
2026-10-11 20:32:55 +00:00

79 lines
3.1 KiB
TypeScript

// Client-side API helper. All requests are same-origin relative paths (they are
// proxied by Vite in dev and served by Express in prod), so no origin is ever
// hardcoded — this works unchanged behind the Traefik proxy.
import type { GameState, Preset, PresetMeta } from '@shared/types';
import type {
AddActionResponse,
AddDiplomacyResponse,
AdvisorResponse,
CreateGameResponse,
DeleteGameResponse,
GameSummary,
RemoveQueueItemResponse,
TurnResponse,
} from '@shared/protocol';
async function req<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, {
headers: { 'content-type': 'application/json' },
...init,
});
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(body.error ?? `HTTP ${res.status}`);
}
return res.json() as Promise<T>;
}
export const api = {
listPresets: () => req<{ presets: PresetMeta[] }>('/api/presets'),
getPreset: (id: string) => req<{ preset: Preset }>(`/api/presets/${id}`),
createGame: (presetId: string, playerNationId: string) =>
req<CreateGameResponse>('/api/games', {
method: 'POST',
body: JSON.stringify({ presetId, playerNationId }),
}),
getGame: (id: string) => req<{ game: GameState }>(`/api/games/${id}`),
// Delete a saved game entirely. Fast, non-LLM. Returns { deleted: true }.
deleteGame: (id: string) =>
req<DeleteGameResponse>(`/api/games/${id}`, { method: 'DELETE' }),
// Every saved game as a lightweight summary, newest-updated first (for the
// home screen's "Saved games" section / resume affordances).
listGames: () => req<{ games: GameSummary[] }>('/api/games'),
// Queue a free-text player action onto the pending round. No LLM call — the
// round is only committed by `jump`. Returns the updated game (with pending).
addAction: (id: string, text: string) =>
req<AddActionResponse>(`/api/games/${id}/action`, {
method: 'POST',
body: JSON.stringify({ text }),
}),
// Queue a diplomatic message to a target nation onto the pending round.
addDiplomacy: (id: string, to: string, message: string) =>
req<AddDiplomacyResponse>(`/api/games/${id}/diplomacy`, {
method: 'POST',
body: JSON.stringify({ to, message }),
}),
// Remove one queued item from the pending round, by its index in `game.pending`.
// Fast, non-LLM — no turn advance. Returns the updated game.
removeQueueItem: (id: string, index: number) =>
req<RemoveQueueItemResponse>(`/api/games/${id}/queue/remove`, {
method: 'POST',
body: JSON.stringify({ index }),
}),
// Commit the whole pending round (or an empty one) in a single LLM turn.
jump: (id: string) =>
req<TurnResponse>(`/api/games/${id}/jump`, {
method: 'POST',
body: JSON.stringify({}),
}),
advisor: (id: string, prompt?: string) =>
req<AdvisorResponse>(`/api/games/${id}/advisor`, {
method: 'POST',
body: JSON.stringify({ prompt }),
}),
map: (id: string) =>
req<{ regions: Record<string, { color: string; owner: string; ownerId: string }> }>(
`/api/games/${id}/map`,
),
};