Files
BlindTest/public/index.html
T
ADHogu 4659049cef
Deploy / deploy (push) Waiting to run
oupsi avec la pièce jointe c'est mieu
2026-09-28 11:47:44 +02:00

203 lines
7.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>