import { Component, DestroyRef, HostListener, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AuthService } from '../../services/auth.service'; import { AuthDialogService } from '../../services/auth-dialog.service'; import { CurrentUser } from '../../models/auth.models'; import { LoginDialogComponent } from '../login-dialog/login-dialog.component'; import { RegisterDialogComponent } from '../register-dialog/register-dialog.component'; import { ThemeService } from '../../services/theme.service'; import { ChallengeEventService } from '../../services/challenge-event.service'; import { ChallengeService } from '../../services/challenge.service'; import { ChallengeWebSocketService } from '../../services/challenge-websocket.service'; import { Challenge } from '../../models/challenge.models'; @Component({ selector: 'app-toolbar', standalone: true, imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent], templateUrl: './toolbar.component.html', styleUrl: './toolbar.component.css' }) export class ToolbarComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); private readonly authService = inject(AuthService); private readonly authDialogService = inject(AuthDialogService); private readonly themeService = inject(ThemeService); private readonly challengeEventService = inject(ChallengeEventService); private readonly challengeService = inject(ChallengeService); private readonly challengeWs = inject(ChallengeWebSocketService); private readonly router = inject(Router); private pollHandle: ReturnType | null = null; private readonly navigatedChallengeIds = new Set(); currentUser: CurrentUser | null = null; showLoginDialog = false; showRegisterDialog = false; isDarkMode = false; profileOpen = false; notifOpen = false; incomingChallenges: Challenge[] = []; acceptingId: string | null = null; decliningId: string | null = null; ngOnInit(): void { this.destroyRef.onDestroy(() => this.stopPolling()); this.authService.currentUser$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(user => { this.currentUser = user; if (user) { this.challengeWs.connect(); this.startPolling(); } else { this.challengeWs.disconnect(); this.stopPolling(); this.navigatedChallengeIds.clear(); this.challengeEventService.clear(); } }); this.authDialogService.dialogState$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(state => { this.showLoginDialog = state === 'login'; this.showRegisterDialog = state === 'register'; }); this.themeService.darkMode$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(isDark => { this.isDarkMode = isDark; }); this.challengeEventService.getIncomingChallenges$() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(challenges => { this.incomingChallenges = challenges; }); } private startPolling(): void { this.fetchChallenges(); this.pollHandle = setInterval(() => this.fetchChallenges(), 10_000); } private stopPolling(): void { if (this.pollHandle !== null) { clearInterval(this.pollHandle); this.pollHandle = null; } } private fetchChallenges(): void { this.challengeService.listChallenges().subscribe({ next: response => { const incoming = response.in ?? response.incoming ?? []; this.challengeEventService.setIncomingChallenges(incoming); const outgoing = response.out ?? response.outgoing ?? []; for (const c of outgoing) { if (c.status === 'accepted' && c.gameId && !this.navigatedChallengeIds.has(c.id)) { this.navigatedChallengeIds.add(c.id); if (!this.router.url.includes(`/game/${c.gameId}`)) { void this.router.navigate(['/game', c.gameId]); } } } } }); } @HostListener('document:click', ['$event']) onDocumentClick(event: MouseEvent): void { if (!(event.target as HTMLElement).closest('[data-dropdown]')) { this.profileOpen = false; this.notifOpen = false; } } toggleProfile(event: MouseEvent): void { event.stopPropagation(); const wasOpen = this.profileOpen; this.profileOpen = false; this.notifOpen = false; this.profileOpen = !wasOpen; } toggleNotif(event: MouseEvent): void { event.stopPropagation(); const wasOpen = this.notifOpen; this.profileOpen = false; this.notifOpen = false; this.notifOpen = !wasOpen; } openLoginDialog(): void { this.profileOpen = false; this.notifOpen = false; this.authDialogService.openLogin(); } closeLoginDialog(): void { this.authDialogService.close(); } openRegisterDialog(): void { this.profileOpen = false; this.notifOpen = false; this.authDialogService.openRegister(); } closeRegisterDialog(): void { this.authDialogService.close(); } logout(): void { this.profileOpen = false; this.notifOpen = false; this.authService.logout(); } toggleTheme(event: MouseEvent): void { event.stopPropagation(); this.themeService.toggleTheme(); } goToHome(): void { void this.router.navigate(['/']); } goToProfile(): void { this.profileOpen = false; this.notifOpen = false; void this.router.navigate(['/profile']); } goToGames(): void { this.profileOpen = false; this.notifOpen = false; void this.router.navigate(['/games']); } goToTournaments(): void { this.profileOpen = false; this.notifOpen = false; void this.router.navigate(['/tournaments']); } goToChallenges(): void { this.profileOpen = false; this.notifOpen = false; void this.router.navigate(['/challenges']); } goToBots(): void { this.profileOpen = false; this.notifOpen = false; void this.router.navigate(['/bots']); } onLoginSuccess(): void { this.closeLoginDialog(); } onRegisterSuccess(): void { this.closeRegisterDialog(); } getInitial(): string { return this.currentUser?.username?.charAt(0).toUpperCase() ?? '?'; } getTimeControlDisplay(challenge: Challenge): string { const { limit, increment } = challenge.timeControl; if (!limit || !increment) return 'Unlimited'; return `${Math.floor(limit / 60)}+${increment}`; } getExpirationInfo(challenge: Challenge): string { const diff = new Date(challenge.expiresAt).getTime() - Date.now(); if (diff <= 0) return 'Expired'; const min = Math.floor(diff / 60000); return min > 60 ? `${Math.floor(min / 60)}h` : `${min}m`; } acceptChallenge(event: MouseEvent, challenge: Challenge): void { event.stopPropagation(); if (this.acceptingId || this.decliningId) return; this.acceptingId = challenge.id; this.challengeService.acceptChallenge(challenge.id).subscribe({ next: accepted => { this.acceptingId = null; this.challengeEventService.onChallengeAccepted(accepted); if (accepted.gameId) void this.router.navigate(['/game', accepted.gameId]); }, error: () => { this.acceptingId = null; } }); } declineChallenge(event: MouseEvent, challenge: Challenge): void { event.stopPropagation(); if (this.acceptingId || this.decliningId) return; this.decliningId = challenge.id; this.challengeService.declineChallenge(challenge.id, { reason: 'generic' }).subscribe({ next: () => { this.decliningId = null; this.challengeEventService.removeChallenge(challenge.id); }, error: () => { this.decliningId = null; } }); } }