Continuer ESC
Réponse Espace
20 s
La question s’affiche d’abord, la réponse se révèle ensuite (dans un Jeopardy classique, c'est l'inverse :)
Saisissez votre contenu au format suivant : nombre de points | question | réponse
Vous pouvez utiliser le collecteur de questions pour créer un Jeopardy collaboratif.
Le nombre de points, de catégories, de questions, d’équipes (et leur nom) ainsi que la durée du chronomètre sont modulables selon vos besoins. Les données sont stockées localement sur votre appareil.
<script>
(function(){
const $ = sel => document.querySelector(sel);
const grid = $("#grid");
const modal = $("#question-modal");
const timerBar = $("#timer-bar");
const timerText = $("#timer-text");
const bell = $("#bell");
let gameData = null;
let current = null;
let timerId = null;
let endTime = 0;
let duration = 20;
let timeUp = false;
function escapeHTML(str){
return String(str ?? "").replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
}
function formatText(str){
// Autorise le HTML dans les questions et réponses.
// Exemples : <b>texte</b>, <img src="image.jpg">, <audio controls src="son.mp3"></audio>
return String(str ?? "").replace(/\n/g,"<br>");
}
function showError(msg){
const el = $("#error");
el.textContent = msg;
el.style.display = "block";
}
function hideError(){ $("#error").style.display = "none"; }
function parseJeopardy(text){
const lines = text.replace(/\r/g,"").split("\n");
let title = "Jeopardy";
const categories = [];
let currentCat = null;
for(const raw of lines){
const line = raw.trim();
if(!line) continue;
const titleMatch = line.match(/^Titre\s+du\s+Jeopardy\s*:\s*(.+)$/i);
if(titleMatch){ title = titleMatch[1].trim(); continue; }
const catMatch = line.match(/^Cat[ée]gorie\s+\d+\s*:\s*(.+)$/i);
if(catMatch){
currentCat = {title:catMatch[1].trim(), items:[]};
categories.push(currentCat);
continue;
}
if(line.includes("|")){
if(!currentCat) throw new Error("Une ligne de question apparaît avant la première catégorie.");
const parts = line.split("|").map(x => x.trim());
if(parts.length < 3) throw new Error("Chaque ligne doit respecter : points | question | réponse");
const points = parseInt(parts[0],10);
if(Number.isNaN(points)) throw new Error("Les points doivent être des nombres : " + line);
currentCat.items.push({
points,
question: parts[1],
answer: parts.slice(2).join(" | ")
});
}
}
if(categories.length === 0) throw new Error("Ajoutez au moins une catégorie.");
if(categories.some(c => c.items.length === 0)) throw new Error("Chaque catégorie doit contenir au moins une question.");
return {title,categories};
}
function renderGame(data){
gameData = data;
document.body.classList.toggle("style-multicolor", $("#visualStyle").value === "multicolor");
$("#game-title").textContent = data.title;
grid.innerHTML = "";
const maxRows = Math.max(...data.categories.map(c => c.items.length));
const catRow = document.createElement("div");
catRow.className = "grid-row grid-row-cats";
catRow.setAttribute("role","row");
data.categories.forEach((cat,col)=>{
const cell = document.createElement("div");
cell.className = "grid-cell";
cell.dataset.col = col;
cell.innerHTML = `<div class="cell-inner cat-cell" role="columnheader">${formatText(cat.title)}</div>`;
catRow.appendChild(cell);
});
grid.appendChild(catRow);
for(let row=0; row<maxRows; row++){
const r = document.createElement("div");
r.className = "grid-row grid-row-questions";
r.setAttribute("role","row");
data.categories.forEach((cat,col)=>{
const item = cat.items[row];
const cell = document.createElement("div");
cell.className = "grid-cell question-cell";
cell.setAttribute("role","cell");
cell.tabIndex = item ? 0 : -1;
cell.dataset.row = row;
cell.dataset.col = col;
if(item){
cell.setAttribute("aria-label", item.points);
cell.innerHTML = `<div class="cell-inner point-cell">${item.points}</div>`;
cell.addEventListener("click",()=>openQuestion(row,col,cell));
cell.addEventListener("keydown",e=>{
if(e.key === "Enter" || e.key === " "){ e.preventDefault(); openQuestion(row,col,cell); }
});
}
r.appendChild(cell);
});
grid.appendChild(r);
}
renderTeams(parseInt($("#teamCount").value,10));
document.body.classList.add("playing");
}
function renderTeams(count){
const list = $("#teamList");
list.innerHTML = "";
for(let i=0;i<count;i++){
const team = document.createElement("div");
team.className = "team";
team.innerHTML = `
<div class="name" contenteditable="true">Équipe ${i+1}</div>
<div class="points" contenteditable="true">0</div>
<div class="score-buttons"><span class="minus" title="Retirer les points">−</span><span class="plus" title="Ajouter les points">+</span></div>`;
team.querySelector(".plus").addEventListener("click",()=>score(team,1));
team.querySelector(".minus").addEventListener("click",()=>score(team,-1));
list.appendChild(team);
}
}
function score(team, sign){
const pointsEl = team.querySelector(".points");
const base = parseInt(pointsEl.textContent,10) || 0;
const val = current ? current.item.points : 0;
pointsEl.textContent = base + sign * val;
team.classList.add("active");
setTimeout(()=>team.classList.remove("active"),350);
if(current && current.cell) markUsed(current.cell);
}
function markUsed(cell){
cell.classList.add("used");
cell.setAttribute("aria-label","Answered");
}
function openQuestion(row,col,cell){
if(cell.classList.contains("used")) return;
const item = gameData.categories[col].items[row];
current = {row,col,item,cell};
$("#question-title").textContent = `${gameData.categories[col].title} pour ${item.points}`;
$("#modalQuestion").innerHTML = formatText(item.question);
$("#modalAnswer").innerHTML = formatText(item.answer);
$("#answerBlock").classList.remove("revealed");
modal.style.display = "block";
startTimer();
}
function startTimer(){
clearInterval(timerId);
duration = Math.max(5, parseInt($("#duration").value,10) || 20);
timeUp = false;
endTime = Date.now() + duration * 1000;
timerBar.style.transition = "none";
timerBar.style.transform = "scaleX(1)";
timerText.textContent = duration + " s";
requestAnimationFrame(()=>{
timerBar.style.transition = `transform ${duration}s linear`;
timerBar.style.transform = "scaleX(0)";
});
timerId = setInterval(()=>{
const left = Math.max(0, Math.ceil((endTime - Date.now())/1000));
timerText.textContent = left + " s";
if(left <= 0 && !timeUp){
timeUp = true;
clearInterval(timerId);
try{
bell.currentTime = 0;
bell.play();
}catch(e){}
}
},150);
}
function reveal(){
$("#answerBlock").classList.add("revealed");
if(current && current.cell) markUsed(current.cell);
}
function closeModal(){
clearInterval(timerId);
modal.style.display = "none";
if(current && current.cell) current.cell.focus();
}
function getTeamState(){
return Array.from(document.querySelectorAll("#teamList .team")).map(team => ({
name: team.querySelector(".name")?.textContent || "",
points: team.querySelector(".points")?.textContent || "0"
}));
}
function applyTeamState(teams){
if(!Array.isArray(teams)) return;
const teamEls = Array.from(document.querySelectorAll("#teamList .team"));
teams.forEach((team, i) => {
const el = teamEls[i];
if(!el) return;
el.querySelector(".name").textContent = team.name ?? `Team ${i+1}`;
el.querySelector(".points").textContent = team.points ?? "0";
});
}
function getUsedCells(){
return Array.from(document.querySelectorAll(".question-cell.used")).map(cell => ({
row: cell.dataset.row,
col: cell.dataset.col
}));
}
function applyUsedCells(used){
if(!Array.isArray(used)) return;
used.forEach(pos => {
const cell = document.querySelector(`.question-cell[data-row="${pos.row}"][data-col="${pos.col}"]`);
if(cell) markUsed(cell);
});
}
function makeSafeFileName(name){
return (name || "jeopardy")
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-zA-Z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.toLowerCase() || "jeopardy";
}
function downloadGeneratedPage(){
hideError();
if(!gameData){
try{ renderGame(parseJeopardy($("#source").value)); }
catch(e){ showError(e.message); return; }
}
const state = {
source: $("#source").value,
visualStyle: $("#visualStyle").value || "classic",
teamCount: parseInt($("#teamCount").value, 10) || getTeamState().length || 2,
duration: parseInt($("#duration").value, 10) || 20,
teams: getTeamState(),
used: getUsedCells()
};
const clone = document.documentElement.cloneNode(true);
const modalClone = clone.querySelector("#question-modal");
if(modalClone) modalClone.style.display = "none";
const errorClone = clone.querySelector("#error");
if(errorClone){ errorClone.textContent = ""; errorClone.style.display = "none"; }
const exportScript = clone.ownerDocument.createElement("script");
exportScript.textContent = `\nwindow.__JEOPARDY_EXPORT_STATE__ = ${JSON.stringify(state)};\ndocument.addEventListener("DOMContentLoaded", function(){\n var st = window.__JEOPARDY_EXPORT_STATE__;\n if(!st) return;\n var source = document.getElementById("source");\n var visualStyle = document.getElementById("visualStyle");\n var teamCount = document.getElementById("teamCount");\n var duration = document.getElementById("duration");\n if(source) source.value = st.source || "";\n if(visualStyle) visualStyle.value = st.visualStyle || "classic";\n if(teamCount) teamCount.value = String(st.teamCount || 2);\n if(duration) duration.value = String(st.duration || 20);\n var build = document.getElementById("build");\n if(build) build.click();\n setTimeout(function(){\n var teams = Array.prototype.slice.call(document.querySelectorAll("#teamList .team"));\n (st.teams || []).forEach(function(team, i){\n var el = teams[i];\n if(!el) return;\n el.querySelector(".name").textContent = team.name || ("Team " + (i+1));\n el.querySelector(".points").textContent = team.points || "0";\n });\n (st.used || []).forEach(function(pos){\n var cell = document.querySelector('.question-cell[data-row="' + pos.row + '"][data-col="' + pos.col + '"]');\n if(cell){ cell.classList.add("used"); cell.setAttribute("aria-label", "Answered"); }\n });\n document.body.classList.add("playing");\n }, 0);\n});\n`;
clone.querySelector("body").appendChild(exportScript);
const html = "<!DOCTYPE html>\n" + clone.outerHTML;
const blob = new Blob([html], {type:"text/html;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = makeSafeFileName(gameData.title) + "-jeopardy.html";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
$("#visualStyle").addEventListener("change",()=>{
document.body.classList.toggle("style-multicolor", $("#visualStyle").value === "multicolor");
});
$("#build").addEventListener("click",()=>{
hideError();
try{ renderGame(parseJeopardy($("#source").value)); }
catch(e){ showError(e.message); }
});
$("#menuBtn").addEventListener("click",()=>document.body.classList.remove("playing"));
$("#downloadPage").addEventListener("click",downloadGeneratedPage);
$("#clearState").addEventListener("click",()=>{ localStorage.clear(); showError("Sauvegarde locale réinitialisée."); });
$("#revealBtn").addEventListener("click",reveal);
$("#closeBtn").addEventListener("click",closeModal);
document.addEventListener("keydown",e=>{
if(!document.body.classList.contains("playing")) return;
if(modal.style.display === "block"){
if(e.key === "Escape"){ e.preventDefault(); closeModal(); }
if(e.key === " "){ e.preventDefault(); reveal(); }
}
});
})();
</script>