From 7b181a5f12c5b22f21260a293a81e37a23f69e0c Mon Sep 17 00:00:00 2001 From: Frank Meeuwsen Date: Mon, 3 Nov 2025 16:23:11 +0100 Subject: [PATCH] feat: Sessie IJsbreker - interactief workshop spel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/CLAUDE.md | 79 ++++++++++++++ .gitignore | 25 +++++ README.md | 115 +++++++++++++++++++ app.js | 170 +++++++++++++++++++++++++++++ config.json | 48 ++++++++ editor.css | 273 ++++++++++++++++++++++++++++++++++++++++++++++ editor.html | 80 ++++++++++++++ editor.js | 237 ++++++++++++++++++++++++++++++++++++++++ index.html | 43 ++++++++ requirements.txt | 2 + server.py | 67 ++++++++++++ style.css | 179 ++++++++++++++++++++++++++++++ 12 files changed, 1318 insertions(+) create mode 100644 .claude/CLAUDE.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.js create mode 100644 config.json create mode 100644 editor.css create mode 100644 editor.html create mode 100644 editor.js create mode 100644 index.html create mode 100644 requirements.txt create mode 100644 server.py create mode 100644 style.css 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; +}