import { CommonModule } from '@angular/common'; import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { ChessBoardComponent } from '../../components/chess-board/chess-board.component'; import { InputCardComponent } from '../../components/input-card/input-card.component'; import { PromotionDialogComponent } from '../../components/promotion-dialog/promotion-dialog.component'; import { GameFacade } from './game.facade'; type TimerTurn = 'white' | 'black'; type BoardTheme = 'arabian' | 'classic'; interface TimerSnapshot { whiteSeconds: number; blackSeconds: number; turn: TimerTurn; savedAt: number; } @Component({ selector: 'app-game', standalone: true, imports: [CommonModule, FormsModule, RouterLink, ChessBoardComponent, InputCardComponent, PromotionDialogComponent], providers: [GameFacade], templateUrl: './game.component.html', styleUrl: './game.component.css' }) export class GameComponent implements OnInit, OnDestroy { private static readonly TIMER_START_SECONDS = 10 * 60; private static readonly BOARD_THEME_STORAGE_KEY = 'nowchess.boardTheme'; private readonly route = inject(ActivatedRoute); private readonly destroyRef = inject(DestroyRef); readonly facade = inject(GameFacade); whiteTimerSeconds = GameComponent.TIMER_START_SECONDS; blackTimerSeconds = GameComponent.TIMER_START_SECONDS; exportType: 'fen' | 'pgn' = 'fen'; boardTheme: BoardTheme = 'arabian'; isDarkMode = false; exportValue = ''; exportNotice = ''; private timerIntervalId: number | null = null; private activeGameId = ''; ngOnInit(): void { this.applyIncomingTheme(); this.syncThemeFromDocument(); this.boardTheme = this.resolveStoredBoardTheme(); this.startDummyTimers(); this.route.paramMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((paramMap) => { const id = paramMap.get('gameId'); if (!id) { this.facade.errorMessage = 'Missing gameId in route.'; this.facade.loading = false; return; } this.activeGameId = id; this.restoreTimers(id); this.facade.setGameId(id); this.syncExportValue(); }); } ngOnDestroy(): void { if (this.timerIntervalId !== null) { window.clearInterval(this.timerIntervalId); } this.persistTimers(this.resolveCurrentTurn()); } private syncThemeFromDocument(): void { this.isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark'; } private applyIncomingTheme(): void { const incomingTheme = window.history.state?.theme; if (incomingTheme === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); localStorage.setItem('theme', 'dark'); return; } if (incomingTheme === 'light') { document.documentElement.removeAttribute('data-theme'); localStorage.removeItem('theme'); } } setExportType(type: 'fen' | 'pgn'): void { this.exportType = type; this.exportNotice = ''; this.syncExportValue(); } setBoardTheme(theme: BoardTheme): void { this.boardTheme = theme; localStorage.setItem(GameComponent.BOARD_THEME_STORAGE_KEY, theme); } completeExport(): void { this.syncExportValue(); if (!this.exportValue.trim()) { this.exportNotice = 'Nothing to export yet.'; return; } if (!navigator.clipboard?.writeText) { this.exportNotice = 'Export is ready in the text box.'; return; } void navigator.clipboard .writeText(this.exportValue) .then(() => { this.exportNotice = 'Copied to clipboard.'; }) .catch(() => { this.exportNotice = 'Export is ready in the text box.'; }); } formatTimer(totalSeconds: number): string { const safeSeconds = Math.max(0, totalSeconds); const minutes = Math.floor(safeSeconds / 60) .toString() .padStart(2, '0'); const seconds = (safeSeconds % 60).toString().padStart(2, '0'); return `${minutes}:${seconds}`; } private startDummyTimers(): void { if (this.timerIntervalId !== null) { return; } this.timerIntervalId = window.setInterval(() => { this.tickDummyTimers(); this.syncExportValue(); }, 1000); } private tickDummyTimers(): void { const state = this.facade.state; if (!state || this.facade.loading || this.facade.isGameFinished) { return; } if (state.turn === 'white') { this.whiteTimerSeconds = Math.max(0, this.whiteTimerSeconds - 1); this.persistTimers('white'); return; } this.blackTimerSeconds = Math.max(0, this.blackTimerSeconds - 1); this.persistTimers('black'); } private syncExportValue(): void { const state = this.facade.state; if (!state) { this.exportValue = ''; return; } this.exportValue = this.exportType === 'fen' ? state.fen : state.pgn; } private restoreTimers(gameId: string): void { const fallbackTurn = this.resolveCurrentTurn(); const rawSnapshot = localStorage.getItem(this.getTimerStorageKey(gameId)); if (!rawSnapshot) { this.resetTimers(); this.persistTimers(fallbackTurn); return; } const snapshot = this.parseSnapshot(rawSnapshot); if (!snapshot) { this.resetTimers(); this.persistTimers(fallbackTurn); return; } this.applySnapshot(snapshot); this.persistTimers(snapshot.turn); } private parseSnapshot(rawSnapshot: string): TimerSnapshot | null { try { const parsed = JSON.parse(rawSnapshot) as Partial; if ( typeof parsed.whiteSeconds !== 'number' || typeof parsed.blackSeconds !== 'number' || (parsed.turn !== 'white' && parsed.turn !== 'black') || typeof parsed.savedAt !== 'number' ) { return null; } return { whiteSeconds: Math.max(0, Math.floor(parsed.whiteSeconds)), blackSeconds: Math.max(0, Math.floor(parsed.blackSeconds)), turn: parsed.turn, savedAt: parsed.savedAt }; } catch { return null; } } private applySnapshot(snapshot: TimerSnapshot): void { const elapsedSeconds = Math.max(0, Math.floor((Date.now() - snapshot.savedAt) / 1000)); this.whiteTimerSeconds = snapshot.whiteSeconds; this.blackTimerSeconds = snapshot.blackSeconds; if (snapshot.turn === 'white') { this.whiteTimerSeconds = Math.max(0, this.whiteTimerSeconds - elapsedSeconds); return; } this.blackTimerSeconds = Math.max(0, this.blackTimerSeconds - elapsedSeconds); } private persistTimers(turn: TimerTurn): void { if (!this.activeGameId) { return; } const snapshot: TimerSnapshot = { whiteSeconds: this.whiteTimerSeconds, blackSeconds: this.blackTimerSeconds, turn, savedAt: Date.now() }; localStorage.setItem(this.getTimerStorageKey(this.activeGameId), JSON.stringify(snapshot)); } private resolveCurrentTurn(): TimerTurn { return this.facade.state?.turn ?? 'white'; } private resetTimers(): void { this.whiteTimerSeconds = GameComponent.TIMER_START_SECONDS; this.blackTimerSeconds = GameComponent.TIMER_START_SECONDS; } private getTimerStorageKey(gameId: string): string { return `nowchess.timer.${gameId}`; } private resolveStoredBoardTheme(): BoardTheme { const stored = localStorage.getItem(GameComponent.BOARD_THEME_STORAGE_KEY); return stored === 'classic' ? 'classic' : 'arabian'; } }