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 }}

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') }})

  1. {{ 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; } } }