feat(spa): render final leaderboard summary in host shell
All checks were successful
CI / test-and-quality (push) Successful in 2m10s
CI / test-and-quality (pull_request) Successful in 2m26s

This commit is contained in:
2026-03-01 15:31:23 +00:00
parent 55e646651e
commit 634bd617e7
3 changed files with 47 additions and 8 deletions

View File

@@ -11,10 +11,16 @@ 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: Array<{ id: number; nickname: string; score: number }>;
winner?: { id: number; nickname: string; score: number } | null;
leaderboard: LeaderboardEntry[];
winner?: LeaderboardEntry | null;
}
@Component({
@@ -53,6 +59,13 @@ interface LeaderboardResponse {
<li *ngFor="let p of session.players">{{ p.nickname }}: {{ p.score }}</li>
</ul>
<pre *ngIf="scoreboardPayload">{{ scoreboardPayload }}</pre>
<div *ngIf="finalLeaderboard.length">
<h3>Final leaderboard</h3>
<p *ngIf="finalWinner"><strong>Winner:</strong> {{ finalWinner.nickname }} ({{ finalWinner.score }} pts)</p>
<ol>
<li *ngFor="let entry of finalLeaderboard">{{ entry.nickname }}: {{ entry.score }}</li>
</ol>
</div>
<pre *ngIf="finalLeaderboardPayload">{{ finalLeaderboardPayload }}</pre>
</div>
`,
@@ -68,6 +81,8 @@ export class HostShellComponent {
finishError = '';
scoreboardPayload = '';
finalLeaderboardPayload = '';
finalLeaderboard: LeaderboardEntry[] = [];
finalWinner: LeaderboardEntry | null = null;
session: SessionDetail | null = null;
private readonly controller = createVerticalSliceController(createApiClient());
@@ -110,7 +125,7 @@ export class HostShellComponent {
this.sessionCode = this.session.session.code;
this.roundQuestionId = this.session.round_question?.id ? String(this.session.round_question.id) : '';
if (this.session.session.status !== 'finished') {
this.finalLeaderboardPayload = '';
this.resetFinalLeaderboard();
}
} catch (error) {
this.error = `Session refresh failed: ${(error as Error).message}`;
@@ -129,7 +144,7 @@ export class HostShellComponent {
this.sessionCode = this.session.session.code;
this.roundQuestionId = this.session.round_question?.id ? String(this.session.round_question.id) : '';
this.scoreboardPayload = '';
this.finalLeaderboardPayload = '';
this.resetFinalLeaderboard();
});
}
@@ -183,7 +198,7 @@ export class HostShellComponent {
const code = this.normalizeCode(this.sessionCode);
await this.request(`/lobby/sessions/${encodeURIComponent(code)}/rounds/next`, 'POST', {});
this.scoreboardPayload = '';
this.finalLeaderboardPayload = '';
this.resetFinalLeaderboard();
await this.refreshSession();
} catch (error) {
this.nextRoundError = `Next round failed: ${(error as Error).message}`;
@@ -200,6 +215,13 @@ export class HostShellComponent {
const code = this.normalizeCode(this.sessionCode);
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) => {
if (b.score !== a.score) {
return b.score - a.score;
}
return a.nickname.localeCompare(b.nickname);
});
this.finalWinner = payload.winner ?? this.finalLeaderboard[0] ?? null;
await this.refreshSession();
} catch (error) {
this.finishError = `Finish game failed: ${(error as Error).message}`;
@@ -208,6 +230,14 @@ export class HostShellComponent {
}
}
private resetFinalLeaderboard(): void {
this.finalLeaderboardPayload = '';
this.finalLeaderboard = [];
this.finalWinner = null;
}
private async runAction(action: () => Promise<void>): Promise<void> {
this.loading = true;
this.error = '';