import { Component, DestroyRef, OnInit, inject } from '@angular/core'; import { CommonModule, TitleCasePipe } from '@angular/common'; import { Router, RouterLink } from '@angular/router'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { forkJoin } from 'rxjs'; import { TournamentService } from '../../services/tournament.service'; import { AuthService } from '../../services/auth.service'; import { BotService } from '../../services/bot.service'; import { OfficialBotService } from '../../services/official-bot.service'; import { TournamentServerService, ExternalTournamentServer } from '../../services/tournament-server.service'; import { Bot } from '../../models/bot.models'; import { Tournament, TournamentResult, RoundPairings } from '../../models/tournament.models'; import { CurrentUser } from '../../models/auth.models'; import { environment } from '../../../environments/environment'; type StatusTab = 'started' | 'created' | 'finished'; @Component({ selector: 'app-tournaments', standalone: true, imports: [CommonModule, RouterLink, FormsModule, ReactiveFormsModule, TitleCasePipe], templateUrl: './tournaments.component.html', styleUrl: './tournaments.component.css' }) export class TournamentsComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); private readonly tournamentService = inject(TournamentService); private readonly authService = inject(AuthService); private readonly fb = inject(FormBuilder); private readonly botService = inject(BotService); private readonly officialBotService = inject(OfficialBotService); private readonly tournamentServerService = inject(TournamentServerService); private readonly router = inject(Router); loading = true; tab: StatusTab = 'started'; currentUser: CurrentUser | null = null; started: Tournament[] = []; created: Tournament[] = []; finished: Tournament[] = []; selectedTournament: Tournament | null = null; pairings: RoundPairings | null = null; pairingsLoading = false; showCreateDialog = false; createForm: FormGroup = this.fb.group({ name: ['', [Validators.required, Validators.minLength(3)]], nbRounds: [4, [Validators.required, Validators.min(1), Validators.max(20)]], clockLimitMinutes: [3, [Validators.required, Validators.min(1), Validators.max(60)]], clockIncrement: [0, [Validators.required, Validators.min(0), Validators.max(60)]], rated: [false] }); createLoading = false; createError: string | null = null; startingId: string | null = null; joinDialogTournamentId: string | null = null; userBots: Bot[] = []; officialBots: Bot[] = []; botsLoading = false; joiningBotId: string | null = null; joinError: string | null = null; readonly officialDifficulties = ['easy', 'medium', 'hard', 'expert'] as const; joiningOfficialDifficulty: string | null = null; officialJoinError: string | null = null; showServersDialog = false; servers: ExternalTournamentServer[] = []; serversLoading = false; ngOnInit(): void { this.authService.currentUser$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(u => { this.currentUser = u; }); this.loadTournaments(); this.tournamentServerService.list() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: res => { this.servers = res.servers; }, error: () => {} }); } openCreateDialog(): void { this.createForm.reset({ name: '', nbRounds: 4, clockLimitMinutes: 3, clockIncrement: 0, rated: false }); this.createError = null; this.showCreateDialog = true; } closeCreateDialog(): void { this.showCreateDialog = false; } submitCreate(): void { if (this.createForm.invalid) return; this.createLoading = true; this.createError = null; this.tournamentService.create(this.createForm.value).subscribe({ next: t => { this.createLoading = false; this.showCreateDialog = false; this.created = [t, ...this.created]; this.tab = 'created'; this.selectedTournament = null; }, error: err => { this.createLoading = false; this.createError = err.error?.message ?? err.error?.error ?? 'Failed to create tournament.'; } }); } setTab(tab: StatusTab): void { this.tab = tab; this.selectedTournament = null; this.pairings = null; } selectTournament(t: Tournament): void { if (this.selectedTournament?.id === t.id) { this.selectedTournament = null; this.pairings = null; return; } this.selectedTournament = t; this.pairings = null; if (t.round > 0) { this.loadPairings(t.id, t.round); } } get activeList(): Tournament[] { return this[this.tab]; } clockDisplay(t: Tournament): string { const min = Math.floor(t.clock.limit / 60); return `${min}+${t.clock.increment}`; } rankMedal(rank: number): string { if (rank === 1) return '🥇'; if (rank === 2) return '🥈'; if (rank === 3) return '🥉'; return `${rank}.`; } scoreDisplay(r: TournamentResult): string { return r.points % 1 === 0 ? `${r.points}` : `${r.points}`; } startTournament(event: MouseEvent, t: Tournament): void { event.stopPropagation(); this.startingId = t.id; this.tournamentService.start(t.id).subscribe({ next: updated => { this.startingId = null; const list = this.created.map(x => x.id === t.id ? updated : x); this.created = list.filter(x => x.status === 'created'); if (!this.started.find(x => x.id === updated.id)) this.started = [updated, ...this.started]; this.selectedTournament = updated; this.tab = 'started'; }, error: () => { this.startingId = null; } }); } watchGame(gameId: string): void { const tid = this.selectedTournament?.id; if (!tid) return; const server = this.servers[0]?.url || environment.tournamentServerUrl; if (!server) { this.joinError = 'No tournament server configured. Cannot open stream.'; return; } void this.router.navigate(['/tournament', tid, 'game', gameId], { queryParams: { server } }); } openJoinDialog(event: MouseEvent, tournamentId: string): void { event.stopPropagation(); this.joinDialogTournamentId = tournamentId; this.joinError = null; this.botsLoading = true; forkJoin({ user: this.botService.list(), official: this.botService.listOfficial() }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: ({ user, official }) => { this.userBots = user; this.officialBots = official; this.botsLoading = false; }, error: () => { this.botsLoading = false; } }); } closeJoinDialog(): void { this.joinDialogTournamentId = null; this.userBots = []; this.officialBots = []; this.joiningBotId = null; this.joinError = null; this.joiningOfficialDifficulty = null; this.officialJoinError = null; } joinWithOfficialBot(difficulty: string): void { if (!this.joinDialogTournamentId || this.joiningOfficialDifficulty || this.joiningBotId) return; this.joiningOfficialDifficulty = difficulty; this.officialJoinError = null; const tid = this.joinDialogTournamentId; this.officialBotService.joinTournament(tid, difficulty).subscribe({ next: () => { this.joiningOfficialDifficulty = null; this.closeJoinDialog(); this.tournamentService.get(tid) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(updated => { this.created = this.created.map(x => x.id === tid ? updated : x); this.started = this.started.map(x => x.id === tid ? updated : x); if (this.selectedTournament?.id === tid) this.selectedTournament = updated; }); }, error: err => { this.joiningOfficialDifficulty = null; this.officialJoinError = err.error?.error ?? 'Failed to join with official bot.'; } }); } joinWithBot(bot: Bot): void { if (!this.joinDialogTournamentId || this.joiningBotId) return; this.joiningBotId = bot.id; this.joinError = null; this.tournamentService.join(this.joinDialogTournamentId, bot.id, bot.name).subscribe({ next: () => { this.joiningBotId = null; const tid = this.joinDialogTournamentId!; this.closeJoinDialog(); this.tournamentService.get(tid) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(updated => { this.created = this.created.map(x => x.id === tid ? updated : x); this.started = this.started.map(x => x.id === tid ? updated : x); if (this.selectedTournament?.id === tid) this.selectedTournament = updated; }); }, error: err => { this.joiningBotId = null; this.joinError = err.error?.message ?? err.error?.error ?? 'Failed to join tournament.'; } }); } openServersDialog(): void { this.showServersDialog = true; this.serversLoading = true; this.tournamentServerService.list() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: res => { this.servers = res.servers; this.serversLoading = false; }, error: () => { this.serversLoading = false; } }); } closeServersDialog(): void { this.showServersDialog = false; } private loadTournaments(): void { this.tournamentService.list() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: list => { this.started = list.started; this.created = list.created; this.finished = list.finished; this.loading = false; if (this.started.length === 0 && this.created.length > 0) this.tab = 'created'; else if (this.started.length === 0 && this.finished.length > 0) this.tab = 'finished'; }, error: () => { this.loading = false; } }); } private loadPairings(id: string, round: number): void { this.pairingsLoading = true; this.tournamentService.roundPairings(id, round) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: p => { this.pairings = p; this.pairingsLoading = false; }, error: () => { this.pairingsLoading = false; } }); } }