feat(player): add reconnect loading and fallback join state (#187)
CI / test-and-quality (push) Successful in 2m5s
CI / test-and-quality (pull_request) Successful in 2m8s

This commit is contained in:
2026-03-01 16:55:33 +00:00
parent c4850f2e0e
commit d26d2b1a09
2 changed files with 125 additions and 2 deletions
@@ -13,6 +13,14 @@ interface SessionDetail {
}
type ConnectionState = 'online' | 'reconnecting' | 'offline';
type LoadingTransition = 'refresh' | 'join' | 'submit-lie' | 'submit-guess' | null;
function resolveLocalStorage(): Storage | undefined {
if (typeof window === 'undefined') {
return undefined;
}
return window.localStorage;
}
@Component({
selector: 'app-player-shell',
@@ -31,12 +39,16 @@ type ConnectionState = 'online' | 'reconnecting' | 'offline';
<p *ngIf="connectionState === 'reconnecting'" class="error">
Reconnecting… trying to refresh session state.
<button type="button" (click)="retryReconnect()" [disabled]="loading">Retry now</button>
<button type="button" (click)="returnToJoin()" [disabled]="loading">Back to join</button>
</p>
<p *ngIf="connectionState === 'offline'" class="error">
You are offline. Reconnect to continue gameplay.
<button type="button" (click)="retryReconnect()" [disabled]="loading">Retry now</button>
<button type="button" (click)="returnToJoin()" [disabled]="loading">Back to join</button>
</p>
<p *ngIf="loading" class="hint">{{ loadingMessage }}</p>
<div class="panel" *ngIf="session">
<p><strong>Status:</strong> {{ session.session.status }}</p>
<p *ngIf="session.round_question"><strong>Prompt:</strong> {{ session.round_question.prompt }}</p>
@@ -70,6 +82,11 @@ type ConnectionState = 'online' | 'reconnecting' | 'offline';
<p *ngIf="error" class="error">{{ error }}</p>
<p *ngIf="submitError" class="error">{{ submitError.message }}</p>
<div class="panel" *ngIf="error || submitError">
<button type="button" (click)="retryReconnect()" [disabled]="loading">Retry</button>
<button type="button" (click)="returnToJoin()" [disabled]="loading">Back to join</button>
</div>
`,
})
export class PlayerShellComponent implements OnInit, OnDestroy {
@@ -85,8 +102,9 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
session: SessionDetail | null = null;
finalLeaderboard: Array<{ id: number; nickname: string; score: number }> = [];
connectionState: ConnectionState = 'online';
loadingTransition: LoadingTransition = null;
private readonly sessionContextStore = createSessionContextStore();
private readonly sessionContextStore = createSessionContextStore(resolveLocalStorage());
private readonly controller = createVerticalSliceController(createApiClient(), this.sessionContextStore);
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
@@ -131,7 +149,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
private readonly handleOnline = (): void => {
this.connectionState = 'reconnecting';
this.retryReconnect();
void this.retryReconnect();
};
private readonly handleOffline = (): void => {
@@ -146,6 +164,20 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
}
}
get loadingMessage(): string {
switch (this.loadingTransition) {
case 'join':
return 'Joining session… restoring your player state.';
case 'submit-lie':
return 'Submitting lie… waiting for guess phase.';
case 'submit-guess':
return 'Submitting guess… waiting for reveal.';
case 'refresh':
default:
return 'Loading latest session state…';
}
}
private normalizeCode(value: string): string {
return value.trim().toUpperCase();
}
@@ -200,6 +232,21 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
await this.refreshSession();
}
returnToJoin(): void {
this.loadingTransition = null;
this.clearReconnectTimer();
this.connectionState = typeof navigator !== 'undefined' && !navigator.onLine ? 'offline' : 'online';
this.session = null;
this.finalLeaderboard = [];
this.selectedGuess = '';
this.lieText = '';
this.submitError = null;
this.error = '';
this.playerId = 0;
this.sessionToken = '';
this.sessionContextStore.clear();
}
private syncFinalLeaderboard(): void {
if (!this.session || this.session.session.status !== 'finished') {
this.finalLeaderboard = [];
@@ -235,6 +282,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
async refreshSession(): Promise<void> {
this.loading = true;
this.loadingTransition = 'refresh';
this.error = '';
try {
const state = await this.controller.hydrateLobby(this.sessionCode);
@@ -253,11 +301,13 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
this.markConnectionIssue(error);
} finally {
this.loading = false;
this.loadingTransition = null;
}
}
async joinSession(): Promise<void> {
this.loading = true;
this.loadingTransition = 'join';
this.error = '';
try {
const state = await this.controller.joinLobby(this.sessionCode, this.nickname);
@@ -280,6 +330,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
this.markConnectionIssue(error);
} finally {
this.loading = false;
this.loadingTransition = null;
}
}
@@ -288,6 +339,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
return;
}
this.loading = true;
this.loadingTransition = 'submit-lie';
this.submitError = null;
try {
await this.request(
@@ -306,6 +358,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
this.markConnectionIssue(error);
} finally {
this.loading = false;
this.loadingTransition = null;
}
}
@@ -314,6 +367,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
return;
}
this.loading = true;
this.loadingTransition = 'submit-guess';
this.submitError = null;
try {
await this.request(
@@ -332,6 +386,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
this.markConnectionIssue(error);
} finally {
this.loading = false;
this.loadingTransition = null;
}
}
}