ijsbreker/app.js
Frank Meeuwsen 7b181a5f12 feat: Sessie IJsbreker - interactief workshop spel
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>
2025-11-03 16:23:11 +01:00

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();