import { state } from './state.js';
import { badgeColorForYear } from '../utils/colors.js';
import {
$answerForm,
$answerResult,
$dashboardList,
$guesser,
$lobby,
$mediaControls,
$nextArea,
$np,
$placeArea,
$readyChk,
$revealBanner,
$room,
$roomId,
$slotSelect,
$startGame,
$status,
$timeline,
} from './dom.js';
export function renderRoom(room) {
state.room = room;
if (!room) {
$lobby.classList.remove('hidden');
$room.classList.add('hidden');
return;
}
try {
localStorage.setItem('lastRoomId', room.id);
} catch {}
$lobby.classList.add('hidden');
$room.classList.remove('hidden');
$roomId.textContent = room.id;
$status.textContent = room.state.status;
$guesser.textContent = shortName(room.state.currentGuesser);
// If our local playerId doesn't match any player in the snapshot, but there's
// exactly one player, we must be that player (since only room members get updates).
if (state.playerId && !room.players.some((p) => p.id === state.playerId)) {
if (room.players.length === 1) {
const sole = room.players[0];
state.playerId = sole.id;
try {
localStorage.setItem('playerId', sole.id);
} catch {}
}
}
const me = room.players.find((p) => p.id === state.playerId);
if ($dashboardList) {
$dashboardList.innerHTML = room.players
.map((p) => {
const connected = p.connected
? 'online'
: 'offline';
const ready = p.ready
? 'bereit'
: '-';
const score = room.state.timeline?.[p.id]?.length ?? 0;
const tokens = room.state.tokens?.[p.id] ?? 0;
const isMe = p.id === state.playerId;
return `
|
${escapeHtml(p.name)}${p.spectator ? ' 👻' : ''}
${p.id === room.hostId ? '\u2B50' : ''}
${isMe ? '(du)' : ''}
|
${connected} |
${ready} |
${score} |
${tokens} |
`;
})
.join('');
}
const myTl = room.state.timeline?.[state.playerId] || [];
$timeline.innerHTML = myTl
.map((t) => {
const title = escapeHtml(t.title || t.trackId || 'Unbekannt');
const artist = t.artist ? escapeHtml(t.artist) : '';
const year = t.year ?? '?';
const badgeStyle = badgeColorForYear(year);
return `
`;
})
.join('');
if ($readyChk) {
const serverReady = !!me?.ready;
if (state.pendingReady === null || state.pendingReady === undefined) {
$readyChk.checked = serverReady;
} else {
$readyChk.checked = !!state.pendingReady;
if (serverReady === state.pendingReady) state.pendingReady = null;
}
$readyChk.parentElement.classList.toggle('hidden', room.state.status !== 'lobby');
}
console.log('room host id:', room.hostId, 'my id:', state.playerId);
const isHost = state.playerId === room.hostId;
const activePlayers = room.players.filter((p) => !p.spectator && p.connected);
const allReady = activePlayers.length > 0 && activePlayers.every((p) => p.ready);
console.log('room state status:', room.state.status, 'host:', isHost, 'allReady:', allReady);
const canStart = room.state.status === 'lobby' && isHost && allReady;
console.log(
`Rendering room ${room.id}, players: ${room.players.length}, active: ${activePlayers.length}, ready: ${allReady}, isHost: ${isHost}`
);
console.log('Me:', me);
console.log('Pending ready:', state.pendingReady);
console.log('Room state:', room.state);
console.log('My timeline:', myTl);
console.log('CanStart:', canStart);
if ($startGame) $startGame.classList.toggle('hidden', !canStart);
const isMyTurn =
room.state.status === 'playing' &&
room.state.phase === 'guess' &&
room.state.currentGuesser === state.playerId &&
room.state.currentTrack;
const canGuess = isMyTurn;
// Media controls (play/pause) only for current guesser while guessing and a track is active
if ($mediaControls) $mediaControls.classList.toggle('hidden', !isMyTurn);
// Build slot options for insertion positions when it's my turn
if ($placeArea && $slotSelect) {
if (canGuess) {
const tl = room.state.timeline?.[state.playerId] || [];
$slotSelect.innerHTML = '';
for (let i = 0; i <= tl.length; i++) {
const left = i > 0 ? (tl[i - 1]?.year ?? '?') : null;
const right = i < tl.length ? (tl[i]?.year ?? '?') : null;
let label = '';
if (tl.length === 0) label = 'Einsetzen';
else if (i === 0) label = `Vor (${right})`;
else if (i === tl.length) label = `Nach (${left})`;
else label = `Zwischen (${left} / ${right})`;
const opt = document.createElement('option');
opt.value = String(i);
opt.textContent = label;
$slotSelect.appendChild(opt);
}
} else {
// Clear options when not guessing
$slotSelect.innerHTML = '';
}
$placeArea.classList.toggle('hidden', !canGuess);
}
$np.classList.toggle('hidden', !room.state.currentTrack);
if ($revealBanner) {
const inReveal = room.state.phase === 'reveal';
if (!inReveal) {
$revealBanner.className = 'hidden';
$revealBanner.textContent = '';
}
}
const canNext =
room.state.status === 'playing' &&
room.state.phase === 'reveal' &&
(isHost || room.state.currentGuesser === state.playerId);
if ($nextArea) $nextArea.classList.toggle('hidden', !canNext);
// Answer form visible during guess phase while a track is active
const showAnswer =
room.state.status === 'playing' && room.state.phase === 'guess' && !!room.state.currentTrack;
if ($answerForm) $answerForm.classList.toggle('hidden', !showAnswer);
if ($answerResult && !showAnswer) {
$answerResult.textContent = '';
$answerResult.className = 'mt-1 text-sm';
}
}
export 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 escapeHtml(s) {
return String(s).replace(
/[&<>"']/g,
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]
);
}