import { CommonModule } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { createApiClient } from '../../../../../src/api/client';
import type { FinishGameResponse, SessionDetailResponse } from '../../../../../src/api/types';
import { createVerticalSliceController } from '../../../../../src/spa/vertical-slice';
import { clientHasNoAudioOutput, resolvePreferredLocale, subscribeToLocaleChanges, t } from '../../lobby-i18n';
type SessionDetail = SessionDetailResponse;
type LeaderboardEntry = FinishGameResponse['leaderboard'][number];
type LeaderboardResponse = FinishGameResponse;
@Component({
selector: 'app-host-shell',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
{{ copy('host.title') }}
{{ copy('host.audio_locale_hint') }}: {{ locale }}
{{ error }}
{{ nextRoundError }}
{{ finishError }}
{{ copy('common.status') }}: {{ session.session.status }} · {{ copy('common.round') }} {{ session.session.current_round }}
{{ copy('common.round_question_id') }}: {{ roundQuestionId || '-' }}
{{ copy('common.prompt') }}: {{ session.round_question.prompt }}
- {{ p.nickname }}: {{ p.score }}
Reveal
Korrekt svar: {{ session.reveal.correct_answer }}
Spørgsmål: {{ session.reveal.prompt }}
Løgne
- {{ lie.nickname }} løj: {{ lie.text }}
Gæt
-
{{ guess.nickname }} valgte {{ guess.selected_text }}
· korrekt
· narret af {{ guess.fooled_player_nickname }}
· forkert
{{ copy('host.final_leaderboard') }}
{{ copy('host.winner') }}: {{ finalWinner.nickname }} ({{ finalWinner.score }} {{ copy('common.points_short') }})
- {{ entry.nickname }}: {{ entry.score }}
{{ finalLeaderboardPayload }}
`,
})
export class HostShellComponent implements OnInit, OnDestroy {
locale = resolvePreferredLocale();
readonly clientHasNoAudioOutput = clientHasNoAudioOutput;
sessionCode = '';
categorySlug = 'general';
roundQuestionId = '';
loading = false;
error = '';
nextRoundError = '';
finishError = '';
finalLeaderboardPayload = '';
finalLeaderboard: LeaderboardEntry[] = [];
finalWinner: LeaderboardEntry | null = null;
session: SessionDetail | null = null;
private readonly api = createApiClient();
private readonly controller = createVerticalSliceController(this.api);
private unsubscribeLocale: (() => void) | null = null;
ngOnInit(): void {
this.unsubscribeLocale = subscribeToLocaleChanges((locale) => {
this.locale = locale;
});
if (typeof window === 'undefined') {
return;
}
const hashRoute = window.location.hash.replace(/^#\/?/, '');
const match = hashRoute.match(/^host(?:\/[^/]+)?(?:\/([^/?#]+))?/i);
const codeFromRoute = match?.[1] ?? '';
const storedCode = window.sessionStorage.getItem('wpp.host-session-code') ?? '';
const candidate = codeFromRoute || storedCode;
if (!candidate) {
return;
}
this.sessionCode = this.normalizeCode(candidate);
this.persistSessionCode(this.sessionCode);
void this.refreshSession();
}
ngOnDestroy(): void {
this.unsubscribeLocale?.();
this.unsubscribeLocale = null;
}
get canStartRound(): boolean {
return Boolean(this.session?.phase_view_model?.host?.can_start_round ?? !this.session);
}
get canStartNextRound(): boolean {
return Boolean(this.session?.phase_view_model?.host?.can_start_next_round);
}
get canFinishGame(): boolean {
return Boolean(this.session?.phase_view_model?.host?.can_finish_game);
}
copy(key: string): string {
return t(key, this.locale);
}
private normalizeCode(value: string): string {
return value.trim().toUpperCase();
}
private persistSessionCode(code: string): void {
if (typeof window !== 'undefined') {
window.sessionStorage.setItem('wpp.host-session-code', code);
}
}
private async request(path: string, method: 'GET' | 'POST', payload?: unknown): Promise {
const response = await fetch(path, {
method,
headers: {
Accept: 'application/json',
...(payload === undefined ? {} : { 'Content-Type': 'application/json' }),
},
...(payload === undefined ? {} : { body: JSON.stringify(payload) }),
credentials: 'same-origin',
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error((body as { error?: string }).error ?? `HTTP ${response.status}`);
}
return body as T;
}
async refreshSession(): Promise {
this.loading = true;
this.error = '';
this.nextRoundError = '';
this.finishError = '';
try {
const state = await this.controller.hydrateLobby(this.sessionCode);
if (!state.session || state.errorMessage) {
throw new Error(state.errorMessage ?? this.copy('common.unknown_error'));
}
this.session = state.session as SessionDetail;
this.sessionCode = this.session.session.code;
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();
}
this.syncRouteFromSession();
} catch (error) {
this.error = `${this.copy('host.session_refresh_failed')}: ${(error as Error).message}`;
} finally {
this.loading = false;
}
}
async startRound(): Promise {
await this.runAction(async () => {
const state = await this.controller.startRound(this.sessionCode, this.categorySlug.trim());
if (!state.session || state.errorMessage) {
throw new Error(state.errorMessage ?? this.copy('common.unknown_error'));
}
this.session = state.session as SessionDetail;
this.sessionCode = this.session.session.code;
this.persistSessionCode(this.sessionCode);
this.roundQuestionId = this.session.round_question?.id ? String(this.session.round_question.id) : '';
this.resetFinalLeaderboard();
this.syncRouteFromSession();
});
}
async startNextRound(): Promise {
this.loading = true;
this.nextRoundError = '';
this.error = '';
try {
const code = this.normalizeCode(this.sessionCode);
if (!code) {
throw new Error(this.copy('host.session_code_required'));
}
await this.request(`/lobby/sessions/${encodeURIComponent(code)}/rounds/next`, 'POST', {});
this.resetFinalLeaderboard();
await this.refreshSession();
} catch (error) {
this.nextRoundError = `${this.copy('host.next_round_failed')}: ${(error as Error).message}`;
} finally {
this.loading = false;
}
}
async finishGame(): Promise {
this.loading = true;
this.finishError = '';
this.error = '';
try {
const code = this.normalizeCode(this.sessionCode);
if (!code) {
throw new Error(this.copy('host.session_code_required'));
}
const payload = await this.request(`/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 = `${this.copy('host.finish_game_failed')}: ${(error as Error).message}`;
} finally {
this.loading = false;
}
}
private resetFinalLeaderboard(): void {
this.finalLeaderboardPayload = '';
this.finalLeaderboard = [];
this.finalWinner = null;
}
private syncRouteFromSession(): void {
if (!this.session) {
return;
}
const phase = this.session.session.status || 'lobby';
const code = this.normalizeCode(this.session.session.code || this.sessionCode);
if (!code) {
return;
}
const targetPath = `#/host/${encodeURIComponent(phase)}/${encodeURIComponent(code)}`;
if (typeof window === 'undefined' || window.location.hash === targetPath) {
return;
}
window.history.replaceState(window.history.state, '', targetPath);
}
private async runAction(action: () => Promise): Promise {
this.loading = true;
this.error = '';
try {
await action();
} catch (error) {
this.error = (error as Error).message;
} finally {
this.loading = false;
}
}
}