feat(spa): add baseline API client for health and session read
This commit is contained in:
1
frontend/.gitignore
vendored
Normal file
1
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
12
frontend/README.md
Normal file
12
frontend/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Frontend API client baseline
|
||||||
|
|
||||||
|
Dette er baseline-klientlaget for SPA-sporet.
|
||||||
|
|
||||||
|
## Kører checks lokalt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
1454
frontend/package-lock.json
generated
Normal file
1454
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
frontend/package.json
Normal file
15
frontend/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "wpp-frontend-api-client-baseline",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"build": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.10",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vitest": "^2.1.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
frontend/src/api/client.ts
Normal file
56
frontend/src/api/client.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { ApiResult, HealthResponse, SessionDetailResponse } from './types';
|
||||||
|
|
||||||
|
export interface ApiClient {
|
||||||
|
health(): Promise<ApiResult<HealthResponse>>;
|
||||||
|
getSession(code: string): Promise<ApiResult<SessionDetailResponse>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createApiClient(baseUrl = '', fetchImpl: typeof fetch = fetch): ApiClient {
|
||||||
|
async function request<T>(path: string): Promise<ApiResult<T>> {
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetchImpl(`${baseUrl}${path}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' }
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 0,
|
||||||
|
error: { kind: 'network', status: 0, message: 'Network error while contacting API' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: unknown;
|
||||||
|
try {
|
||||||
|
payload = await response.json();
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: response.status,
|
||||||
|
error: { kind: 'parse', status: response.status, message: 'Invalid JSON response from API' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: response.status,
|
||||||
|
error: {
|
||||||
|
kind: 'http',
|
||||||
|
status: response.status,
|
||||||
|
message: `HTTP ${response.status}`,
|
||||||
|
payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, status: response.status, data: payload as T };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
health: () => request<HealthResponse>('/healthz'),
|
||||||
|
getSession: (code: string) =>
|
||||||
|
request<SessionDetailResponse>(`/lobby/sessions/${encodeURIComponent(code.trim().toUpperCase())}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
57
frontend/src/api/types.ts
Normal file
57
frontend/src/api/types.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export interface HealthResponse {
|
||||||
|
ok: boolean;
|
||||||
|
service: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionSummary {
|
||||||
|
code: string;
|
||||||
|
status: string;
|
||||||
|
host_id: number | null;
|
||||||
|
current_round: number;
|
||||||
|
players_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionPlayer {
|
||||||
|
id: number;
|
||||||
|
nickname: string;
|
||||||
|
score: number;
|
||||||
|
is_connected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionAnswer {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionRoundQuestion {
|
||||||
|
id: number;
|
||||||
|
round_number: number;
|
||||||
|
prompt: string;
|
||||||
|
shown_at: string;
|
||||||
|
answers: SessionAnswer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PhaseViewModel {
|
||||||
|
phase: string;
|
||||||
|
available_actions: string[];
|
||||||
|
constraints: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionDetailResponse {
|
||||||
|
session: SessionSummary;
|
||||||
|
players: SessionPlayer[];
|
||||||
|
round_question: SessionRoundQuestion | null;
|
||||||
|
phase_view_model: PhaseViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiErrorKind = 'network' | 'http' | 'parse';
|
||||||
|
|
||||||
|
export interface ApiFailure {
|
||||||
|
kind: ApiErrorKind;
|
||||||
|
message: string;
|
||||||
|
status: number;
|
||||||
|
payload?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiResult<T> =
|
||||||
|
| { ok: true; status: number; data: T }
|
||||||
|
| { ok: false; status: number; error: ApiFailure };
|
||||||
12
frontend/tsconfig.json
Normal file
12
frontend/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"types": ["vitest/globals", "node"]
|
||||||
|
},
|
||||||
|
"include": ["src/api", "tests"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user