feat(spa): guard host/player API contract with typed client calls
This commit is contained in:
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user