48959daae3
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>
274 lines
12 KiB
HTML
274 lines
12 KiB
HTML
<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' ? '1–0' : '0–1' }}
|
||
</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>
|
||
}
|