commit 7b181a5f12c5b22f21260a293a81e37a23f69e0c Author: Frank Meeuwsen Date: Mon Nov 3 16:23:11 2025 +0100 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 diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..d0420a8 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Doel + +Browser-based interactief workshop spel waarbij deelnemers fysiek kiezen tussen twee stellingen die op een beamer worden getoond. Volledig offline werkend, configureerbaar via JSON. + +## Architectuur + +**Simpel & Offline First:** +- Pure HTML/CSS/JavaScript (geen frameworks, geen build proces) +- Werkt door `index.html` direct te openen in browser +- Alle configuratie via `config.json` (inclusief kleuren, timing, stellingen) + +**Kern Componenten:** + +1. **config.json** - Single source of truth voor alle instellingen + - Timer duur, font grootte, kleuren, stellingen, button tekst + - Geladen via fetch API bij app start + +2. **index.html** - Drie scherm states + - Statement screen (50/50 split met twee stellingen) + - Blank screen (met volgende knop) + - Finish screen (eindscherm) + +3. **style.css** - Timer animatie via CSS + - 4 balkjes (top/right/bottom/left) animeren synchroon + - CSS custom properties voor dynamische timer duur + - Flexbox voor 50/50 split + +4. **app.js** - State management en interactie + - Config laden en toepassen op DOM + - Screen state switching + - Web Audio API voor beep geluid + - Keyboard + click event handlers + +## Flow + +``` +Start → Laad config → Toon stelling (timer start) → +Beep + blanco scherm → Knop/spatiebalk → +Volgende stelling → ... → Eindscherm +``` + +## Configuratie Aanpak + +**Alles in config.json - niets hardcoded:** +- Kleuren: Hex codes voor links/rechts achtergrond +- Teksten: Button labels en finish tekst +- Timing: Timer duur in seconden +- Styling: Font grootte als CSS waarde +- Content: Array van stellingen (links/rechts paren) + +CSS custom properties worden runtime gezet vanuit config via JavaScript. + +## Interactie Pattern + +Dual input voor "volgende": +- Click op button (beamer presentatie met muis) +- Spatiebalk (sneller tijdens presentatie) + +Timer afloop triggert Web Audio API beep (800Hz sine wave, 300ms met fade out). + +## Wijzigingen Maken + +- Nieuwe features: Overweeg of het via config.json kan (behoud simpliciteit) +- Styling aanpassingen: Pas config opties aan waar mogelijk, geen hardcoded values +- Geluid wijzigen: Pas Web Audio parameters in `playBeep()` functie +- Extra scherm states: Volg bestaand pattern (hidden class toggles) + +## Testing + +Open `index.html` lokaal in browser. Test: +- Config wijzigingen worden direct toegepast +- Timer loopt correct af +- Beep speelt bij timer einde +- Spatiebalk werkt op blank screen +- Laatste stelling toont finish screen diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7707e51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Python +venv/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python + +# Flask +instance/ +.webassets-cache + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..037df6c --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Sessie IJsbreker + +Een browser-based interactief spel voor workshops waarbij deelnemers fysiek kiezen tussen twee stellingen. + +## Installatie + +**Eerste keer setup:** + +```bash +# Installeer Python dependencies +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +## Gebruik + +### 1. Start de Flask server + +```bash +# Activeer virtual environment (indien nog niet actief) +source venv/bin/activate + +# Start de server +python3 server.py +``` + +De server draait nu op `http://localhost:8000` + +### 2. Beheer Stellingen + +Open in je browser: `http://localhost:8000/editor.html` + +Hier kun je: +- âš™ī¸ Instellingen aanpassen (timer, kleuren, teksten) +- âœī¸ Stellingen toevoegen, bewerken, verwijderen +- 💾 Opslaan naar config.json +- âŒ¨ī¸ Keyboard shortcut: `Ctrl+S` of `Cmd+S` om op te slaan + +### 3. Spel Draaien (Presentatie) + +Open in je browser: `http://localhost:8000` + +Sluit aan op beamer/groot scherm en: +1. Het spel toont automatisch de eerste stelling +2. Deelnemers lopen naar links of rechts +3. Na de timer: beep + overlay met knop +4. Druk op knop of **spatiebalk** voor volgende stelling +5. Laatste stelling toont direct eindscherm + +## Configuratie + +Alle instellingen staan in `config.json`: + +```json +{ + "timer": 30, // Timer duur in seconden + "fontSize": "3rem", // Tekst grootte (CSS waarde) + "buttonText": "Volgende Stelling", + "finishText": "Sessie Afgelopen!", + "colors": { + "left": "#3B82F6", // Achtergrondkleur links (hex) + "right": "#EF4444" // Achtergrondkleur rechts (hex) + }, + "stellingen": [ + { + "links": "Koffie", + "rechts": "Thee" + } + ] +} +``` + +### Configuratie Opties + +- **timer**: Aantal seconden per stelling (bijv. `30`, `45`, `60`) +- **fontSize**: CSS font grootte (bijv. `"3rem"`, `"48px"`, `"4vw"`) +- **buttonText**: Tekst op de 'volgende' knop +- **finishText**: Tekst op het eindscherm +- **colors.left**: Hex kleurcode voor linker stelling achtergrond +- **colors.right**: Hex kleurcode voor rechter stelling achtergrond +- **stellingen**: Array van stelling paren + +## Interactie + +- **Klik op knop**: Ga naar volgende stelling +- **Spatiebalk**: Ga naar volgende stelling (sneller tijdens presentatie) + +## Features + +**Presentatie:** +- ✅ Visuele timer rondom scherm (loopt synchroon rond) +- ✅ Geluidssignaal bij einde timer +- ✅ Overlay met stellingen na timer (grayed out) +- ✅ Keyboard shortcut (spatiebalk) +- ✅ Direct eindscherm bij laatste stelling + +**Editor:** +- ✅ Web-based stellingen beheer +- ✅ Real-time preview van aantal stellingen +- ✅ Kleurenpicker voor achtergronden +- ✅ Drag-free toevoegen/verwijderen +- ✅ Keyboard shortcut (Ctrl/Cmd+S) +- ✅ Validatie van invoer + +**Algemeen:** +- ✅ Volledig offline werkend +- ✅ Configureerbaar via JSON Ên webinterface +- ✅ Geen authenticatie nodig (lokaal gebruik) + +## Technisch + +- **Frontend:** Pure HTML/CSS/JavaScript +- **Backend:** Python Flask voor config management +- **Dependencies:** Flask, flask-cors diff --git a/app.js b/app.js new file mode 100644 index 0000000..c5d2073 --- /dev/null +++ b/app.js @@ -0,0 +1,170 @@ +// 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(); diff --git a/config.json b/config.json new file mode 100644 index 0000000..027005c --- /dev/null +++ b/config.json @@ -0,0 +1,48 @@ +{ + "buttonText": "Volgende stelling", + "colors": { + "left": "#3b82f6", + "right": "#ef4444" + }, + "finishText": "Dat was het! Having fun yet?", + "fontSize": "3rem", + "stellingen": [ + { + "links": "Koffie", + "rechts": "Thee" + }, + { + "links": "Friet", + "rechts": "Patat" + }, + { + "links": "Liever email", + "rechts": "Liever telefoon" + }, + { + "links": "Sneltoetsen", + "rechts": "Muisgebruik" + }, + { + "links": "Overzichtelijke mappen", + "rechts": "Zoeken werkt sneller" + }, + { + "links": "Ik vind eenvoudig de juiste informatie terug", + "rechts": "Ik ben constant alles kwijt" + }, + { + "links": "Naamgeving van bestanden is een breinbreker", + "rechts": "Naamgeving is volslagen logisch" + }, + { + "links": "Ik maak eigen notities op ÊÊn plek", + "rechts": "Overal post-its en losse bestanden" + }, + { + "links": "â¤ī¸ â¤ī¸ â¤ī¸ Meetingverslagen template ", + "rechts": "â˜ ī¸â˜ ī¸â˜ ī¸ Wie bedenkt die templates?" + } + ], + "timer": 2 +} \ No newline at end of file diff --git a/editor.css b/editor.css new file mode 100644 index 0000000..b8835ee --- /dev/null +++ b/editor.css @@ -0,0 +1,273 @@ +/* Reset en basis */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: #f5f5f5; + color: #333; + line-height: 1.6; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +/* Header */ +header { + background: white; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +header h1 { + font-size: 1.8rem; + color: #2563EB; +} + +.header-actions { + display: flex; + gap: 1rem; +} + +/* Sections */ +section { + background: white; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 2rem; +} + +section h2 { + font-size: 1.4rem; + margin-bottom: 1.5rem; + color: #333; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.count-badge { + background: #E5E7EB; + color: #4B5563; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 600; +} + +/* Settings grid */ +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; +} + +.setting-item { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.setting-item label { + font-weight: 600; + color: #4B5563; + font-size: 0.9rem; +} + +.setting-item input[type="text"], +.setting-item input[type="number"] { + padding: 0.75rem; + border: 2px solid #E5E7EB; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.2s; +} + +.setting-item input[type="text"]:focus, +.setting-item input[type="number"]:focus { + outline: none; + border-color: #3B82F6; +} + +.setting-item input[type="color"] { + height: 50px; + border: 2px solid #E5E7EB; + border-radius: 8px; + cursor: pointer; +} + +/* Stellingen lijst */ +.statements-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.statement-row { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 1rem; + align-items: center; + padding: 1rem; + background: #F9FAFB; + border-radius: 8px; + border: 2px solid #E5E7EB; + transition: border-color 0.2s; +} + +.statement-row:hover { + border-color: #D1D5DB; +} + +.statement-input { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.statement-input label { + font-size: 0.75rem; + font-weight: 600; + color: #6B7280; + text-transform: uppercase; +} + +.statement-input input { + padding: 0.75rem; + border: 2px solid #E5E7EB; + border-radius: 6px; + font-size: 1rem; + transition: border-color 0.2s; +} + +.statement-input input:focus { + outline: none; + border-color: #3B82F6; +} + +/* Buttons */ +.btn { + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: 600; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; + display: inline-block; +} + +.btn-primary { + background: #3B82F6; + color: white; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); +} + +.btn-primary:hover { + background: #2563EB; + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4); +} + +.btn-secondary { + background: #6B7280; + color: white; +} + +.btn-secondary:hover { + background: #4B5563; +} + +.btn-add { + background: #10B981; + color: white; + width: 100%; + margin-top: 1rem; +} + +.btn-add:hover { + background: #059669; +} + +.btn-remove { + background: #EF4444; + color: white; + padding: 0.5rem 1rem; + font-size: 1.2rem; + min-width: 44px; +} + +.btn-remove:hover { + background: #DC2626; +} + +/* Actions */ +.actions-section { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +#saveConfig { + font-size: 1.2rem; + padding: 1rem 2rem; +} + +/* Status message */ +.status-message { + padding: 1rem; + border-radius: 8px; + font-weight: 600; + text-align: center; + min-height: 20px; + transition: all 0.3s; +} + +.status-message.success { + background: #D1FAE5; + color: #065F46; +} + +.status-message.error { + background: #FEE2E2; + color: #991B1B; +} + +/* Responsive */ +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + header { + flex-direction: column; + gap: 1rem; + text-align: center; + } + + .statement-row { + grid-template-columns: 1fr; + } + + .settings-grid { + grid-template-columns: 1fr; + } +} diff --git a/editor.html b/editor.html new file mode 100644 index 0000000..084dffa --- /dev/null +++ b/editor.html @@ -0,0 +1,80 @@ + + + + + + Stellingen Editor - IJsbreker + + + +
+
+

đŸŽ¯ Sessie IJsbreker - Stellingen Editor

+ +
+ +
+ +
+

âš™ī¸ Instellingen

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + + +
+
+
+ + +
+
+

📝 Stellingen

+ 0 stellingen +
+ +
+ +
+ + +
+ + +
+ +
+
+
+
+ + + + diff --git a/editor.js b/editor.js new file mode 100644 index 0000000..f2fbb10 --- /dev/null +++ b/editor.js @@ -0,0 +1,237 @@ +// 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 = ` +
+ + +
+
+ + +
+ + `; + + 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(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..e54296a --- /dev/null +++ b/index.html @@ -0,0 +1,43 @@ + + + + + + Sessie IJsbreker + + + + +
+
+
+
+
+
+ + +
+ +
+
+

+
+
+

+
+
+ + + + + + +
+ + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6acd0bf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.0 +flask-cors==4.0.0 diff --git a/server.py b/server.py new file mode 100644 index 0000000..a9d233e --- /dev/null +++ b/server.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +Flask server voor IJsbreker spel +Serveert statische bestanden en biedt API endpoints voor config management +""" + +from flask import Flask, send_from_directory, request, jsonify +from flask_cors import CORS +import json +import os + +app = Flask(__name__) +CORS(app) # Enable CORS voor alle routes + +CONFIG_FILE = 'config.json' + +# Serve static files +@app.route('/') +def serve_index(): + return send_from_directory('.', 'index.html') + +@app.route('/') +def serve_static(path): + if os.path.exists(path): + return send_from_directory('.', path) + return "File not found", 404 + +# API endpoint om config te laden +@app.route('/api/config', methods=['GET']) +def get_config(): + try: + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + return jsonify(config) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +# API endpoint om config op te slaan +@app.route('/api/save-config', methods=['POST']) +def save_config(): + try: + data = request.json + + # Validatie: check of stellingen array bestaat + if 'stellingen' not in data: + return jsonify({'error': 'Geen stellingen gevonden'}), 400 + + # Filter lege stellingen eruit + data['stellingen'] = [ + s for s in data['stellingen'] + if s.get('links', '').strip() or s.get('rechts', '').strip() + ] + + # Schrijf naar config.json met mooie formatting + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + return jsonify({'success': True, 'message': 'Config opgeslagen!'}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +if __name__ == '__main__': + print("đŸŽ¯ IJsbreker Server gestart!") + print("📝 Editor: http://localhost:8000/editor.html") + print("🎮 Spel: http://localhost:8000/") + print("\nDruk Ctrl+C om te stoppen") + app.run(debug=True, host='0.0.0.0', port=8000) diff --git a/style.css b/style.css new file mode 100644 index 0000000..ca3fc4b --- /dev/null +++ b/style.css @@ -0,0 +1,179 @@ +/* Reset en basis styling */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + overflow: hidden; + background: #1a1a1a; +} + +/* Container voor alle schermen */ +.container { + width: 100vw; + height: 100vh; + position: relative; +} + +/* Timer border animatie rondom scherm */ +.timer-border { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 1000; +} + +.timer-bar { + position: absolute; + background: white; +} + +/* Top balk */ +.timer-top { + top: 0; + left: 0; + height: 8px; + width: 0; +} + +/* Right balk */ +.timer-right { + top: 0; + right: 0; + width: 8px; + height: 0; +} + +/* Bottom balk */ +.timer-bottom { + bottom: 0; + right: 0; + height: 8px; + width: 0; +} + +/* Left balk */ +.timer-left { + bottom: 0; + left: 0; + width: 8px; + height: 0; +} + +/* Timer animatie - elke balk is 1/4 van totale tijd */ +.timer-active .timer-top { + animation: fillWidth calc(var(--timer-duration) / 4) linear forwards; +} + +.timer-active .timer-right { + animation: fillHeight calc(var(--timer-duration) / 4) linear forwards; + animation-delay: calc(var(--timer-duration) / 4); +} + +.timer-active .timer-bottom { + animation: fillWidth calc(var(--timer-duration) / 4) linear forwards; + animation-delay: calc(var(--timer-duration) / 2); +} + +.timer-active .timer-left { + animation: fillHeight calc(var(--timer-duration) / 4) linear forwards; + animation-delay: calc(var(--timer-duration) * 3 / 4); +} + +@keyframes fillWidth { + from { width: 0; } + to { width: 100vw; } +} + +@keyframes fillHeight { + from { height: 0; } + to { height: 100vh; } +} + +/* Stellingen scherm - 50/50 split */ +.statement-screen { + display: flex; + width: 100%; + height: 100%; +} + +.statement { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.statement p { + color: white; + font-weight: bold; + text-align: center; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + line-height: 1.4; +} + +/* Overlay met knop (over stellingen heen) */ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.7); + z-index: 500; +} + +.next-button { + padding: 1.5rem 3rem; + font-size: 1.5rem; + font-weight: bold; + color: white; + background: #3B82F6; + border: none; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +.next-button:hover { + background: #2563EB; + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(59, 130, 246, 0.6); +} + +.next-button:active { + transform: translateY(0); +} + +/* Eind scherm */ +.finish-screen { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background: #1a1a1a; +} + +.finish-screen h1 { + color: white; + font-weight: bold; + text-align: center; + padding: 2rem; +} + +/* Helper classes */ +.hidden { + display: none !important; +}