diff --git a/assets/admin-session-tabs.js b/assets/admin-session-tabs.js new file mode 100644 index 0000000..a913751 --- /dev/null +++ b/assets/admin-session-tabs.js @@ -0,0 +1,27 @@ +document.addEventListener('DOMContentLoaded', () => { + const buttons = document.querySelectorAll('[data-tab-target]'); + if (!buttons.length) { + return; + } + + const activate = (id) => { + document.querySelectorAll('[id^="player-"]').forEach(el => el.style.display = 'none'); + const target = document.getElementById(id); + if (target) { + target.style.display = 'block'; + } + + buttons.forEach(btn => { + const active = btn.dataset.tabTarget === id; + btn.style.background = active ? '#fff' : '#f8fafc'; + btn.style.color = active ? '#1e40af' : '#64748b'; + btn.style.fontWeight = active ? '600' : '400'; + btn.style.borderColor = active ? '#3b82f6' : '#e2e8f0'; + btn.style.borderBottom = active ? '1px solid #fff' : '1px solid #e2e8f0'; + }); + }; + + buttons.forEach(btn => { + btn.addEventListener('click', () => activate(btn.dataset.tabTarget)); + }); +}); diff --git a/assets/app.js b/assets/app.js index 598d0b7..14b7557 100644 --- a/assets/app.js +++ b/assets/app.js @@ -5,3 +5,6 @@ import './styles/app.scss'; import 'bootstrap/js/dist/collapse'; import 'bootstrap/js/dist/alert'; import 'bootstrap/js/dist/dropdown'; +import './game-waiting'; +import './game-lobby'; +import './admin-session-tabs'; diff --git a/assets/game-lobby.js b/assets/game-lobby.js new file mode 100644 index 0000000..7fce79b --- /dev/null +++ b/assets/game-lobby.js @@ -0,0 +1,62 @@ +document.addEventListener('DOMContentLoaded', () => { + const config = document.getElementById('game-lobby-config'); + if (!config) { + return; + } + + const publicUrl = config.dataset.mercurePublicUrl; + const topic = config.dataset.topic; + const chatLog = document.getElementById('lobby-chat-log'); + + function appendLobbyMessage(username, content, createdAt) { + if (!chatLog) { + return; + } + const emptyNotice = document.getElementById('lobby-chat-empty'); + if (emptyNotice) { + emptyNotice.remove(); + } + + const time = createdAt + ? new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : ''; + + const wrapper = document.createElement('div'); + wrapper.className = 'lobby-message'; + + const author = document.createElement('strong'); + author.textContent = username; + + const timestamp = document.createElement('span'); + timestamp.className = 'text-muted small'; + timestamp.textContent = ' ' + time; + + const body = document.createElement('div'); + body.textContent = content; + + wrapper.appendChild(author); + wrapper.appendChild(timestamp); + wrapper.appendChild(body); + chatLog.appendChild(wrapper); + chatLog.scrollTop = chatLog.scrollHeight; + } + + if (publicUrl && topic) { + const url = new URL(publicUrl); + url.searchParams.append('topic', topic); + + const eventSource = new EventSource(url); + eventSource.onmessage = event => { + const data = JSON.parse(event.data); + if (data.type === 'lobby_message') { + appendLobbyMessage(data.username, data.content, data.createdAt); + } else if (data.type === 'player_joined' || data.type === 'session_started') { + window.location.reload(); + } + }; + } + + if (chatLog) { + chatLog.scrollTop = chatLog.scrollHeight; + } +}); diff --git a/assets/game-waiting.js b/assets/game-waiting.js new file mode 100644 index 0000000..feb868d --- /dev/null +++ b/assets/game-waiting.js @@ -0,0 +1,68 @@ +document.addEventListener('DOMContentLoaded', () => { + const config = document.getElementById('game-waiting-config'); + if (!config) { + return; + } + + const publicUrl = config.dataset.mercurePublicUrl; + const topic = config.dataset.topic; + const readyAt = config.dataset.readyAt; + + let reloading = false; + const reloadOnce = (eventSource) => { + if (reloading) { + return; + } + reloading = true; + if (eventSource) { + eventSource.close(); + } + window.location.reload(); + }; + + if (publicUrl && topic) { + const url = new URL(publicUrl); + url.searchParams.append('topic', topic); + + const eventSource = new EventSource(url); + eventSource.onmessage = event => { + const data = JSON.parse(event.data); + if (data.type === 'all_ready' || data.type === 'player_ready') { + reloadOnce(eventSource); + } + }; + } + + // Our own ready status expires 60s after we set it - proactively tell the + // server as close to that deadline as possible, so the other players find + // out live instead of only whenever someone else's request happens to + // trigger the lazy check. + if (readyAt) { + const timeoutMs = 61000; // slightly more than the server-side 60s + const readyAtMs = readyAt * 1000; + const countdownEl = document.getElementById('ready-countdown'); + const expireForm = document.getElementById('expire-ready-form'); + + const updateCountdown = () => { + const remaining = Math.max(0, Math.ceil((readyAtMs + timeoutMs - Date.now()) / 1000)); + if (countdownEl) { + const m = Math.floor(remaining / 60); + const s = remaining % 60; + countdownEl.textContent = m + ':' + s.toString().padStart(2, '0'); + } + return remaining; + }; + + const remaining = updateCountdown(); + if (remaining <= 0) { + expireForm?.submit(); + } else { + const countdownInterval = setInterval(() => { + if (updateCountdown() <= 0) { + clearInterval(countdownInterval); + expireForm?.submit(); + } + }, 1000); + } + } +}); diff --git a/templates/game/waiting.html.twig b/templates/game/waiting.html.twig index 69f66b5..88c43bc 100644 --- a/templates/game/waiting.html.twig +++ b/templates/game/waiting.html.twig @@ -90,74 +90,10 @@ -
- - {% endblock %}