feat(spa): guard host/player API contract with typed client calls
All checks were successful
CI / test-and-quality (push) Successful in 2m13s
CI / test-and-quality (pull_request) Successful in 2m9s

This commit is contained in:
2026-03-01 16:20:10 +00:00
parent 82711dd537
commit 9a69110c7d
4 changed files with 226 additions and 92 deletions

View File

@@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { createApiClient } from '../../../../../src/api/client';
import type { FinishGameResponse, ScoreboardResponse } from '../../../../../src/api/types';
import { createVerticalSliceController } from '../../../../../src/spa/vertical-slice';
interface SessionDetail {
@@ -11,17 +12,8 @@ interface SessionDetail {
players: Array<{ id: number; nickname: string; score: number }>;
}
interface LeaderboardEntry {
id: number;
nickname: string;
score: number;
}
interface LeaderboardResponse {
session: { code: string; status: string; current_round: number };
leaderboard: LeaderboardEntry[];
winner?: LeaderboardEntry | null;
}
type LeaderboardEntry = ScoreboardResponse['leaderboard'][number];
type LeaderboardResponse = FinishGameResponse;
@Component({
selector: 'app-host-shell',
@@ -85,9 +77,14 @@ export class HostShellComponent implements OnInit {
finalWinner: LeaderboardEntry | null = null;
session: SessionDetail | null = null;
private readonly controller = createVerticalSliceController(createApiClient());
private readonly api = createApiClient();
private readonly controller = createVerticalSliceController(this.api);
ngOnInit(): void {
if (typeof window === 'undefined') {
return;
}
const hashRoute = window.location.hash.replace(/^#\/?/, '');
const match = hashRoute.match(/^host(?:\/[^/]+)?(?:\/([^/?#]+))?/i);
const codeFromRoute = match?.[1] ?? '';
@@ -99,7 +96,7 @@ export class HostShellComponent implements OnInit {
}
this.sessionCode = this.normalizeCode(candidate);
window.sessionStorage.setItem('wpp.host-session-code', this.sessionCode);
this.persistSessionCode(this.sessionCode);
void this.refreshSession();
}
@@ -107,6 +104,12 @@ export class HostShellComponent implements OnInit {
return value.trim().toUpperCase();
}
private persistSessionCode(code: string): void {
if (typeof window !== 'undefined') {
window.sessionStorage.setItem('wpp.host-session-code', code);
}
}
private async request<T>(path: string, method: 'GET' | 'POST', payload?: unknown): Promise<T> {
const response = await fetch(path, {
method,
@@ -139,7 +142,7 @@ export class HostShellComponent implements OnInit {
}
this.session = state.session as SessionDetail;
this.sessionCode = this.session.session.code;
window.sessionStorage.setItem('wpp.host-session-code', this.sessionCode);
this.persistSessionCode(this.sessionCode);
this.roundQuestionId = this.session.round_question?.id ? String(this.session.round_question.id) : '';
if (this.session.session.status !== 'finished') {
this.resetFinalLeaderboard();
@@ -159,7 +162,7 @@ export class HostShellComponent implements OnInit {
}
this.session = state.session as SessionDetail;
this.sessionCode = this.session.session.code;
window.sessionStorage.setItem('wpp.host-session-code', this.sessionCode);
this.persistSessionCode(this.sessionCode);
this.roundQuestionId = this.session.round_question?.id ? String(this.session.round_question.id) : '';
this.scoreboardPayload = '';
this.resetFinalLeaderboard();
@@ -214,6 +217,9 @@ export class HostShellComponent implements OnInit {
this.error = '';
try {
const code = this.normalizeCode(this.sessionCode);
if (!code) {
throw new Error('Session code is required');
}
await this.request(`/lobby/sessions/${encodeURIComponent(code)}/rounds/next`, 'POST', {});
this.scoreboardPayload = '';
this.resetFinalLeaderboard();
@@ -231,6 +237,9 @@ export class HostShellComponent implements OnInit {
this.error = '';
try {
const code = this.normalizeCode(this.sessionCode);
if (!code) {
throw new Error('Session code is required');
}
const payload = await this.request<LeaderboardResponse>(`/lobby/sessions/${encodeURIComponent(code)}/finish`, 'POST', {});
this.finalLeaderboardPayload = JSON.stringify(payload, null, 2);
this.finalLeaderboard = [...payload.leaderboard].sort((a, b) => {
@@ -248,8 +257,6 @@ export class HostShellComponent implements OnInit {
}
}
private resetFinalLeaderboard(): void {
this.finalLeaderboardPayload = '';
this.finalLeaderboard = [];