feat(tournaments): external server management UI and official-bot join
Add TournamentServerService (GET/POST/DELETE /api/tournament/servers). Add OfficialBotService (POST /api/bots/official/join-tournament). Tournaments page gains a Servers button that opens a dialog to register, list, and remove external tournament servers. Join dialog gains four difficulty buttons (Easy/Medium/Hard/Expert) for spawning official bots into a tournament at runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,15 +16,25 @@
|
||||
<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 class="page-actions">
|
||||
@if (currentUser) {
|
||||
<button type="button" class="btn-servers" (click)="openServersDialog()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||
</svg>
|
||||
Servers
|
||||
</button>
|
||||
<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>
|
||||
<div class="tabs" role="tablist">
|
||||
<button type="button" class="tab-btn" [class.active]="tab === 'started'" (click)="setTab('started')">
|
||||
@@ -218,6 +228,97 @@
|
||||
@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>
|
||||
}
|
||||
{{ d | titlecase }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (officialJoinError) {
|
||||
<div class="dialog-error">{{ officialJoinError }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showServersDialog) {
|
||||
<div class="dialog-overlay" (click)="closeServersDialog()">
|
||||
<div class="dialog-card servers-dialog" (click)="$event.stopPropagation()">
|
||||
<div class="dialog-head">
|
||||
<span class="dialog-brand">Tournament servers</span>
|
||||
<button type="button" class="dialog-close" (click)="closeServersDialog()">×</button>
|
||||
</div>
|
||||
|
||||
<p class="join-hint">External tournament servers aggregated into this view. Tournaments from all servers appear in the list.</p>
|
||||
|
||||
@if (serversLoading) {
|
||||
<div class="dialog-loading"><span class="pulse"></span>Loading servers…</div>
|
||||
} @else if (servers.length === 0) {
|
||||
<p class="join-empty">No external servers registered yet.</p>
|
||||
} @else {
|
||||
<div class="servers-list">
|
||||
@for (s of servers; track s.id) {
|
||||
<div class="server-row">
|
||||
<div class="server-info">
|
||||
<span class="server-label">{{ s.label }}</span>
|
||||
<span class="server-url">{{ s.url }}</span>
|
||||
</div>
|
||||
<button type="button" class="server-remove-btn"
|
||||
[disabled]="removingServerId === s.id"
|
||||
(click)="removeServer(s.id)"
|
||||
title="Remove server">
|
||||
@if (removingServerId === s.id) { … } @else {
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
||||
<path d="M10 11v6"/><path d="M14 11v6"/>
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="server-add-form">
|
||||
<h4 class="server-add-heading">Add server</h4>
|
||||
<div class="dialog-field">
|
||||
<label class="dialog-label">Label</label>
|
||||
<input type="text" class="dialog-input" [(ngModel)]="newServerLabel"
|
||||
placeholder="e.g. Local Dev Server" />
|
||||
</div>
|
||||
<div class="dialog-field">
|
||||
<label class="dialog-label">URL</label>
|
||||
<input type="url" class="dialog-input" [(ngModel)]="newServerUrl"
|
||||
placeholder="http://host:8089" />
|
||||
</div>
|
||||
@if (addServerError) {
|
||||
<div class="dialog-error">{{ addServerError }}</div>
|
||||
}
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="btn-ghost" (click)="closeServersDialog()">Close</button>
|
||||
<button type="button" class="btn-primary"
|
||||
[disabled]="addingServer || !newServerLabel.trim() || !newServerUrl.trim()"
|
||||
(click)="addServer()">
|
||||
{{ addingServer ? 'Adding…' : 'Add' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user