From 415c77efa0ffa3651bf05c38a11ecec8aa2d31b2 Mon Sep 17 00:00:00 2001 From: Janis Eccarius Date: Tue, 16 Jun 2026 08:40:25 +0200 Subject: [PATCH] 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 --- .../tournaments/tournaments.component.css | 83 ++++++++++++ .../tournaments/tournaments.component.html | 119 ++++++++++++++++-- .../tournaments/tournaments.component.ts | 101 ++++++++++++++- src/app/services/official-bot.service.ts | 22 ++++ src/app/services/tournament-server.service.ts | 31 +++++ 5 files changed, 344 insertions(+), 12 deletions(-) create mode 100644 src/app/services/official-bot.service.ts create mode 100644 src/app/services/tournament-server.service.ts diff --git a/src/app/pages/tournaments/tournaments.component.css b/src/app/pages/tournaments/tournaments.component.css index 6e40a72..e102bd0 100644 --- a/src/app/pages/tournaments/tournaments.component.css +++ b/src/app/pages/tournaments/tournaments.component.css @@ -64,6 +64,18 @@ } .page-title { font-size: 22px; font-weight: 700; margin: 0; letter-spacing: -0.02em; } +.page-actions { display: flex; align-items: center; gap: 8px; } + +.btn-servers { + display: inline-flex; align-items: center; gap: 6px; + padding: 7px 14px; border-radius: 8px; + border: 1px solid var(--nc-border-strong); + background: transparent; color: var(--nc-text-muted); + font-size: 13px; font-weight: 600; cursor: pointer; + transition: color 0.15s, border-color 0.15s; +} +.btn-servers:hover { color: var(--nc-text); border-color: var(--nc-text-muted); } + .btn-new { display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 8px; border: none; @@ -327,3 +339,74 @@ color: var(--nc-success); letter-spacing: 0.06em; text-transform: uppercase; } .pairing-ongoing svg { animation: pulse 1.4s ease-in-out infinite; } + +/* Official bot join section */ +.join-divider { + display: flex; align-items: center; gap: 10px; + margin: 20px 0 14px; +} +.join-divider::before, .join-divider::after { + content: ''; flex: 1; height: 1px; background: var(--nc-border); +} +.join-divider-label { + font-size: 10px; font-weight: 600; text-transform: uppercase; + letter-spacing: 0.08em; color: var(--nc-text-dim); white-space: nowrap; +} + +.official-bot-grid { + display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; + margin-bottom: 4px; +} + +.official-bot-btn { + display: flex; align-items: center; justify-content: center; gap: 6px; + padding: 9px 4px; border-radius: 8px; border: 1px solid var(--nc-border); + background: var(--nc-surface); font-size: 12px; font-weight: 700; + cursor: pointer; transition: border-color 0.15s, background 0.15s, color 0.15s; + color: var(--nc-text-muted); text-transform: capitalize; +} +.official-bot-btn:disabled { opacity: 0.45; cursor: not-allowed; } +.official-btn-easy:hover:not(:disabled) { border-color: var(--nc-success); color: var(--nc-success); background: rgba(94,229,161,0.07); } +.official-btn-medium:hover:not(:disabled) { border-color: var(--nc-warn); color: var(--nc-warn); background: rgba(255,209,102,0.07); } +.official-btn-hard:hover:not(:disabled) { border-color: var(--nc-neon); color: var(--nc-neon); background: rgba(255,69,200,0.07); } +.official-btn-expert:hover:not(:disabled) { border-color: var(--nc-danger); color: var(--nc-danger); background: rgba(255,122,122,0.07); } + +/* Servers dialog */ +.servers-dialog { max-width: 480px; } + +.servers-list { + display: flex; flex-direction: column; gap: 6px; + margin-bottom: 20px; +} +.server-row { + display: flex; align-items: center; gap: 10px; + padding: 10px 12px; border-radius: 8px; + border: 1px solid var(--nc-border); background: var(--nc-surface); +} +.server-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; } +.server-label { font-size: 13px; font-weight: 600; color: var(--nc-text); } +.server-url { + font-size: 11px; color: var(--nc-text-dim); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + font-family: monospace; +} +.server-remove-btn { + background: none; border: none; cursor: pointer; + color: var(--nc-text-dim); padding: 4px; border-radius: 6px; + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; transition: color 0.15s, background 0.15s; + font-size: 13px; +} +.server-remove-btn:hover:not(:disabled) { color: var(--nc-danger); background: rgba(255,122,122,0.1); } +.server-remove-btn:disabled { opacity: 0.4; cursor: not-allowed; } + +.server-add-form { + border-top: 1px solid var(--nc-border); + padding-top: 16px; + display: flex; flex-direction: column; gap: 0; +} +.server-add-heading { + font-size: 11px; font-weight: 700; text-transform: uppercase; + letter-spacing: 0.08em; color: var(--nc-text-muted); + margin: 0 0 14px; +} diff --git a/src/app/pages/tournaments/tournaments.component.html b/src/app/pages/tournaments/tournaments.component.html index 6ce1fe6..7c51411 100644 --- a/src/app/pages/tournaments/tournaments.component.html +++ b/src/app/pages/tournaments/tournaments.component.html @@ -16,15 +16,25 @@