fix(tournaments): load both user bots and official bots in join dialog
openJoinDialog now fetches user bots and official bots in parallel via forkJoin. Each section shows its own empty state independently. Official bot difficulty buttons are hidden when no official bots are registered. User bots empty state links to /bots to create one. Disables all join buttons while any join is in progress. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -202,53 +202,57 @@
|
||||
<span class="dialog-brand">Join with a bot</span>
|
||||
<button type="button" class="dialog-close" (click)="closeJoinDialog()">×</button>
|
||||
</div>
|
||||
<p class="join-hint">Select an official (engine-backed) bot to enter this tournament. These are the bots that actually play their moves.</p>
|
||||
|
||||
@if (botsLoading) {
|
||||
<div class="dialog-loading"><span class="pulse"></span>Loading bots…</div>
|
||||
} @else if (userBots.length === 0) {
|
||||
<p class="join-empty">No official bots are available. The official-bots engine service must be running to register them.</p>
|
||||
} @else {
|
||||
<div class="bot-pick-list">
|
||||
@for (bot of userBots; track bot.id) {
|
||||
<button type="button" class="bot-pick-row"
|
||||
[disabled]="!!joiningBotId"
|
||||
(click)="joinWithBot(bot)">
|
||||
<span class="bot-pick-avatar">{{ bot.name.charAt(0).toUpperCase() }}</span>
|
||||
<span class="bot-pick-name">{{ bot.name }}</span>
|
||||
<span class="bot-pick-rating">{{ bot.rating }}</span>
|
||||
@if (joiningBotId === bot.id) {
|
||||
<span class="bot-pick-spinner">…</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (joinError) {
|
||||
<div class="dialog-error">{{ joinError }}</div>
|
||||
}
|
||||
|
||||
<div class="join-divider">
|
||||
<span class="join-divider-label">or join with an official bot</span>
|
||||
</div>
|
||||
|
||||
<div class="official-bot-grid">
|
||||
@for (d of officialDifficulties; track d) {
|
||||
<button type="button" class="official-bot-btn"
|
||||
[class]="'official-btn-' + d"
|
||||
[disabled]="!!joiningOfficialDifficulty || !!joiningBotId"
|
||||
(click)="joinWithOfficialBot(d)">
|
||||
@if (joiningOfficialDifficulty === d) {
|
||||
<span class="pulse"></span>
|
||||
@if (userBots.length === 0) {
|
||||
<p class="join-empty">You have no bots. <a routerLink="/bots">Create one</a> to join with your own bot.</p>
|
||||
} @else {
|
||||
<div class="bot-pick-list">
|
||||
@for (bot of userBots; track bot.id) {
|
||||
<button type="button" class="bot-pick-row"
|
||||
[disabled]="!!joiningBotId || !!joiningOfficialDifficulty"
|
||||
(click)="joinWithBot(bot)">
|
||||
<span class="bot-pick-avatar">{{ bot.name.charAt(0).toUpperCase() }}</span>
|
||||
<span class="bot-pick-name">{{ bot.name }}</span>
|
||||
<span class="bot-pick-rating">{{ bot.rating }}</span>
|
||||
@if (joiningBotId === bot.id) {
|
||||
<span class="bot-pick-spinner">…</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
{{ d | titlecase }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (officialJoinError) {
|
||||
<div class="dialog-error">{{ officialJoinError }}</div>
|
||||
@if (joinError) {
|
||||
<div class="dialog-error">{{ joinError }}</div>
|
||||
}
|
||||
|
||||
<div class="join-divider">
|
||||
<span class="join-divider-label">or join with an official bot</span>
|
||||
</div>
|
||||
|
||||
@if (officialBots.length === 0) {
|
||||
<p class="join-empty">No official bots are available. The official-bots engine service must be running to register them.</p>
|
||||
} @else {
|
||||
<div class="official-bot-grid">
|
||||
@for (d of officialDifficulties; track d) {
|
||||
<button type="button" class="official-bot-btn"
|
||||
[class]="'official-btn-' + d"
|
||||
[disabled]="!!joiningOfficialDifficulty || !!joiningBotId"
|
||||
(click)="joinWithOfficialBot(d)">
|
||||
@if (joiningOfficialDifficulty === d) {
|
||||
<span class="pulse"></span>
|
||||
}
|
||||
{{ d | titlecase }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (officialJoinError) {
|
||||
<div class="dialog-error">{{ officialJoinError }}</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -58,6 +59,7 @@ export class TournamentsComponent implements OnInit {
|
||||
|
||||
joinDialogTournamentId: string | null = null;
|
||||
userBots: Bot[] = [];
|
||||
officialBots: Bot[] = [];
|
||||
botsLoading = false;
|
||||
joiningBotId: string | null = null;
|
||||
joinError: string | null = null;
|
||||
@@ -175,16 +177,22 @@ export class TournamentsComponent implements OnInit {
|
||||
this.joinDialogTournamentId = tournamentId;
|
||||
this.joinError = null;
|
||||
this.botsLoading = true;
|
||||
this.botService.listOfficial()
|
||||
forkJoin({ user: this.botService.list(), official: this.botService.listOfficial() })
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: bots => { this.userBots = bots; this.botsLoading = false; },
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user