ijsbreker/editor.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

237 lines
7.2 KiB
JavaScript
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.

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