refactor: Modularize JavaScript Code into Separate Files
All checks were successful
Build and Push Docker Image / docker (push) Successful in 9s
All checks were successful
Build and Push Docker Image / docker (push) Successful in 9s
This commit is contained in:
229
public/js/handlers.js
Normal file
229
public/js/handlers.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 { state } from './state.js';
|
||||||
import { connectWS, sendMsg, cacheSessionId, cacheLastRoomId } from './ws.js';
|
import { connectWS } from './ws.js';
|
||||||
import { renderRoom } from './render.js';
|
import { onMessage } from './handlers.js';
|
||||||
import { initAudioUI, applySync, stopAudioPlayback } from './audio.js';
|
import { wireUi } from './ui.js';
|
||||||
|
import { $nameLobby } from './dom.js';
|
||||||
|
|
||||||
function showToast(msg) {
|
// Initialize UI and open WebSocket connection
|
||||||
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
|
|
||||||
wireUi();
|
wireUi();
|
||||||
connectWS(onMessage);
|
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 {
|
try {
|
||||||
const savedPid = localStorage.getItem('playerId');
|
const savedPid = localStorage.getItem('playerId');
|
||||||
if (savedPid && !state.playerId) {
|
if (savedPid && !state.playerId) {
|
||||||
@@ -459,9 +17,7 @@ connectWS(onMessage);
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
const saved = localStorage.getItem('playerName');
|
const saved = localStorage.getItem('playerName');
|
||||||
if (saved) {
|
if (saved && $nameLobby && $nameLobby.value !== saved) {
|
||||||
if ($nameLobby && $nameLobby.value !== saved) {
|
$nameLobby.value = saved;
|
||||||
$nameLobby.value = saved;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
23
public/js/session.js
Normal file
23
public/js/session.js
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
174
public/js/ui.js
Normal file
174
public/js/ui.js
Normal file
@@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
public/js/utils.js
Normal file
15
public/js/utils.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user