From 35b88c0b29657328f52c5daa699185e09ae93d46 Mon Sep 17 00:00:00 2001 From: Elmar Kresse Date: Fri, 5 Sep 2025 14:41:55 +0200 Subject: [PATCH] refactor: Modularize JavaScript Code into Separate Files --- public/js/handlers.js | 229 +++++++++++++++++++++ public/js/main.js | 460 +----------------------------------------- public/js/session.js | 23 +++ public/js/ui.js | 174 ++++++++++++++++ public/js/utils.js | 15 ++ 5 files changed, 449 insertions(+), 452 deletions(-) create mode 100644 public/js/handlers.js create mode 100644 public/js/session.js create mode 100644 public/js/ui.js create mode 100644 public/js/utils.js diff --git a/public/js/handlers.js b/public/js/handlers.js new file mode 100644 index 0000000..5920802 --- /dev/null +++ b/public/js/handlers.js @@ -0,0 +1,229 @@ +import { + $answerResult, + $audio, + $guessArtist, + $guessTitle, + $npArtist, + $npTitle, + $npYear, +} from './dom.js'; +import { state } from './state.js'; +import { cacheLastRoomId, cacheSessionId, sendMsg } from './ws.js'; +import { renderRoom } from './render.js'; +import { applySync } from './audio.js'; + +function updatePlayerIdFromRoom(r) { + try { + if (r?.players?.length === 1) { + const only = r.players[0]; + if (only && only.id && only.id !== state.playerId) { + state.playerId = only.id; + try { + localStorage.setItem('playerId', only.id); + } catch {} + } + } + } catch {} +} + +function shortName(id) { + if (!id) return '-'; + const p = state.room?.players.find((x) => x.id === id); + return p ? p.name : id.slice(0, 4); +} + +export function handleConnected(msg) { + console.debug('handleConnected', msg); + state.playerId = msg.playerId; + try { + if (msg.playerId) localStorage.setItem('playerId', msg.playerId); + } catch {} + if (msg.sessionId) { + const existing = localStorage.getItem('sessionId'); + if (!existing) cacheSessionId(msg.sessionId); + } + + // lazy import to avoid cycle + import('./session.js').then(({ reusePlayerName, reconnectLastRoom }) => { + reusePlayerName(); + reconnectLastRoom(); + }); + + if (state.room) { + try { + updatePlayerIdFromRoom(state.room); + renderRoom(state.room); + } catch {} + } +} + +export function handleRoomUpdate(msg) { + if (msg?.room?.id) cacheLastRoomId(msg.room.id); + const r = msg.room; + updatePlayerIdFromRoom(r); + renderRoom(r); +} + +export function handlePlayTrack(msg) { + const t = msg.track; + state.lastTrack = t; + state.revealed = false; + $npTitle.textContent = '???'; + $npArtist.textContent = ''; + $npYear.textContent = ''; + if ($guessTitle) $guessTitle.value = ''; + if ($guessArtist) $guessArtist.value = ''; + if ($answerResult) { + $answerResult.textContent = ''; + $answerResult.className = 'mt-1 text-sm'; + } + try { + $audio.preload = 'auto'; + } catch {} + $audio.src = t.url; + const pf = document.getElementById('progressFill'); + if (pf) pf.style.width = '0%'; + const rd = document.getElementById('recordDisc'); + if (rd) { + rd.classList.remove('spin-record'); + rd.src = '/hitstar.png'; + } + const { startAt, serverNow } = msg; + const now = Date.now(); + const offsetMs = startAt - serverNow; + const localStart = now + offsetMs; + const delay = Math.max(0, localStart - now); + setTimeout(() => { + $audio.currentTime = 0; + $audio.play().catch(() => {}); + const disc = document.getElementById('recordDisc'); + if (disc) disc.classList.add('spin-record'); + }, delay); + if (state.room) renderRoom(state.room); +} + +export function handleSync(msg) { + applySync(msg.startAt, msg.serverNow); +} + +export function handleControl(msg) { + const { action, startAt, serverNow } = msg; + if (action === 'pause') { + $audio.pause(); + const disc = document.getElementById('recordDisc'); + if (disc) disc.classList.remove('spin-record'); + $audio.playbackRate = 1.0; + } else if (action === 'play') { + if (startAt && serverNow) { + const now = Date.now(); + const elapsed = (now - startAt) / 1000; + $audio.currentTime = Math.max(0, elapsed); + } + $audio.play().catch(() => {}); + const disc = document.getElementById('recordDisc'); + if (disc) disc.classList.add('spin-record'); + } +} + +export function handleReveal(msg) { + const { result, track } = msg; + $npTitle.textContent = track.title || track.id || 'Track'; + $npArtist.textContent = track.artist ? ` – ${track.artist}` : ''; + $npYear.textContent = track.year ? ` (${track.year})` : ''; + state.revealed = true; + const $rb = document.getElementById('revealBanner'); + if ($rb) { + if (result.correct) { + $rb.textContent = 'Richtig!'; + $rb.className = + 'inline-block rounded-md bg-emerald-600 text-white px-3 py-1 text-sm font-medium'; + } else { + $rb.textContent = 'Falsch!'; + $rb.className = + 'inline-block rounded-md bg-rose-600 text-white px-3 py-1 text-sm font-medium'; + } + } + const $placeArea = document.getElementById('placeArea'); + if ($placeArea) $placeArea.classList.add('hidden'); + const rd = document.getElementById('recordDisc'); + if (rd && track?.id) { + const coverUrl = `/cover/${encodeURIComponent(track.id)}`; + const img = new Image(); + img.onload = () => { + rd.src = coverUrl; + }; + img.onerror = () => { + /* keep default logo */ + }; + img.src = `${coverUrl}?t=${Date.now()}`; + } +} + +export function handleGameEnded(msg) { + alert(`Gewinner: ${shortName(msg.winner)}`); +} + +export function onMessage(ev) { + const msg = JSON.parse(ev.data); + switch (msg.type) { + case 'resume_result': { + if (msg.ok) { + console.debug('handleResumeResult', msg); + if (msg.playerId) { + state.playerId = msg.playerId; + try { + localStorage.setItem('playerId', msg.playerId); + } catch {} + } + const code = msg.roomId || state.room?.id || localStorage.getItem('lastRoomId'); + if (code) sendMsg({ type: 'join_room', code }); + if (state.room) { + try { + renderRoom(state.room); + } catch {} + } + } else { + const code = state.room?.id || localStorage.getItem('lastRoomId'); + if (code) sendMsg({ type: 'join_room', code }); + } + return; + } + case 'connected': + return handleConnected(msg); + case 'room_update': + return handleRoomUpdate(msg); + case 'play_track': + return handlePlayTrack(msg); + case 'sync': + return handleSync(msg); + case 'control': + return handleControl(msg); + case 'reveal': + return handleReveal(msg); + case 'game_ended': + return handleGameEnded(msg); + case 'answer_result': { + if ($answerResult) { + if (!msg.ok) { + $answerResult.textContent = '⛔ Eingabe ungültig oder gerade nicht möglich'; + $answerResult.className = 'mt-1 text-sm text-rose-600'; + } else { + const okBoth = !!(msg.correctTitle && msg.correctArtist); + const parts = []; + parts.push(msg.correctTitle ? 'Titel ✓' : 'Titel ✗'); + parts.push(msg.correctArtist ? 'Künstler ✓' : 'Künstler ✗'); + let coin = ''; + if (msg.awarded) coin = ' +1 Token'; + else if (msg.alreadyAwarded) coin = ' (bereits erhalten)'; + $answerResult.textContent = `${parts.join(' · ')}${coin}`; + $answerResult.className = okBoth + ? 'mt-1 text-sm text-emerald-600' + : 'mt-1 text-sm text-amber-600'; + } + } + return; + } + default: + return; + } +} diff --git a/public/js/main.js b/public/js/main.js index 3ebfb51..370b88f 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,457 +1,15 @@ -import { - $audio, - $copyRoomCode, - $leaveRoom, - $nameDisplay, - $nameLobby, - $npArtist, - $npTitle, - $npYear, - $pauseBtn, - $placeBtn, - $readyChk, - $roomId, - $roomCode, - $slotSelect, - $startGame, - $volumeSlider, - $playBtn, - $nextBtn, - $createRoom, - $joinRoom, - $lobby, - $room, - $setNameLobby, - $guessTitle, - $guessArtist, - $answerResult, -} from './dom.js'; import { state } from './state.js'; -import { connectWS, sendMsg, cacheSessionId, cacheLastRoomId } from './ws.js'; -import { renderRoom } from './render.js'; -import { initAudioUI, applySync, stopAudioPlayback } from './audio.js'; +import { connectWS } from './ws.js'; +import { onMessage } from './handlers.js'; +import { wireUi } from './ui.js'; +import { $nameLobby } from './dom.js'; -function showToast(msg) { - const el = document.getElementById('toast'); - if (!el) { - return; - } - el.textContent = msg; - el.style.opacity = '1'; - setTimeout(() => { - el.style.opacity = '0'; - }, 1200); -} - -function handleConnected(msg) { - console.debug('handleConnected', msg); - state.playerId = msg.playerId; - try { - if (msg.playerId) localStorage.setItem('playerId', msg.playerId); - } catch {} - if (msg.sessionId) { - // Don't clobber an existing session on reconnect; only set if none exists yet. - const existing = localStorage.getItem('sessionId'); - if (!existing) { - cacheSessionId(msg.sessionId); - } - } - const stored = localStorage.getItem('playerName'); - if (stored) { - if ($nameLobby && $nameLobby.value !== stored) { - $nameLobby.value = stored; - } - if ($nameDisplay) { - $nameDisplay.textContent = stored; - } - sendMsg({ type: 'set_name', name: stored }); - } - const last = state.room?.id || localStorage.getItem('lastRoomId'); - if (last && !localStorage.getItem('sessionId')) { - sendMsg({ type: 'join_room', code: last }); - } - // If we already have a room snapshot, re-render now that we know our playerId - // to correctly compute host/permissions (e.g., Start button visibility). - if (state.room) { - try { - // If we somehow have a room snapshot already but our playerId doesn't match any player, - // and there's only one player, assume it's us (helps when resuming locally without resume). - const r = state.room; - if (r?.players?.length === 1) { - const only = r.players[0]; - if (only && only.id && only.id !== state.playerId) { - state.playerId = only.id; - try { - localStorage.setItem('playerId', only.id); - } catch {} - } - } - renderRoom(state.room); - } catch {} - } -} - -function handleRoomUpdate(msg) { - if (msg?.room?.id) cacheLastRoomId(msg.room.id); - const r = msg.room; - try { - if (r?.players?.length === 1) { - const only = r.players[0]; - if (only && only.id && only.id !== state.playerId) { - state.playerId = only.id; - try { - localStorage.setItem('playerId', only.id); - } catch {} - } - } - } catch {} - renderRoom(r); -} - -function handlePlayTrack(msg) { - const t = msg.track; - state.lastTrack = t; - state.revealed = false; - $npTitle.textContent = '???'; - $npArtist.textContent = ''; - $npYear.textContent = ''; - // reset answer UI - if ($guessTitle) $guessTitle.value = ''; - if ($guessArtist) $guessArtist.value = ''; - if ($answerResult) { - $answerResult.textContent = ''; - $answerResult.className = 'mt-1 text-sm'; - } - try { - $audio.preload = 'auto'; - } catch {} - $audio.src = t.url; - const pf = document.getElementById('progressFill'); - if (pf) { - pf.style.width = '0%'; - } - const rd = document.getElementById('recordDisc'); - if (rd) { - rd.classList.remove('spin-record'); - // Reset cover to default logo at the start of a new track - rd.src = '/hitstar.png'; - } - const { startAt, serverNow } = msg; - const now = Date.now(); - const offsetMs = startAt - serverNow; - const localStart = now + offsetMs; - const delay = Math.max(0, localStart - now); - setTimeout(() => { - $audio.currentTime = 0; - $audio.play().catch(() => {}); - const disc = document.getElementById('recordDisc'); - if (disc) { - disc.classList.add('spin-record'); - } - }, delay); - if (state.room) { - renderRoom(state.room); - } -} - -function handleSync(msg) { - applySync(msg.startAt, msg.serverNow); -} - -function handleControl(msg) { - const { action, startAt, serverNow } = msg; - if (action === 'pause') { - $audio.pause(); - const disc = document.getElementById('recordDisc'); - if (disc) { - disc.classList.remove('spin-record'); - } - $audio.playbackRate = 1.0; - } else if (action === 'play') { - if (startAt && serverNow) { - const now = Date.now(); - const elapsed = (now - startAt) / 1000; - $audio.currentTime = Math.max(0, elapsed); - } - $audio.play().catch(() => {}); - const disc = document.getElementById('recordDisc'); - if (disc) { - disc.classList.add('spin-record'); - } - } -} - -function handleReveal(msg) { - const { result, track } = msg; - $npTitle.textContent = track.title || track.id || 'Track'; - $npArtist.textContent = track.artist ? ` – ${track.artist}` : ''; - $npYear.textContent = track.year ? ` (${track.year})` : ''; - state.revealed = true; - const $rb = document.getElementById('revealBanner'); - if ($rb) { - if (result.correct) { - $rb.textContent = 'Richtig!'; - $rb.className = - 'inline-block rounded-md bg-emerald-600 text-white px-3 py-1 text-sm font-medium'; - } else { - $rb.textContent = 'Falsch!'; - $rb.className = - 'inline-block rounded-md bg-rose-600 text-white px-3 py-1 text-sm font-medium'; - } - } - const $placeArea = document.getElementById('placeArea'); - if ($placeArea) { - $placeArea.classList.add('hidden'); - } - // Try to load embedded cover art and replace the center image - const rd = document.getElementById('recordDisc'); - if (rd && track?.id) { - const coverUrl = `/cover/${encodeURIComponent(track.id)}`; - const img = new Image(); - img.onload = () => { - rd.src = coverUrl; - }; - img.onerror = () => { - /* keep default logo */ - }; - img.src = coverUrl + `?t=${Date.now()}`; // bypass cache just in case - } -} - -function handleGameEnded(msg) { - alert(`Gewinner: ${shortName(msg.winner)}`); -} - -function onMessage(ev) { - const msg = JSON.parse(ev.data); - switch (msg.type) { - case 'resume_result': - if (msg.ok) { - console.debug('handleResumeResult', msg); - if (msg.playerId) { - state.playerId = msg.playerId; - try { - localStorage.setItem('playerId', msg.playerId); - } catch {} - } - const code = msg.roomId || state.room?.id || localStorage.getItem('lastRoomId'); - if (code) sendMsg({ type: 'join_room', code }); - // Re-render with the now-known playerId so host UI updates immediately. - if (state.room) { - try { - renderRoom(state.room); - } catch {} - } - } else { - const code = state.room?.id || localStorage.getItem('lastRoomId'); - if (code) sendMsg({ type: 'join_room', code }); - } - break; - case 'connected': - handleConnected(msg); - break; - case 'room_update': - handleRoomUpdate(msg); - break; - case 'play_track': - handlePlayTrack(msg); - break; - case 'sync': - handleSync(msg); - break; - case 'control': - handleControl(msg); - break; - case 'reveal': - handleReveal(msg); - break; - case 'game_ended': - handleGameEnded(msg); - break; - case 'answer_result': { - if ($answerResult) { - if (!msg.ok) { - $answerResult.textContent = '⛔ Eingabe ungültig oder gerade nicht möglich'; - $answerResult.className = 'mt-1 text-sm text-rose-600'; - } else { - const okBoth = !!(msg.correctTitle && msg.correctArtist); - const parts = []; - parts.push(msg.correctTitle ? 'Titel ✓' : 'Titel ✗'); - parts.push(msg.correctArtist ? 'Künstler ✓' : 'Künstler ✗'); - let coin = ''; - if (msg.awarded) coin = ' +1 Token'; - else if (msg.alreadyAwarded) coin = ' (bereits erhalten)'; - $answerResult.textContent = `${parts.join(' · ')}${coin}`; - $answerResult.className = okBoth - ? 'mt-1 text-sm text-emerald-600' - : 'mt-1 text-sm text-amber-600'; - } - } - break; - } - default: - break; - } -} - -function shortName(id) { - if (!id) return '-'; - const p = state.room?.players.find((x) => x.id === id); - return p ? p.name : id.slice(0, 4); -} - -function wire(el, type, handler) { - if (el) { - el.addEventListener(type, handler); - } -} - -function wireUi() { - initAudioUI(); - wire($setNameLobby, 'click', () => { - const name = $nameLobby.value.trim(); - if (!name) return; - localStorage.setItem('playerName', name); - if ($nameDisplay) { - $nameDisplay.textContent = name; - } - sendMsg({ type: 'set_name', name }); - }); - wire($createRoom, 'click', () => sendMsg({ type: 'create_room' })); - wire($joinRoom, 'click', () => { - const code = $roomCode.value.trim(); - if (code) { - sendMsg({ type: 'join_room', code }); - } - }); - wire($leaveRoom, 'click', () => { - sendMsg({ type: 'leave_room' }); - // Clear all local storage entries on leave - try { - localStorage.clear(); - } catch {} - stopAudioPlayback(); - state.room = null; - // Reset visible name inputs/labels - if ($nameLobby) { - try { - $nameLobby.value = ''; - } catch {} - } - if ($nameDisplay) { - $nameDisplay.textContent = ''; - } - if ($readyChk) { - try { - $readyChk.checked = false; - } catch {} - } - $lobby.classList.remove('hidden'); - $room.classList.add('hidden'); - }); - wire($startGame, 'click', () => sendMsg({ type: 'start_game' })); - wire($readyChk, 'change', (e) => { - const val = !!e.target.checked; - state.pendingReady = val; - sendMsg({ type: 'set_ready', ready: val }); - }); - wire($placeBtn, 'click', () => { - const slot = parseInt($slotSelect.value, 10); - sendMsg({ type: 'place_guess', slot }); - }); - wire($playBtn, 'click', () => sendMsg({ type: 'player_control', action: 'play' })); - wire($pauseBtn, 'click', () => sendMsg({ type: 'player_control', action: 'pause' })); - wire($nextBtn, 'click', () => sendMsg({ type: 'next_track' })); - if ($volumeSlider && $audio) { - try { - $volumeSlider.value = String($audio.volume ?? 1); - } catch {} - $volumeSlider.addEventListener('input', () => { - $audio.volume = parseFloat($volumeSlider.value); - }); - } - if ($copyRoomCode) { - $copyRoomCode.style.display = 'inline-block'; - wire($copyRoomCode, 'click', () => { - if (state.room?.id) { - navigator.clipboard.writeText(state.room.id).then(() => { - $copyRoomCode.textContent = '✔️'; - showToast('Code kopiert!'); - setTimeout(() => { - $copyRoomCode.textContent = '📋'; - }, 1200); - }); - } - }); - } - if ($roomId) { - wire($roomId, 'click', () => { - if (state.room?.id) { - navigator.clipboard.writeText(state.room.id).then(() => { - $roomId.title = 'Kopiert!'; - showToast('Code kopiert!'); - setTimeout(() => { - $roomId.title = 'Klicken zum Kopieren'; - }, 1200); - }); - } - }); - $roomId.style.cursor = 'pointer'; - } - // Answer submit - const form = document.getElementById('answerForm'); - if (form) { - form.addEventListener('submit', (e) => { - e.preventDefault(); - const title = ($guessTitle?.value || '').trim(); - const artist = ($guessArtist?.value || '').trim(); - if (!title || !artist) { - if ($answerResult) { - $answerResult.textContent = 'Bitte Titel und Künstler eingeben'; - $answerResult.className = 'mt-1 text-sm text-amber-600'; - } - return; - } - sendMsg({ type: 'submit_answer', guess: { title, artist } }); - }); - } - // Dashboard one-time hint - const dashboard = document.getElementById('dashboard'); - const dashboardHint = document.getElementById('dashboardHint'); - if (dashboard && dashboardHint) { - try { - const seen = localStorage.getItem('dashboardHintSeen'); - if (!seen) { - dashboardHint.classList.remove('hidden'); - const hide = () => { - dashboardHint.classList.add('hidden'); - try { - localStorage.setItem('dashboardHintSeen', '1'); - } catch {} - dashboard.removeEventListener('toggle', hide); - dashboard.removeEventListener('click', hide); - }; - dashboard.addEventListener('toggle', hide); - // Also hide on explicit click to cover browsers not firing 'toggle' on details - dashboard.addEventListener('click', hide, { once: true }); - // Auto-hide after 6 seconds if no interaction - setTimeout(() => { - if (!localStorage.getItem('dashboardHintSeen')) { - hide(); - } - }, 6000); - } - } catch {} - } -} - -// boot +// Initialize UI and open WebSocket connection wireUi(); connectWS(onMessage); -// restore name immediately if present +// Restore name/id immediately for initial render smoothness (() => { - // Bootstrap playerId early if we have it from a prior session to avoid null during first render try { const savedPid = localStorage.getItem('playerId'); if (savedPid && !state.playerId) { @@ -459,9 +17,7 @@ connectWS(onMessage); } } catch {} const saved = localStorage.getItem('playerName'); - if (saved) { - if ($nameLobby && $nameLobby.value !== saved) { - $nameLobby.value = saved; - } + if (saved && $nameLobby && $nameLobby.value !== saved) { + $nameLobby.value = saved; } })(); diff --git a/public/js/session.js b/public/js/session.js new file mode 100644 index 0000000..1065854 --- /dev/null +++ b/public/js/session.js @@ -0,0 +1,23 @@ +import { $nameDisplay, $nameLobby } from './dom.js'; +import { state } from './state.js'; +import { sendMsg } from './ws.js'; + +// If we have a stored player name, set it in the input and send to server +export function reusePlayerName() { + const stored = localStorage.getItem('playerName'); + if (!stored) return; + if ($nameLobby && $nameLobby.value !== stored) { + $nameLobby.value = stored; + } + if ($nameDisplay) { + $nameDisplay.textContent = stored; + } + sendMsg({ type: 'set_name', name: stored }); +} + +export function reconnectLastRoom() { + const last = state.room?.id || localStorage.getItem('lastRoomId'); + if (last && !localStorage.getItem('sessionId')) { + sendMsg({ type: 'join_room', code: last }); + } +} diff --git a/public/js/ui.js b/public/js/ui.js new file mode 100644 index 0000000..4cb96b4 --- /dev/null +++ b/public/js/ui.js @@ -0,0 +1,174 @@ +import { + $audio, + $answerResult, + $copyRoomCode, + $createRoom, + $guessArtist, + $guessTitle, + $joinRoom, + $lobby, + $nameDisplay, + $nameLobby, + $nextBtn, + $pauseBtn, + $placeBtn, + $readyChk, + $room, + $roomCode, + $roomId, + $setNameLobby, + $slotSelect, + $startGame, + $leaveRoom, + $playBtn, + $volumeSlider, +} from './dom.js'; +import { state } from './state.js'; +import { initAudioUI, stopAudioPlayback } from './audio.js'; +import { sendMsg } from './ws.js'; +import { showToast, wire } from './utils.js'; + +export function wireUi() { + initAudioUI(); + + wire($setNameLobby, 'click', () => { + const name = $nameLobby.value.trim(); + if (!name) return; + localStorage.setItem('playerName', name); + if ($nameDisplay) $nameDisplay.textContent = name; + sendMsg({ type: 'set_name', name }); + }); + + wire($createRoom, 'click', () => sendMsg({ type: 'create_room' })); + + wire($joinRoom, 'click', () => { + const code = $roomCode.value.trim(); + if (code) sendMsg({ type: 'join_room', code }); + }); + + wire($leaveRoom, 'click', () => { + sendMsg({ type: 'leave_room' }); + try { + localStorage.removeItem('playerId'); + localStorage.removeItem('sessionId'); + localStorage.removeItem('dashboardHintSeen'); + localStorage.removeItem('lastRoomId'); + } catch {} + stopAudioPlayback(); + state.room = null; + if ($nameLobby) { + try { + const storedName = localStorage.getItem('playerName') || ''; + $nameLobby.value = storedName; + } catch { + $nameLobby.value = ''; + } + } + if ($nameDisplay) $nameDisplay.textContent = ''; + if ($readyChk) { + try { + $readyChk.checked = false; + } catch {} + } + $lobby.classList.remove('hidden'); + $room.classList.add('hidden'); + }); + + wire($startGame, 'click', () => sendMsg({ type: 'start_game' })); + + wire($readyChk, 'change', (e) => { + const val = !!e.target.checked; + state.pendingReady = val; + sendMsg({ type: 'set_ready', ready: val }); + }); + + wire($placeBtn, 'click', () => { + const slot = parseInt($slotSelect.value, 10); + sendMsg({ type: 'place_guess', slot }); + }); + + wire($playBtn, 'click', () => sendMsg({ type: 'player_control', action: 'play' })); + wire($pauseBtn, 'click', () => sendMsg({ type: 'player_control', action: 'pause' })); + wire($nextBtn, 'click', () => sendMsg({ type: 'next_track' })); + + if ($volumeSlider && $audio) { + try { + $volumeSlider.value = String($audio.volume ?? 1); + } catch {} + $volumeSlider.addEventListener('input', () => { + $audio.volume = parseFloat($volumeSlider.value); + }); + } + + if ($copyRoomCode) { + $copyRoomCode.style.display = 'inline-block'; + wire($copyRoomCode, 'click', () => { + if (state.room?.id) { + navigator.clipboard.writeText(state.room.id).then(() => { + $copyRoomCode.textContent = '✔️'; + showToast('Code kopiert!'); + setTimeout(() => { + $copyRoomCode.textContent = '📋'; + }, 1200); + }); + } + }); + } + + if ($roomId) { + wire($roomId, 'click', () => { + if (state.room?.id) { + navigator.clipboard.writeText(state.room.id).then(() => { + $roomId.title = 'Kopiert!'; + showToast('Code kopiert!'); + setTimeout(() => { + $roomId.title = 'Klicken zum Kopieren'; + }, 1200); + }); + } + }); + $roomId.style.cursor = 'pointer'; + } + + const form = document.getElementById('answerForm'); + if (form) { + form.addEventListener('submit', (e) => { + e.preventDefault(); + const title = ($guessTitle?.value || '').trim(); + const artist = ($guessArtist?.value || '').trim(); + if (!title || !artist) { + if ($answerResult) { + $answerResult.textContent = 'Bitte Titel und Künstler eingeben'; + $answerResult.className = 'mt-1 text-sm text-amber-600'; + } + return; + } + sendMsg({ type: 'submit_answer', guess: { title, artist } }); + }); + } + + // Dashboard one-time hint + const dashboard = document.getElementById('dashboard'); + const dashboardHint = document.getElementById('dashboardHint'); + if (dashboard && dashboardHint) { + try { + const seen = localStorage.getItem('dashboardHintSeen'); + if (!seen) { + dashboardHint.classList.remove('hidden'); + const hide = () => { + dashboardHint.classList.add('hidden'); + try { + localStorage.setItem('dashboardHintSeen', '1'); + } catch {} + dashboard.removeEventListener('toggle', hide); + dashboard.removeEventListener('click', hide); + }; + dashboard.addEventListener('toggle', hide); + dashboard.addEventListener('click', hide, { once: true }); + setTimeout(() => { + if (!localStorage.getItem('dashboardHintSeen')) hide(); + }, 6000); + } + } catch {} + } +} diff --git a/public/js/utils.js b/public/js/utils.js new file mode 100644 index 0000000..8b1b204 --- /dev/null +++ b/public/js/utils.js @@ -0,0 +1,15 @@ +// Shared small utilities + +export function showToast(msg) { + const el = document.getElementById('toast'); + if (!el) return; + el.textContent = msg; + el.style.opacity = '1'; + setTimeout(() => { + el.style.opacity = '0'; + }, 1200); +} + +export function wire(el, type, handler, options) { + if (el) el.addEventListener(type, handler, options); +}