From d73f0da1f982f1d2cf9e295dc7b679f306fd4fa6 Mon Sep 17 00:00:00 2001 From: Ezequiel Juarez Garcia Date: Sun, 10 May 2026 14:49:29 -0400 Subject: [PATCH] feat(02-03): add frontend host UI with token claim, controls, and pause/resume toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.html: host-controls div in now-playing with pause(⏸)/resume(▶)/skip(⏭) buttons - style.css: .host-controls flexbox center, .host-btn 44px touch target #e94560, hover #d13350 - app.js: hostToken from URLSearchParams, client.host_claim on connect, renderHostControls toggle - server.role handler triggers host UI (nickname (host) + controls div) - Host button handlers: Pause swaps to Resume, Resume swaps to Pause, Skip skips - updatePlayback syncs pause/resume button state from server paused flag --- static/app.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++- static/index.html | 11 ++++++++ static/style.css | 35 ++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/static/app.js b/static/app.js index 0b5899f..2da9b75 100644 --- a/static/app.js +++ b/static/app.js @@ -5,6 +5,10 @@ var maxReconnectDelay = 16000; var myNickname = null; var isHost = false; var myVotes = []; +var hostToken = null; + +var urlParams = new URLSearchParams(window.location.search); +hostToken = urlParams.get("host"); function formatDuration(seconds) { if (seconds == null) return ""; @@ -99,6 +103,9 @@ function renderFullState(data) { } renderNicknameDisplay(); + if (isHost) { + renderHostControls(true); + } } function formatSeconds(s) { @@ -157,6 +164,20 @@ function updatePlayback(data) { document.getElementById("current-title").textContent = "\u2014"; document.getElementById("current-duration").textContent = ""; } + + if (isHost) { + var pauseBtn = document.getElementById("host-pause"); + var resumeBtn = document.getElementById("host-resume"); + if (pauseBtn && resumeBtn) { + if (payload.paused) { + pauseBtn.style.display = "none"; + resumeBtn.style.display = "inline-flex"; + } else { + pauseBtn.style.display = "inline-flex"; + resumeBtn.style.display = "none"; + } + } + } } function updateTime(data) { @@ -242,6 +263,7 @@ function handleWsMessage(event) { if (data.payload && data.payload.role === "host") { isHost = true; renderNicknameDisplay(); + renderHostControls(true); } break; } @@ -328,6 +350,17 @@ function enableNicknameEdit() { }; } +function renderHostControls(showHost) { + var container = document.getElementById("host-controls"); + if (!container) return; + + if (showHost) { + container.style.display = "flex"; + } else { + container.style.display = "none"; + } +} + function connectWs() { var proto = location.protocol === "https:" ? "wss" : "ws"; var url = proto + "://" + location.host + "/ws"; @@ -337,6 +370,13 @@ function connectWs() { console.log("WebSocket connected"); reconnectDelay = 1000; + if (hostToken) { + ws.send(JSON.stringify({ + type: "client.host_claim", + payload: { token: hostToken } + })); + } + var storedNickname = localStorage.getItem("jam-session-nickname"); if (storedNickname && !myNickname) { ws.send(JSON.stringify({ @@ -380,7 +420,29 @@ document.getElementById("add-form").addEventListener("submit", function (e) { showToast("Not connected. Trying to reconnect..."); btn.disabled = false; btn.textContent = "Add"; - connectWs(); +connectWs(); + +document.getElementById("host-pause").addEventListener("click", function () { + if (ws && ws.readyState === WebSocket.OPEN && isHost) { + ws.send(JSON.stringify({ type: "client.host.pause", payload: {} })); + this.style.display = "none"; + document.getElementById("host-resume").style.display = "inline-flex"; + } +}); + +document.getElementById("host-resume").addEventListener("click", function () { + if (ws && ws.readyState === WebSocket.OPEN && isHost) { + ws.send(JSON.stringify({ type: "client.host.resume", payload: {} })); + this.style.display = "none"; + document.getElementById("host-pause").style.display = "inline-flex"; + } +}); + +document.getElementById("host-skip").addEventListener("click", function () { + if (ws && ws.readyState === WebSocket.OPEN && isHost) { + ws.send(JSON.stringify({ type: "client.host.skip", payload: {} })); + } +}); } }); diff --git a/static/index.html b/static/index.html index ef73b3e..161900f 100644 --- a/static/index.html +++ b/static/index.html @@ -26,6 +26,17 @@

—

+

--:--

diff --git a/static/style.css b/static/style.css index a3d7ff7..0fabef8 100644 --- a/static/style.css +++ b/static/style.css @@ -346,3 +346,38 @@ button:hover { color: #666; user-select: none; } + +/* === Host Controls === */ + +.host-controls { + display: flex; + justify-content: center; + gap: 8px; + margin-top: 12px; + margin-bottom: 8px; +} + +.host-btn { + background: #e94560; + color: #ffffff; + border: none; + border-radius: 8px; + min-width: 44px; + min-height: 44px; + padding: 8px 12px; + font-size: 20px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + line-height: 1; +} + +.host-btn:hover { + background: #d13350; +} + +.host-btn-icon { + line-height: 1; +} -- 2.51.2