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>
237 lines
7.2 KiB
JavaScript
237 lines
7.2 KiB
JavaScript
// Globale state
|
||
let config = null;
|
||
|
||
// DOM elementen
|
||
const timerInput = document.getElementById('timer');
|
||
const fontSizeInput = document.getElementById('fontSize');
|
||
const buttonTextInput = document.getElementById('buttonText');
|
||
const finishTextInput = document.getElementById('finishText');
|
||
const colorLeftInput = document.getElementById('colorLeft');
|
||
const colorLeftTextInput = document.getElementById('colorLeftText');
|
||
const colorRightInput = document.getElementById('colorRight');
|
||
const colorRightTextInput = document.getElementById('colorRightText');
|
||
const statementsList = document.getElementById('statementsList');
|
||
const addStatementBtn = document.getElementById('addStatement');
|
||
const saveConfigBtn = document.getElementById('saveConfig');
|
||
const statusMessage = document.getElementById('statusMessage');
|
||
const statementCount = document.getElementById('statementCount');
|
||
|
||
// Laad config bij start
|
||
async function loadConfig() {
|
||
try {
|
||
const response = await fetch('/api/config');
|
||
config = await response.json();
|
||
populateForm();
|
||
showStatus('Config geladen!', 'success');
|
||
} catch (error) {
|
||
console.error('Fout bij laden config:', error);
|
||
showStatus('Fout bij laden config', 'error');
|
||
}
|
||
}
|
||
|
||
// Vul formulier met config data
|
||
function populateForm() {
|
||
// Basis instellingen
|
||
timerInput.value = config.timer;
|
||
fontSizeInput.value = config.fontSize;
|
||
buttonTextInput.value = config.buttonText;
|
||
finishTextInput.value = config.finishText;
|
||
colorLeftInput.value = config.colors.left;
|
||
colorLeftTextInput.value = config.colors.left;
|
||
colorRightInput.value = config.colors.right;
|
||
colorRightTextInput.value = config.colors.right;
|
||
|
||
// Render stellingen
|
||
renderStatements();
|
||
}
|
||
|
||
// Render alle stellingen
|
||
function renderStatements() {
|
||
statementsList.innerHTML = '';
|
||
|
||
config.stellingen.forEach((stelling, index) => {
|
||
addStatementRow(stelling, index);
|
||
});
|
||
|
||
updateCount();
|
||
}
|
||
|
||
// Voeg een stelling rij toe aan de UI
|
||
function addStatementRow(stelling = { links: '', rechts: '' }, index) {
|
||
const row = document.createElement('div');
|
||
row.className = 'statement-row';
|
||
row.dataset.index = index;
|
||
|
||
row.innerHTML = `
|
||
<div class="statement-input">
|
||
<label>Stelling Links</label>
|
||
<input
|
||
type="text"
|
||
class="statement-left"
|
||
value="${stelling.links}"
|
||
placeholder="Bijv. Koffie"
|
||
data-index="${index}"
|
||
>
|
||
</div>
|
||
<div class="statement-input">
|
||
<label>Stelling Rechts</label>
|
||
<input
|
||
type="text"
|
||
class="statement-right"
|
||
value="${stelling.rechts}"
|
||
placeholder="Bijv. Thee"
|
||
data-index="${index}"
|
||
>
|
||
</div>
|
||
<button class="btn btn-remove" data-index="${index}">−</button>
|
||
`;
|
||
|
||
statementsList.appendChild(row);
|
||
|
||
// Event listeners voor inputs
|
||
const leftInput = row.querySelector('.statement-left');
|
||
const rightInput = row.querySelector('.statement-right');
|
||
const removeBtn = row.querySelector('.btn-remove');
|
||
|
||
leftInput.addEventListener('input', (e) => {
|
||
config.stellingen[index].links = e.target.value;
|
||
updateCount();
|
||
});
|
||
|
||
rightInput.addEventListener('input', (e) => {
|
||
config.stellingen[index].rechts = e.target.value;
|
||
updateCount();
|
||
});
|
||
|
||
removeBtn.addEventListener('click', () => {
|
||
if (confirm('Weet je zeker dat je deze stelling wilt verwijderen?')) {
|
||
removeStatement(index);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Verwijder stelling
|
||
function removeStatement(index) {
|
||
config.stellingen.splice(index, 1);
|
||
renderStatements();
|
||
showStatus('Stelling verwijderd', 'success');
|
||
}
|
||
|
||
// Voeg nieuwe lege stelling toe
|
||
function addNewStatement() {
|
||
config.stellingen.push({ links: '', rechts: '' });
|
||
renderStatements();
|
||
|
||
// Focus op eerste input van nieuwe rij
|
||
const newRow = statementsList.lastElementChild;
|
||
const firstInput = newRow.querySelector('.statement-left');
|
||
firstInput.focus();
|
||
|
||
showStatus('Nieuwe stelling toegevoegd', 'success');
|
||
}
|
||
|
||
// Update stelling count
|
||
function updateCount() {
|
||
const count = config.stellingen.filter(
|
||
s => s.links.trim() || s.rechts.trim()
|
||
).length;
|
||
statementCount.textContent = `${count} stelling${count !== 1 ? 'en' : ''}`;
|
||
}
|
||
|
||
// Sync kleur inputs (color picker <-> text)
|
||
function syncColorInputs() {
|
||
colorLeftInput.addEventListener('input', (e) => {
|
||
colorLeftTextInput.value = e.target.value;
|
||
});
|
||
|
||
colorLeftTextInput.addEventListener('input', (e) => {
|
||
if (e.target.value.match(/^#[0-9A-Fa-f]{6}$/)) {
|
||
colorLeftInput.value = e.target.value;
|
||
}
|
||
});
|
||
|
||
colorRightInput.addEventListener('input', (e) => {
|
||
colorRightTextInput.value = e.target.value;
|
||
});
|
||
|
||
colorRightTextInput.addEventListener('input', (e) => {
|
||
if (e.target.value.match(/^#[0-9A-Fa-f]{6}$/)) {
|
||
colorRightInput.value = e.target.value;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Sla config op naar server
|
||
async function saveConfig() {
|
||
try {
|
||
// Update config met form values
|
||
config.timer = parseInt(timerInput.value);
|
||
config.fontSize = fontSizeInput.value;
|
||
config.buttonText = buttonTextInput.value;
|
||
config.finishText = finishTextInput.value;
|
||
config.colors.left = colorLeftInput.value;
|
||
config.colors.right = colorRightInput.value;
|
||
|
||
// Filter lege stellingen eruit
|
||
config.stellingen = config.stellingen.filter(
|
||
s => s.links.trim() || s.rechts.trim()
|
||
);
|
||
|
||
// Validatie
|
||
if (config.stellingen.length === 0) {
|
||
showStatus('⚠️ Je moet minimaal 1 stelling toevoegen', 'error');
|
||
return;
|
||
}
|
||
|
||
// POST naar server
|
||
const response = await fetch('/api/save-config', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(config)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showStatus('✅ Config succesvol opgeslagen!', 'success');
|
||
// Reload om lege stellingen te verwijderen
|
||
setTimeout(() => loadConfig(), 500);
|
||
} else {
|
||
showStatus(`❌ Fout: ${result.error}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Fout bij opslaan:', error);
|
||
showStatus('❌ Fout bij opslaan config', 'error');
|
||
}
|
||
}
|
||
|
||
// Toon status bericht
|
||
function showStatus(message, type = 'success') {
|
||
statusMessage.textContent = message;
|
||
statusMessage.className = `status-message ${type}`;
|
||
|
||
// Verberg na 3 seconden
|
||
setTimeout(() => {
|
||
statusMessage.textContent = '';
|
||
statusMessage.className = 'status-message';
|
||
}, 3000);
|
||
}
|
||
|
||
// Event listeners
|
||
addStatementBtn.addEventListener('click', addNewStatement);
|
||
saveConfigBtn.addEventListener('click', saveConfig);
|
||
|
||
// Keyboard shortcuts
|
||
document.addEventListener('keydown', (e) => {
|
||
// Ctrl/Cmd + S = opslaan
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||
e.preventDefault();
|
||
saveConfig();
|
||
}
|
||
});
|
||
|
||
// Initialize
|
||
syncColorInputs();
|
||
loadConfig();
|