Files
NowChess-Frontend/src/app/pages/tournaments/tournaments.component.html
T
lq64 48959daae3 feat: bots (#9)
Co-authored-by: Lala, Shahd <Shahd.Lala@sybit.de>
Reviewed-on: #9
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
2026-06-10 22:38:06 +02:00

274 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="t-shell">
<div class="page">
<nav class="crumb" aria-label="Breadcrumb">
<a routerLink="/" class="crumb-link">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6" />
</svg>
Back to lobby
</a>
<span class="crumb-sep">/</span>
<span class="crumb-current">Tournaments</span>
</nav>
<header class="page-header">
<div class="page-title-row">
<h1 class="page-title">Tournaments</h1>
@if (currentUser) {
<button type="button" class="btn-new" (click)="openCreateDialog()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
New tournament
</button>
}
</div>
<div class="tabs" role="tablist">
<button type="button" class="tab-btn" [class.active]="tab === 'started'" (click)="setTab('started')">
Live
@if (started.length > 0) { <span class="tab-badge live-badge">{{ started.length }}</span> }
</button>
<button type="button" class="tab-btn" [class.active]="tab === 'created'" (click)="setTab('created')">
Upcoming
@if (created.length > 0) { <span class="tab-badge">{{ created.length }}</span> }
</button>
<button type="button" class="tab-btn" [class.active]="tab === 'finished'" (click)="setTab('finished')">
Finished
</button>
</div>
</header>
@if (loading) {
<div class="state-msg"><span class="pulse"></span>Loading tournaments…</div>
} @else if (activeList.length === 0) {
<div class="empty-state">
<div class="empty-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round">
<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/>
<path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/>
<path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/>
<path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/>
</svg>
</div>
<p class="empty-title">No tournaments here</p>
<p class="empty-sub">Check back later or look in another tab.</p>
</div>
} @else {
<div class="t-list">
@for (t of activeList; track t.id) {
<div class="t-card" [class.expanded]="selectedTournament?.id === t.id"
(click)="selectTournament(t)" role="button" tabindex="0"
(keydown.enter)="selectTournament(t)">
<div class="t-card-main">
<div class="t-card-left">
<span class="t-status-dot" [class]="'dot-' + t.status"></span>
<div class="t-info">
<span class="t-name">{{ t.fullName }}</span>
<span class="t-meta">
{{ clockDisplay(t) }} · {{ t.nbRounds }} rounds ·
@if (t.status === 'started') { Round {{ t.round }}/{{ t.nbRounds }} · }
{{ t.nbPlayers }} player{{ t.nbPlayers === 1 ? '' : 's' }}
@if (t.rated) { · Rated }
</span>
</div>
</div>
<div class="t-card-right">
@if (t.status === 'finished' && t.winner) {
<span class="winner-badge">🏆 {{ t.winner.name }}</span>
}
@if (currentUser && t.status === 'created') {
@if (t.createdBy === currentUser.id) {
<button type="button" class="t-action-btn t-btn-start"
[disabled]="startingId === t.id"
(click)="startTournament($event, t)">
{{ startingId === t.id ? '…' : 'Start' }}
</button>
}
<button type="button" class="t-action-btn t-btn-join"
(click)="openJoinDialog($event, t.id)">
Join with bot
</button>
}
<svg class="chevron" [class.open]="selectedTournament?.id === t.id"
width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
</div>
@if (selectedTournament?.id === t.id) {
<div class="t-detail" (click)="$event.stopPropagation()">
<!-- Leaderboard -->
@if (t.standing.players.length > 0) {
<section class="detail-section">
<h3 class="detail-heading">Leaderboard</h3>
<table class="standings-table">
<thead>
<tr>
<th class="col-rank">#</th>
<th class="col-name">Bot</th>
<th class="col-pts">Pts</th>
<th class="col-tb">Bkh</th>
<th class="col-games">W/D/L</th>
</tr>
</thead>
<tbody>
@for (r of t.standing.players; track r.bot.id) {
<tr [class.top-row]="r.rank <= 3">
<td class="col-rank">{{ rankMedal(r.rank) }}</td>
<td class="col-name">{{ r.bot.name }}</td>
<td class="col-pts">{{ scoreDisplay(r) }}</td>
<td class="col-tb">{{ r.tieBreak }}</td>
<td class="col-games">
<span class="wdl">
<span class="w">{{ r.wins }}</span>/<span class="d">{{ r.draws }}</span>/<span class="l">{{ r.losses }}</span>
</span>
</td>
</tr>
}
</tbody>
</table>
</section>
} @else {
<p class="no-standings">No standings yet — waiting for games to complete.</p>
}
<!-- Current round pairings -->
@if (t.round > 0) {
<section class="detail-section">
<h3 class="detail-heading">Round {{ t.round }} pairings</h3>
@if (pairingsLoading) {
<div class="state-msg small"><span class="pulse"></span>Loading…</div>
} @else if (pairings && pairings.pairings.length > 0) {
<div class="pairings-list">
@for (p of pairings.pairings; track p.id) {
<div class="pairing-row" [class.is-watchable]="!!p.gameId"
(click)="p.gameId && watchGame(p.gameId)">
<span class="pairing-white">{{ p.white?.name ?? 'Bye' }}</span>
<span class="pairing-vs">vs</span>
<span class="pairing-black">{{ p.black.name }}</span>
@if (p.winner) {
<span class="pairing-result" [class]="'result-' + p.winner">
{{ p.winner === 'draw' ? '½–½' : p.winner === 'white' ? '10' : '01' }}
</span>
} @else if (p.gameId) {
<span class="pairing-ongoing">
<svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10"/>
</svg>
Watch
</span>
}
</div>
}
</div>
} @else {
<p class="no-standings">No pairings recorded yet.</p>
}
</section>
}
</div>
}
</div>
}
</div>
}
</div>
</div>
@if (joinDialogTournamentId) {
<div class="dialog-overlay" (click)="closeJoinDialog()">
<div class="dialog-card" (click)="$event.stopPropagation()">
<div class="dialog-head">
<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>
</div>
}
@if (showCreateDialog) {
<div class="dialog-overlay" (click)="closeCreateDialog()">
<div class="dialog-card" (click)="$event.stopPropagation()">
<div class="dialog-head">
<span class="dialog-brand">New tournament</span>
<button type="button" class="dialog-close" (click)="closeCreateDialog()">×</button>
</div>
<form [formGroup]="createForm" (ngSubmit)="submitCreate()">
<div class="dialog-field">
<label class="dialog-label">Name</label>
<input type="text" class="dialog-input" formControlName="name" placeholder="e.g. Friday Blitz Open" />
</div>
<div class="dialog-row">
<div class="dialog-field">
<label class="dialog-label">Rounds</label>
<input type="number" class="dialog-input" formControlName="nbRounds" min="1" max="20" />
</div>
<div class="dialog-field">
<label class="dialog-label">Clock (min)</label>
<input type="number" class="dialog-input" formControlName="clockLimitMinutes" min="1" max="60" />
</div>
<div class="dialog-field">
<label class="dialog-label">Increment (s)</label>
<input type="number" class="dialog-input" formControlName="clockIncrement" min="0" max="60" />
</div>
</div>
<label class="dialog-toggle">
<input type="checkbox" formControlName="rated" />
<span class="toggle-track"></span>
<span class="toggle-label">Rated</span>
</label>
@if (createError) {
<div class="dialog-error">{{ createError }}</div>
}
<div class="dialog-actions">
<button type="button" class="btn-ghost" (click)="closeCreateDialog()">Cancel</button>
<button type="submit" class="btn-primary" [disabled]="createLoading || createForm.invalid">
{{ createLoading ? 'Creating…' : 'Create' }}
</button>
</div>
</form>
</div>
</div>
}