Browser-based interactief spel voor workshops waarbij deelnemers fysiek kiezen tussen twee stellingen die op een beamer worden getoond. Features: - Presentatie modus met visuele timer rondom scherm - Timer animatie loopt synchroon rond in opgegeven tijd - Geluidssignaal bij einde timer - Overlay met stellingen na timer (grayed out) - Keyboard shortcuts (spatiebalk voor volgende) - Direct eindscherm bij laatste stelling - Web-based stellingen editor - Flask backend voor config management - Real-time CRUD operaties op stellingen - Kleurenpicker voor achtergronden - Validatie en filtering van lege stellingen - Volledig offline werkend Tech stack: - Frontend: Pure HTML/CSS/JavaScript - Backend: Python Flask + flask-cors - Config driven via JSON 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
170 lines
4.9 KiB
JavaScript
170 lines
4.9 KiB
JavaScript
// Globale state
|
|
let config = null;
|
|
let currentIndex = 0;
|
|
let audioContext = null;
|
|
|
|
// DOM elementen
|
|
const timerBorder = document.querySelector('.timer-border');
|
|
const statementScreen = document.getElementById('statementScreen');
|
|
const overlay = document.getElementById('overlay');
|
|
const finishScreen = document.getElementById('finishScreen');
|
|
const leftStatement = document.getElementById('leftStatement');
|
|
const rightStatement = document.getElementById('rightStatement');
|
|
const leftText = document.getElementById('leftText');
|
|
const rightText = document.getElementById('rightText');
|
|
const nextButton = document.getElementById('nextButton');
|
|
const finishText = document.getElementById('finishText');
|
|
|
|
// Laad configuratie bij start
|
|
async function loadConfig() {
|
|
try {
|
|
const response = await fetch('config.json');
|
|
config = await response.json();
|
|
initializeApp();
|
|
} catch (error) {
|
|
console.error('Fout bij laden config:', error);
|
|
alert('Kan config.json niet laden. Zorg dat alle bestanden in dezelfde map staan.');
|
|
}
|
|
}
|
|
|
|
// Initialiseer app met config
|
|
function initializeApp() {
|
|
// Stel CSS variabelen in
|
|
document.documentElement.style.setProperty('--timer-duration', `${config.timer}s`);
|
|
|
|
// Pas font grootte toe
|
|
document.querySelectorAll('.statement p').forEach(el => {
|
|
el.style.fontSize = config.fontSize;
|
|
});
|
|
finishText.style.fontSize = config.fontSize;
|
|
|
|
// Pas kleuren toe
|
|
leftStatement.style.backgroundColor = config.colors.left;
|
|
rightStatement.style.backgroundColor = config.colors.right;
|
|
|
|
// Zet knop tekst
|
|
nextButton.textContent = config.buttonText;
|
|
finishText.textContent = config.finishText;
|
|
|
|
// Start met eerste stelling
|
|
showStatement(0);
|
|
}
|
|
|
|
// Toon stelling op index
|
|
function showStatement(index) {
|
|
currentIndex = index;
|
|
|
|
// Check of we klaar zijn
|
|
if (index >= config.stellingen.length) {
|
|
showFinish();
|
|
return;
|
|
}
|
|
|
|
const stelling = config.stellingen[index];
|
|
|
|
// Update teksten
|
|
leftText.textContent = stelling.links;
|
|
rightText.textContent = stelling.rechts;
|
|
|
|
// Toon stellingen scherm, verberg overlay en finish
|
|
statementScreen.classList.remove('hidden');
|
|
overlay.classList.add('hidden');
|
|
finishScreen.classList.add('hidden');
|
|
|
|
// Start timer
|
|
startTimer();
|
|
}
|
|
|
|
// Start timer animatie
|
|
function startTimer() {
|
|
// Reset timer
|
|
timerBorder.classList.remove('timer-active');
|
|
void timerBorder.offsetWidth; // Force reflow
|
|
|
|
// Start timer
|
|
timerBorder.classList.add('timer-active');
|
|
|
|
// Wacht tot timer klaar is
|
|
setTimeout(() => {
|
|
playBeep();
|
|
// Bij laatste stelling: toon direct finish scherm
|
|
if (currentIndex === config.stellingen.length - 1) {
|
|
showFinish();
|
|
} else {
|
|
showOverlay();
|
|
}
|
|
}, config.timer * 1000);
|
|
}
|
|
|
|
// Toon overlay met knop (stellingen blijven zichtbaar)
|
|
function showOverlay() {
|
|
overlay.classList.remove('hidden');
|
|
timerBorder.classList.remove('timer-active');
|
|
}
|
|
|
|
// Toon eind scherm
|
|
function showFinish() {
|
|
statementScreen.classList.add('hidden');
|
|
overlay.classList.add('hidden');
|
|
finishScreen.classList.remove('hidden');
|
|
timerBorder.classList.remove('timer-active');
|
|
}
|
|
|
|
// Initialiseer audio context (herbruikbaar)
|
|
function initAudioContext() {
|
|
if (!audioContext) {
|
|
try {
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
} catch (error) {
|
|
console.error('Fout bij initialiseren audio context:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Speel beep geluid via Web Audio API
|
|
function playBeep() {
|
|
try {
|
|
// Zorg dat audioContext bestaat
|
|
initAudioContext();
|
|
if (!audioContext) return;
|
|
|
|
const oscillator = audioContext.createOscillator();
|
|
const gainNode = audioContext.createGain();
|
|
|
|
// Configureer oscillator (beep geluid)
|
|
oscillator.type = 'sine';
|
|
oscillator.frequency.setValueAtTime(800, audioContext.currentTime); // 800 Hz
|
|
|
|
// Configureer volume (fade out)
|
|
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
|
|
|
// Verbind nodes
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(audioContext.destination);
|
|
|
|
// Speel beep (300ms)
|
|
oscillator.start(audioContext.currentTime);
|
|
oscillator.stop(audioContext.currentTime + 0.3);
|
|
} catch (error) {
|
|
console.error('Fout bij afspelen geluid:', error);
|
|
}
|
|
}
|
|
|
|
// Event listeners
|
|
nextButton.addEventListener('click', () => {
|
|
showStatement(currentIndex + 1);
|
|
});
|
|
|
|
// Spatiebalk voor volgende stelling
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.code === 'Space') {
|
|
e.preventDefault();
|
|
if (!overlay.classList.contains('hidden')) {
|
|
showStatement(currentIndex + 1);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Start de app
|
|
loadConfig();
|