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