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>
This commit is contained in:
commit
7b181a5f12
12 changed files with 1318 additions and 0 deletions
79
.claude/CLAUDE.md
Normal file
79
.claude/CLAUDE.md
Normal file
|
|
@ -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
|
||||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
|
@ -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
|
||||||
115
README.md
Normal file
115
README.md
Normal file
|
|
@ -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
|
||||||
170
app.js
Normal file
170
app.js
Normal file
|
|
@ -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();
|
||||||
48
config.json
Normal file
48
config.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
273
editor.css
Normal file
273
editor.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
editor.html
Normal file
80
editor.html
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Stellingen Editor - IJsbreker</title>
|
||||||
|
<link rel="stylesheet" href="editor.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>🎯 Sessie IJsbreker - Stellingen Editor</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a href="/" class="btn btn-secondary">← Terug naar Spel</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Basis instellingen -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>⚙️ Instellingen</h2>
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="timer">Timer (seconden):</label>
|
||||||
|
<input type="number" id="timer" min="5" max="300" value="30">
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="fontSize">Font grootte:</label>
|
||||||
|
<input type="text" id="fontSize" value="3rem" placeholder="3rem">
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="buttonText">Knop tekst:</label>
|
||||||
|
<input type="text" id="buttonText" value="Volgende Stelling">
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="finishText">Eind tekst:</label>
|
||||||
|
<input type="text" id="finishText" value="Sessie Afgelopen!">
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="colorLeft">Kleur links:</label>
|
||||||
|
<input type="color" id="colorLeft" value="#3B82F6">
|
||||||
|
<input type="text" id="colorLeftText" value="#3B82F6">
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="colorRight">Kleur rechts:</label>
|
||||||
|
<input type="color" id="colorRight" value="#EF4444">
|
||||||
|
<input type="text" id="colorRightText" value="#EF4444">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Stellingen -->
|
||||||
|
<section class="statements-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>📝 Stellingen</h2>
|
||||||
|
<span id="statementCount" class="count-badge">0 stellingen</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="statementsList" class="statements-list">
|
||||||
|
<!-- Stellingen worden hier dynamisch toegevoegd -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="addStatement" class="btn btn-add">
|
||||||
|
+ Nieuwe Stelling
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Opslaan -->
|
||||||
|
<section class="actions-section">
|
||||||
|
<button id="saveConfig" class="btn btn-primary">
|
||||||
|
💾 Opslaan naar Config
|
||||||
|
</button>
|
||||||
|
<div id="statusMessage" class="status-message"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="editor.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
237
editor.js
Normal file
237
editor.js
Normal file
|
|
@ -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 = `
|
||||||
|
<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();
|
||||||
43
index.html
Normal file
43
index.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sessie IJsbreker</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Timer balk rondom scherm -->
|
||||||
|
<div class="timer-border">
|
||||||
|
<div class="timer-bar timer-top"></div>
|
||||||
|
<div class="timer-bar timer-right"></div>
|
||||||
|
<div class="timer-bar timer-bottom"></div>
|
||||||
|
<div class="timer-bar timer-left"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hoofdcontainer -->
|
||||||
|
<div id="app" class="container">
|
||||||
|
<!-- Stellingen scherm -->
|
||||||
|
<div id="statementScreen" class="statement-screen">
|
||||||
|
<div id="leftStatement" class="statement left-statement">
|
||||||
|
<p id="leftText"></p>
|
||||||
|
</div>
|
||||||
|
<div id="rightStatement" class="statement right-statement">
|
||||||
|
<p id="rightText"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay met knop (over stellingen heen) -->
|
||||||
|
<div id="overlay" class="overlay hidden">
|
||||||
|
<button id="nextButton" class="next-button"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Eind scherm -->
|
||||||
|
<div id="finishScreen" class="finish-screen hidden">
|
||||||
|
<h1 id="finishText"></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Flask==3.0.0
|
||||||
|
flask-cors==4.0.0
|
||||||
67
server.py
Normal file
67
server.py
Normal file
|
|
@ -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('/<path:path>')
|
||||||
|
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)
|
||||||
179
style.css
Normal file
179
style.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue