This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Blind Test – Master</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="master">
|
||||
<header><h1>🎧 Blind Test</h1><div id="roomTag" class="tag"></div></header>
|
||||
|
||||
<div id="mainLayout" class="layout">
|
||||
<section>
|
||||
<!-- Lobby : QR code + saisie de l'URL -->
|
||||
<div id="lobby" class="card">
|
||||
<div class="lobby-grid">
|
||||
<div class="qr">
|
||||
<img id="qr" alt="QR code pour rejoindre la partie">
|
||||
<p id="joinUrl" class="small"></p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Nouveau morceau</h2>
|
||||
<p class="muted">Colle l'URL d'une vidéo YouTube puis valide.</p>
|
||||
<div class="row">
|
||||
<input id="url" type="url" placeholder="https://www.youtube.com/watch?v=…" autocomplete="off">
|
||||
<button id="go" class="primary">Valider</button>
|
||||
</div>
|
||||
<p id="urlError" class="error"></p>
|
||||
<button id="endFromLobby" class="ghost">Terminer la partie</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jeu : vidéo + buzz -->
|
||||
<div id="game" class="card" hidden>
|
||||
<div class="video">
|
||||
<div id="yt"></div>
|
||||
<div id="mask" class="mask" hidden>🎵</div>
|
||||
</div>
|
||||
<div id="status" class="status"></div>
|
||||
<div id="judge" class="judge" hidden>
|
||||
<button id="btnTrue" class="ok">✅ Vrai</button>
|
||||
<button id="btnFalse" class="ko">❌ Faux</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="next" class="primary"></button>
|
||||
<button id="endBtn" class="ghost">Terminer la partie</button>
|
||||
<label><input type="checkbox" id="hide"> Masquer la vidéo</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fin de partie -->
|
||||
<div id="endScreen" class="card" hidden>
|
||||
<h2>🏆 Résultats</h2>
|
||||
<ol id="final" class="board final"></ol>
|
||||
<button id="newGame" class="primary">Nouvelle partie</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside id="side" class="card">
|
||||
<h2>Participants</h2>
|
||||
<ul id="board" class="board"></ul>
|
||||
<p id="empty" class="muted">Personne n'a encore rejoint.</p>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
const socket = io();
|
||||
let roomId = localStorage.getItem('bt_room');
|
||||
let state = { phase: 'lobby', players: [], buzzer: null, lockedOut: [] };
|
||||
let nextVideo = null;
|
||||
|
||||
/* ---------- YouTube ---------- */
|
||||
let yt = null, ytReady = false, queued = null;
|
||||
const apiTag = document.createElement('script');
|
||||
apiTag.src = 'https://www.youtube.com/iframe_api';
|
||||
document.head.appendChild(apiTag);
|
||||
window.onYouTubeIframeAPIReady = () => { if (queued && !yt) createPlayer(); };
|
||||
function createPlayer() {
|
||||
yt = new YT.Player('yt', {
|
||||
width: '100%', height: '100%',
|
||||
playerVars: { autoplay: 1, rel: 0, playsinline: 1, origin: location.origin },
|
||||
events: { onReady: () => { ytReady = true; if (queued) { yt.loadVideoById(queued); queued = null; } } }
|
||||
});
|
||||
}
|
||||
function loadVideo(id) {
|
||||
if (ytReady) { yt.loadVideoById(id); return; }
|
||||
queued = id;
|
||||
if (!yt && window.YT && YT.Player) createPlayer();
|
||||
}
|
||||
const ytDo = fn => { if (ytReady) yt[fn](); };
|
||||
|
||||
function parseYoutubeId(input) {
|
||||
input = input.trim();
|
||||
if (/^[\w-]{11}$/.test(input)) return input;
|
||||
try {
|
||||
const u = new URL(input);
|
||||
const h = u.hostname.replace(/^www\.|^m\./, '');
|
||||
if (h === 'youtu.be') return u.pathname.slice(1, 12);
|
||||
if (h.endsWith('youtube.com')) {
|
||||
if (u.searchParams.get('v')) return u.searchParams.get('v').slice(0, 11);
|
||||
const m = u.pathname.match(/^\/(embed|shorts|live)\/([\w-]{11})/);
|
||||
if (m) return m[2];
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- Socket ---------- */
|
||||
socket.on('connect', () => {
|
||||
socket.emit('master:create', { roomId }, res => {
|
||||
roomId = res.roomId;
|
||||
localStorage.setItem('bt_room', roomId);
|
||||
const joinUrl = `${location.origin}/buzzer.html?room=${roomId}`;
|
||||
$('qr').src = '/qr.svg?text=' + encodeURIComponent(joinUrl);
|
||||
$('joinUrl').textContent = joinUrl;
|
||||
$('roomTag').textContent = 'SALLE ' + roomId;
|
||||
});
|
||||
});
|
||||
socket.on('state', s => { const prev = state.phase; state = s; render(prev); });
|
||||
|
||||
/* ---------- Rendu ---------- */
|
||||
function sorted(players) {
|
||||
return [...players].sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
||||
}
|
||||
function fillBoard(el, players, medals) {
|
||||
el.textContent = '';
|
||||
const icons = ['🥇', '🥈', '🥉'];
|
||||
sorted(players).forEach((p, i) => {
|
||||
const li = document.createElement('li');
|
||||
if (medals) {
|
||||
const m = document.createElement('span'); m.textContent = icons[i] || (i + 1) + '.'; li.appendChild(m);
|
||||
} else {
|
||||
const d = document.createElement('span'); d.className = 'dot' + (p.connected ? '' : ' off'); li.appendChild(d);
|
||||
}
|
||||
const n = document.createElement('span'); n.className = 'name'; n.textContent = p.name;
|
||||
const s = document.createElement('span'); s.className = 'score'; s.textContent = p.score;
|
||||
li.append(n, s);
|
||||
el.appendChild(li);
|
||||
});
|
||||
}
|
||||
function render(prev) {
|
||||
const ph = state.phase;
|
||||
$('lobby').hidden = ph !== 'lobby';
|
||||
$('game').hidden = !['playing', 'buzzed', 'result'].includes(ph);
|
||||
$('endScreen').hidden = ph !== 'ended';
|
||||
$('side').hidden = ph === 'ended';
|
||||
$('mainLayout').style.gridTemplateColumns = ph === 'ended' ? '1fr' : '';
|
||||
$('empty').hidden = state.players.length > 0;
|
||||
fillBoard($('board'), state.players, false);
|
||||
fillBoard($('final'), state.players, true);
|
||||
|
||||
// Vidéo
|
||||
if (ph !== prev) {
|
||||
if (ph === 'playing' && prev === 'lobby' && nextVideo) { loadVideo(nextVideo); nextVideo = null; }
|
||||
else if (ph === 'playing' && prev === 'buzzed') ytDo('playVideo');
|
||||
else if (ph === 'buzzed') ytDo('pauseVideo');
|
||||
else if (ph === 'lobby' || ph === 'ended') ytDo('stopVideo');
|
||||
}
|
||||
|
||||
// Statut
|
||||
const name = state.buzzer ? state.buzzer.name : '';
|
||||
let text = '';
|
||||
if (ph === 'playing') {
|
||||
text = '🎵 Écoute… premier à buzzer !';
|
||||
if (state.lockedOut.length) text += ` (${state.lockedOut.length} bloqué${state.lockedOut.length > 1 ? 's' : ''})`;
|
||||
} else if (ph === 'buzzed') text = `🔔 ${name} propose …`;
|
||||
else if (ph === 'result') text = `✅ ${name} gagne un point !`;
|
||||
$('status').textContent = text;
|
||||
$('judge').hidden = ph !== 'buzzed';
|
||||
$('next').textContent = ph === 'result' ? 'Morceau suivant ▶' : 'Passer ce morceau ⏭';
|
||||
updateMask();
|
||||
}
|
||||
function updateMask() {
|
||||
$('mask').hidden = !($('hide').checked && state.phase !== 'result');
|
||||
}
|
||||
$('hide').addEventListener('change', updateMask);
|
||||
|
||||
/* ---------- Actions ---------- */
|
||||
function submitUrl() {
|
||||
const id = parseYoutubeId($('url').value);
|
||||
if (!id) { $('urlError').textContent = "URL YouTube invalide."; return; }
|
||||
$('urlError').textContent = '';
|
||||
nextVideo = id;
|
||||
$('url').value = '';
|
||||
socket.emit('master:start');
|
||||
}
|
||||
$('go').addEventListener('click', submitUrl);
|
||||
$('url').addEventListener('keydown', e => { if (e.key === 'Enter') submitUrl(); });
|
||||
$('btnTrue').addEventListener('click', () => socket.emit('master:judge', true));
|
||||
$('btnFalse').addEventListener('click', () => socket.emit('master:judge', false));
|
||||
$('next').addEventListener('click', () => socket.emit('master:next'));
|
||||
$('endBtn').addEventListener('click', () => socket.emit('master:end'));
|
||||
$('endFromLobby').addEventListener('click', () => socket.emit('master:end'));
|
||||
$('newGame').addEventListener('click', () => socket.emit('master:newgame'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user