diff --git a/frontend/angular/src/app/features/host/host-shell.component.spec.ts b/frontend/angular/src/app/features/host/host-shell.component.spec.ts index 626db73..fe0bc2e 100644 --- a/frontend/angular/src/app/features/host/host-shell.component.spec.ts +++ b/frontend/angular/src/app/features/host/host-shell.component.spec.ts @@ -262,5 +262,26 @@ describe('HostShellComponent gameplay wiring', () => { await component.refreshSession(); expect(replaceState).toHaveBeenCalledWith(null, '', '#/host/guess/ABCD12'); + expect(component.canStartRound).toBe(false); + expect(component.canShowQuestion).toBe(false); + expect(component.canMixAnswers).toBe(false); + expect(component.canCalculateScores).toBe(true); + }); + + it('uses phase_view_model to keep host action surface phase-specific', async () => { + const component = new HostShellComponent(); + + expect(component.canStartRound).toBe(true); + expect(component.canShowQuestion).toBe(false); + + component.session = sessionDetailPayload('lie') as any; + expect(component.canStartRound).toBe(false); + expect(component.canShowQuestion).toBe(true); + expect(component.canMixAnswers).toBe(true); + + component.session = sessionDetailPayload('reveal') as any; + expect(component.canRevealScoreboard).toBe(true); + expect(component.canStartNextRound).toBe(false); + expect(component.canFinishGame).toBe(false); }); }); diff --git a/frontend/angular/src/app/features/host/host-shell.component.ts b/frontend/angular/src/app/features/host/host-shell.component.ts index 4253051..fa3f5e9 100644 --- a/frontend/angular/src/app/features/host/host-shell.component.ts +++ b/frontend/angular/src/app/features/host/host-shell.component.ts @@ -11,6 +11,17 @@ interface SessionDetail { session: { code: string; status: string; current_round: number }; round_question: { id: number; prompt: string; answers: Array<{ text: string }> } | null; players: Array<{ id: number; nickname: string; score: number }>; + phase_view_model?: { + host?: { + can_start_round?: boolean; + can_show_question?: boolean; + can_mix_answers?: boolean; + can_calculate_scores?: boolean; + can_reveal_scoreboard?: boolean; + can_start_next_round?: boolean; + can_finish_game?: boolean; + }; + }; } type LeaderboardEntry = ScoreboardResponse['leaderboard'][number]; @@ -25,18 +36,15 @@ type LeaderboardResponse = FinishGameResponse;
- + - - - - - - - - - - + + + + + + +

{{ copy('host.audio_locale_hint') }}: {{ locale }}

@@ -114,6 +122,34 @@ export class HostShellComponent implements OnInit, OnDestroy { this.unsubscribeLocale = null; } + get canStartRound(): boolean { + return Boolean(this.session?.phase_view_model?.host?.can_start_round ?? !this.session); + } + + get canShowQuestion(): boolean { + return Boolean(this.session?.phase_view_model?.host?.can_show_question); + } + + get canMixAnswers(): boolean { + return Boolean(this.session?.phase_view_model?.host?.can_mix_answers); + } + + get canCalculateScores(): boolean { + return Boolean(this.session?.phase_view_model?.host?.can_calculate_scores); + } + + get canRevealScoreboard(): boolean { + return Boolean(this.session?.phase_view_model?.host?.can_reveal_scoreboard); + } + + 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); } diff --git a/frontend/angular/src/app/features/player/player-shell.component.spec.ts b/frontend/angular/src/app/features/player/player-shell.component.spec.ts index 5bd1d1d..c256895 100644 --- a/frontend/angular/src/app/features/player/player-shell.component.spec.ts +++ b/frontend/angular/src/app/features/player/player-shell.component.spec.ts @@ -518,4 +518,28 @@ describe('PlayerShellComponent gameplay wiring', () => { expect(component.clientHasNoAudioOutput).toBe(false); }); + it('keeps phone client controls phase-specific and low-complexity', () => { + const component = new PlayerShellComponent(); + + expect(component.showJoinControls).toBe(true); + expect(component.showLieControls).toBe(false); + expect(component.showGuessControls).toBe(false); + expect(component.showFinalLeaderboard).toBe(false); + + component.session = sessionDetailPayload('lie') as any; + component.playerId = 9; + component.sessionToken = 'tok'; + expect(component.showJoinControls).toBe(false); + expect(component.showLieControls).toBe(true); + expect(component.showGuessControls).toBe(false); + + component.session = sessionDetailPayload('guess', { answers: ['A', 'B'] }) as any; + expect(component.showLieControls).toBe(false); + expect(component.showGuessControls).toBe(true); + + component.session = sessionDetailPayload('finished', { players: [{ id: 1, nickname: 'Luna', score: 8 }] }) as any; + expect(component.showGuessControls).toBe(false); + expect(component.showFinalLeaderboard).toBe(true); + }); + }); diff --git a/frontend/angular/src/app/features/player/player-shell.component.ts b/frontend/angular/src/app/features/player/player-shell.component.ts index 393723a..ed73fc5 100644 --- a/frontend/angular/src/app/features/player/player-shell.component.ts +++ b/frontend/angular/src/app/features/player/player-shell.component.ts @@ -11,6 +11,14 @@ interface SessionDetail { session: { code: string; status: string; current_round: number }; round_question: { id: number; prompt: string; answers: Array<{ text: string }> } | null; players: Array<{ id: number; nickname: string; score: number }>; + phase_view_model?: { + player?: { + can_join?: boolean; + can_submit_lie?: boolean; + can_submit_guess?: boolean; + can_view_final_result?: boolean; + }; + }; } type ConnectionState = 'online' | 'reconnecting' | 'offline'; @@ -49,9 +57,9 @@ function resolveLocalStorage(): Storage | undefined {
- + - +

@@ -71,26 +79,30 @@ function resolveLocalStorage(): Storage | undefined {

{{ copy('common.status') }}: {{ session.session.status }}

{{ copy('common.prompt') }}: {{ session.round_question.prompt }}

- - - + + + + + -
- -
+ +
+ +
- - + + +
-
+

{{ copy('player.final_leaderboard') }}

  1. {{ entry.nickname }}: {{ entry.score }}
  2. @@ -300,6 +312,25 @@ export class PlayerShellComponent implements OnInit, OnDestroy { }, 3000); } + get showJoinControls(): boolean { + if (!this.session) { + return true; + } + return Boolean(this.session?.phase_view_model?.player?.can_join && !this.playerId && !this.sessionToken); + } + + get showLieControls(): boolean { + return Boolean(this.session?.phase_view_model?.player?.can_submit_lie); + } + + get showGuessControls(): boolean { + return Boolean(this.session?.phase_view_model?.player?.can_submit_guess); + } + + get showFinalLeaderboard(): boolean { + return Boolean(this.session?.phase_view_model?.player?.can_view_final_result); + } + get loadingMessage(): string { switch (this.loadingTransition) { case 'join': diff --git a/frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts b/frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts index 26adc77..23754af 100644 --- a/frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts +++ b/frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts @@ -26,11 +26,13 @@ describe('i18n MVP flow smoke (host/player + audio policy)', () => { host.ngOnInit(); player.ngOnInit(); + expect(host.copy('common.refresh')).toBe('Refresh'); expect(host.copy('game.host.start_round')).toBe('Start round'); expect(player.copy('game.player.submit_guess')).toBe('Submit guess'); setPreferredLocale('da'); + expect(host.copy('common.refresh')).toBe('Opdatér'); expect(host.copy('game.host.start_round')).toBe('Start runde'); expect(player.copy('game.player.submit_guess')).toBe('Send gæt');