diff --git a/CLAUDE.md b/CLAUDE.md index 72e238c..076db79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ Een presentatie tool voor workshops waar deelnemers tussen twee stellingen moete ### Tech Stack - Frontend: Pure HTML/CSS/JavaScript - Backend: Python Flask + flask-cors -- Config driven via JSON +- Config driven via JSON (sets.json voor meerdere configuraties) - Geen externe dependencies voor presentatie modus ## Bestandsstructuur @@ -35,11 +35,13 @@ Een presentatie tool voor workshops waar deelnemers tussen twee stellingen moete ├── index.html # Presentatie modus (hoofdspel) ├── app.js # Game logic en timer animatie ├── style.css # Presentatie styling -├── config.json # Stellingen database (JSON) -├── editor.html # Web-based config editor -├── editor.js # Editor functionaliteit +├── config.json # Actieve configuratie (gebruikt door presentatie) +├── sets.json # Database met alle workshop sets +├── stellingkast.json # Bibliotheek met stellingen voor import +├── editor.html # Web-based config editor met sets beheer +├── editor.js # Editor functionaliteit en sets management ├── editor.css # Editor styling -├── server.py # Flask backend voor editor +├── server.py # Flask backend voor editor en sets API ├── requirements.txt # Python dependencies ├── .gitignore # Exclude venv, .DS_Store, etc └── README.md # Uitgebreide documentatie @@ -67,15 +69,17 @@ pip install -r requirements.txt python server.py # Open editor -open http://localhost:5000/editor.html +open http://localhost:8000/editor.html ``` **Editor features:** -- Toevoegen/verwijderen stellingen -- Timer aanpassen per stelling -- Kleuren aanpassen -- Preview kleuren -- Auto-save naar config.json +- **Sets beheer:** Meerdere workshop configuraties opslaan en laden +- **Sets dropdown:** Selecteer actieve set of maak nieuwe set aan +- Toevoegen/verwijderen stellingen via drag & drop +- Stellingkast met bibliotheek voor snelle import +- Timer aanpassen per set +- Kleuren aanpassen per set +- Auto-save naar sets.json en config.json ## Session History @@ -168,4 +172,59 @@ Tijdens gebruik bleek de knop onderaan moeilijk leesbaar door de overlay, en de **Status:** - Features volledig werkend en getest in browser -- Editor robuuster gemaakt tegen laadfouten \ No newline at end of file +- Editor robuuster gemaakt tegen laadfouten + +### 2025-12-16 - Workshop Sets Management +**Nieuwe features:** +- **Sets systeem:** Meerdere complete workshop configuraties kunnen opslaan en laden +- **Sets dropdown:** Bovenaan editor voor eenvoudig switchen tussen sets +- **"Opslaan als Nieuwe Set"** knop voor nieuwe configuraties aanmaken +- **Automatische migratie:** Huidige config.json wordt bij eerste gebruik omgezet naar default set +- **Backwards compatibility:** Systeem blijft werken zonder sets.json (legacy fallback) + +**Gewijzigde bestanden:** +- `server.py` - Nieuwe endpoints: GET/POST `/api/sets`, helper functies voor sets management +- `editor.html` - Sets sectie met dropdown en knoppen toegevoegd +- `editor.css` - Styling voor sets UI (blauwe border-left accent) +- `editor.js` - Complete sets management logica: laden, switchen, opslaan, nieuwe sets aanmaken +- `config.json` - Blijft actieve configuratie voor presentatie modus + +**Nieuwe bestanden:** +- `sets.json` - Database met alle workshop sets (wordt automatisch aangemaakt) + +**Hoe het werkt:** +1. Editor openen → Zie dropdown met alle sets +2. Set selecteren → Laadt stellingen en instellingen van die set +3. Bewerken en opslaan → Overschrijft huidige set +4. "Opslaan als Nieuwe Set" → Maakt nieuwe set aan met custom naam +5. Presentatie starten → Gebruikt laatst opgeslagen set uit config.json + +**Data structuur sets.json:** +```json +{ + "sets": [ + { + "id": "default", + "name": "Standaard Set", + "config": { /* complete config */ } + }, + { + "id": "set-1234567890", + "name": "PKM Workshop 2025", + "config": { /* complete config */ } + } + ], + "activeSetId": "default" +} +``` + +**Waarom:** +Facilitators kunnen nu meerdere workshop configuraties voorbereiden (bijv. verschillende doelgroepen, thema's) en eenvoudig tussen sets wisselen zonder telkens stellingen handmatig te moeten aanpassen. + +**Status:** +- Backend endpoints volledig werkend (GET/POST /api/sets) +- Frontend UI en logic compleet geïmplementeerd +- Automatische migratie van bestaande config.json getest +- Sets kunnen worden aangemaakt, geladen, gewijzigd en opgeslagen +- Backwards compatible met oude systeem +- Klaar voor productie gebruik \ No newline at end of file diff --git a/config.json b/config.json index fdcd7c0..f9c6c9d 100644 --- a/config.json +++ b/config.json @@ -1,60 +1,36 @@ { - "buttonText": "Volgende stelling", + "buttonText": "Volgende", "colors": { - "left": "#3b82f6", - "right": "#ef4444" + "left": "#10b981", + "right": "#f59e0b" }, - "finishText": "Dat was het! Having fun yet?", - "fontSize": "3rem", + "finishText": "Bedankt!", + "fontSize": "2.5rem", "stellingen": [ { - "links": "Koffie", - "rechts": "Thee" + "links": "Inbox Zero held", + "rechts": "1000+ ongelezen mails" }, { - "links": "Structurele planner", - "rechts": "Creatieve chaoot" - }, - { - "links": "Ik gebruik meer sneltoetsen", - "rechts": "Ik ben van team muisgebruik" - }, - { - "links": "We hebben duidelijke afspraken over naamgeving van bestanden", - "rechts": "Mijn naamgeving van bestanden is veel logischer" - }, - { - "links": "Mappenstructuur", - "rechts": "Zoekfunctie" - }, - { - "links": "Ik maak eigen notities op één plek", - "rechts": "Ik maak overal notities en zoek me suf" + "links": "Alles digitaal", + "rechts": "Mijn papieren notitieboek is heilig" }, { "links": "Samenwerken in één document", "rechts": "Concept_versie_3_def_final.docx mailen" }, { - "links": "Mijn TO-DO lijst is actueel", - "rechts": "Mijn TO-DO lijst is fictie" + "links": "Camera aan tijdens Teams", + "rechts": "Lekker onzichtbaar luisteren" }, { - "links": "Ik gebruik tags en labels", - "rechts": "Ik stop alles in mapjes" + "links": "Agenda blokken voor focus", + "rechts": "Mijn deur staat altijd open (digitaal)" }, { - "links": "Browser met 50+ open tabbladen", - "rechts": "Opgeruimde browser" - }, - { - "links": "Kennis zit in mijn hoofd", - "rechts": "Kennis staat in het systeem" - }, - { - "links": "Ik kom vandaag vooral halen", - "rechts": "Ik kom vandaag ook brengen" + "links": "Ik weet wat de AVG van me vraagt", + "rechts": "Privacy is voor de juristen" } ], - "timer": 10 + "timer": 3 } \ No newline at end of file diff --git a/editor.css b/editor.css index 40f787c..3a0328b 100644 --- a/editor.css +++ b/editor.css @@ -77,6 +77,66 @@ section h2 { font-weight: 600; } +/* Sets Section Styling */ +.sets-section { + border-left: 4px solid #3b82f6; +} + +.sets-controls { + display: flex; + gap: 2rem; + align-items: flex-start; + flex-wrap: wrap; +} + +.set-selector-group { + flex: 1; + min-width: 300px; +} + +.set-selector-group label { + display: block; + font-weight: 500; + margin-bottom: 0.5rem; + color: #374151; +} + +.set-select { + width: 100%; + padding: 0.75rem; + border: 2px solid #e5e7eb; + border-radius: 6px; + font-size: 1rem; + background: white; + cursor: pointer; + transition: border-color 0.2s; +} + +.set-select:hover { + border-color: #3b82f6; +} + +.set-select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.set-actions { + display: flex; + gap: 0.75rem; + align-items: flex-end; +} + +.btn-danger { + background: #ef4444; + color: white; +} + +.btn-danger:hover { + background: #dc2626; +} + /* Settings grid */ .settings-grid { display: grid; diff --git a/editor.html b/editor.html index 36a2d18..75b6515 100644 --- a/editor.html +++ b/editor.html @@ -16,6 +16,23 @@
+ +
+

📂 Workshop Sets

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

⚙️ Instellingen

diff --git a/editor.js b/editor.js index e17e551..8f12350 100644 --- a/editor.js +++ b/editor.js @@ -2,6 +2,8 @@ let config = null; let stellingkastItems = []; let dragSrcEl = null; +let allSets = null; +let currentSetId = null; // DOM elementen const timerInput = document.getElementById('timer'); @@ -25,26 +27,229 @@ const stellingkastPanel = document.getElementById('stellingkastPanel'); const stellingkastList = document.getElementById('stellingkastList'); const searchStellingkastInput = document.getElementById('searchStellingkast'); +// Sets elementen +const setSelector = document.getElementById('setSelector'); +const saveAsNewSetBtn = document.getElementById('saveAsNewSet'); +const deleteSetBtn = document.getElementById('deleteSet'); + // Initialize syncColorInputs(); -// Start beide laad-acties onafhankelijk van elkaar -loadConfig(); +// Start alle laad-acties onafhankelijk van elkaar +loadSets(); loadStellingkast(); -// Laad config bij start +// --- SETS MANAGEMENT --- + +// Laad alle sets bij start +async function loadSets() { + try { + console.log('Fetching sets...'); + const response = await fetch('/api/sets?t=' + Date.now()); + if (!response.ok) throw new Error('Kon sets niet laden (' + response.status + ')'); + + allSets = await response.json(); + console.log('Sets loaded:', allSets); + + // Ensure structure + if (!allSets.sets) allSets.sets = []; + + populateSetSelector(); + + // Laad actieve set of eerste set + if (allSets.activeSetId) { + currentSetId = allSets.activeSetId; + } else if (allSets.sets.length > 0) { + currentSetId = allSets.sets[0].id; + } + + if (currentSetId) { + loadSetById(currentSetId); + } + + showStatus('Sets geladen!', 'success'); + } catch (error) { + console.error('Fout bij laden sets:', error); + showStatus('Fout bij laden sets: ' + error.message, 'error'); + // Fallback naar oude config.json manier + loadConfig(); + } +} + +// Vul set selector dropdown +function populateSetSelector() { + if (!setSelector || !allSets) return; + + setSelector.innerHTML = ''; + + allSets.sets.forEach(set => { + const option = document.createElement('option'); + option.value = set.id; + option.textContent = set.name; + if (set.id === currentSetId) { + option.selected = true; + } + setSelector.appendChild(option); + }); +} + +// Laad een specifieke set in de editor +function loadSetById(setId) { + const set = allSets.sets.find(s => s.id === setId); + if (!set || !set.config) { + console.error('Set niet gevonden:', setId); + return; + } + + config = set.config; + currentSetId = setId; + + // Ensure minimal config structure + if (!config.stellingen) config.stellingen = []; + if (!config.colors) config.colors = { left: '#3B82F6', right: '#EF4444' }; + + populateForm(); + showStatus(`Set geladen: ${set.name}`, 'success'); +} + +// Event handler voor set selectie +function onSetSelected(event) { + const setId = event.target.value; + + if (!setId) { + // "Nieuwe set" optie gekozen + currentSetId = null; + config = { + timer: 30, + fontSize: '3rem', + buttonText: 'Volgende Stelling', + finishText: 'Einde!', + colors: { left: '#3B82F6', right: '#EF4444' }, + stellingen: [] + }; + populateForm(); + showStatus('Nieuwe set - vul gegevens in en gebruik "Opslaan als Nieuwe Set"', 'info'); + } else { + loadSetById(setId); + } +} + +// Sla huidige config op naar actieve set +async function saveCurrentSet() { + if (!currentSetId) { + showStatus('Geen actieve set - gebruik "Opslaan als Nieuwe Set"', 'error'); + return false; + } + + try { + // Update set in allSets + const setIndex = allSets.sets.findIndex(s => s.id === currentSetId); + if (setIndex === -1) { + showStatus('Set niet gevonden', 'error'); + return false; + } + + // Update config van deze set + allSets.sets[setIndex].config = { ...config }; + allSets.activeSetId = currentSetId; + + // Sla op naar server + const response = await fetch('/api/sets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(allSets) + }); + + const result = await response.json(); + + if (result.success) { + showStatus('✅ Set opgeslagen!', 'success'); + return true; + } else { + showStatus(`❌ Fout: ${result.error}`, 'error'); + return false; + } + } catch (error) { + console.error('Fout bij opslaan set:', error); + showStatus('❌ Fout bij opslaan set', 'error'); + return false; + } +} + +// Sla op als nieuwe set (met naam prompt) +async function saveAsNewSet() { + const setName = prompt('Geef een naam voor deze set:', 'Nieuwe Workshop Set'); + + if (!setName || setName.trim() === '') { + showStatus('Opslaan geannuleerd', 'info'); + return; + } + + try { + // Genereer unieke ID + const newId = 'set-' + Date.now(); + + // Maak nieuwe set + const newSet = { + id: newId, + name: setName.trim(), + config: { ...config } + }; + + // Voeg toe aan sets + allSets.sets.push(newSet); + allSets.activeSetId = newId; + currentSetId = newId; + + // Sla op naar server + const response = await fetch('/api/sets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(allSets) + }); + + const result = await response.json(); + + if (result.success) { + populateSetSelector(); + setSelector.value = newId; + showStatus(`✅ Nieuwe set "${setName}" aangemaakt!`, 'success'); + } else { + showStatus(`❌ Fout: ${result.error}`, 'error'); + } + } catch (error) { + console.error('Fout bij aanmaken set:', error); + showStatus('❌ Fout bij aanmaken set', 'error'); + } +} + +// Toon status bericht +function showStatus(message, type = 'info') { + if (!statusMessage) return; + + statusMessage.textContent = message; + statusMessage.className = 'status-message ' + type; + statusMessage.style.display = 'block'; + + // Auto-hide na 5 seconden + setTimeout(() => { + statusMessage.style.display = 'none'; + }, 5000); +} + +// Fallback: Laad config op oude manier (voor backwards compatibility) async function loadConfig() { try { console.log('Fetching config...'); const response = await fetch('/api/config?t=' + Date.now()); if (!response.ok) throw new Error('Kon config niet laden (' + response.status + ')'); - + config = await response.json(); console.log('Config loaded:', config); - + // Ensure minimal config structure if (!config.stellingen) config.stellingen = []; if (!config.colors) config.colors = { left: '#3B82F6', right: '#EF4444' }; - + populateForm(); showStatus('Config geladen!', 'success'); } catch (error) { @@ -276,7 +481,16 @@ async function saveConfig() { return; } - // POST naar server + // Als we sets gebruiken, sla via sets op + if (allSets && currentSetId) { + const saved = await saveCurrentSet(); + if (saved) { + setTimeout(() => loadSetById(currentSetId), 500); + } + return; + } + + // Fallback naar oude methode (legacy) const response = await fetch('/api/save-config', { method: 'POST', headers: { @@ -289,7 +503,6 @@ async function saveConfig() { if (result.success) { showStatus('✅ Config succesvol opgeslagen!', 'success'); - // Reload om lege stellingen te verwijderen setTimeout(() => loadConfig(), 500); } else { showStatus(`❌ Fout: ${result.error}`, 'error'); @@ -388,6 +601,10 @@ function importStelling(item) { if (addStatementBtn) addStatementBtn.addEventListener('click', addNewStatement); if (saveConfigBtn) saveConfigBtn.addEventListener('click', saveConfig); +// Sets events +if (setSelector) setSelector.addEventListener('change', onSetSelected); +if (saveAsNewSetBtn) saveAsNewSetBtn.addEventListener('click', saveAsNewSet); + // Stellingkast events if (openStellingkastBtn) { openStellingkastBtn.addEventListener('click', () => { diff --git a/server.py b/server.py index a9d233e..8d3c057 100644 --- a/server.py +++ b/server.py @@ -13,6 +13,7 @@ app = Flask(__name__) CORS(app) # Enable CORS voor alle routes CONFIG_FILE = 'config.json' +SETS_FILE = 'sets.json' # Serve static files @app.route('/') @@ -59,6 +60,76 @@ def save_config(): except Exception as e: return jsonify({'error': str(e)}), 500 +# API endpoint om alle sets te laden +@app.route('/api/sets', methods=['GET']) +def get_sets(): + try: + # Als sets.json niet bestaat, maak initieel bestand aan + if not os.path.exists(SETS_FILE): + initialize_sets_file() + + with open(SETS_FILE, 'r', encoding='utf-8') as f: + sets_data = json.load(f) + return jsonify(sets_data) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +# API endpoint om sets op te slaan +@app.route('/api/sets', methods=['POST']) +def save_sets(): + try: + data = request.json + + # Validatie: check of sets array bestaat + if 'sets' not in data: + return jsonify({'error': 'Geen sets gevonden'}), 400 + + # Schrijf naar sets.json met mooie formatting + with open(SETS_FILE, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + # Update config.json met actieve set als die gezet is + if 'activeSetId' in data and data['activeSetId']: + active_set = next((s for s in data['sets'] if s['id'] == data['activeSetId']), None) + if active_set and 'config' in active_set: + update_active_config(active_set['config']) + + return jsonify({'success': True, 'message': 'Sets opgeslagen!'}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +# Helper: Update config.json met set configuratie +def update_active_config(set_config): + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(set_config, f, indent=2, ensure_ascii=False) + +# Helper: Initialiseer sets.json met huidige config als default set +def initialize_sets_file(): + try: + # Lees huidige config.json + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + current_config = json.load(f) + + # Maak initiële sets structuur + initial_sets = { + 'sets': [ + { + 'id': 'default', + 'name': 'Standaard Set', + 'config': current_config + } + ], + 'activeSetId': 'default' + } + + # Schrijf naar sets.json + with open(SETS_FILE, 'w', encoding='utf-8') as f: + json.dump(initial_sets, f, indent=2, ensure_ascii=False) + + print("✅ sets.json aangemaakt met default set") + except Exception as e: + print(f"⚠️ Fout bij initialiseren sets.json: {e}") + if __name__ == '__main__': print("🎯 IJsbreker Server gestart!") print("📝 Editor: http://localhost:8000/editor.html") diff --git a/sets.json b/sets.json new file mode 100644 index 0000000..853747b --- /dev/null +++ b/sets.json @@ -0,0 +1,149 @@ +{ + "activeSetId": "set-1765866890733", + "sets": [ + { + "config": { + "buttonText": "Volgende stelling", + "colors": { + "left": "#3b82f6", + "right": "#ef4444" + }, + "finishText": "Dat was het! Having fun yet?", + "fontSize": "3rem", + "stellingen": [ + { + "links": "Koffie", + "rechts": "Thee" + }, + { + "links": "Structurele planner", + "rechts": "Creatieve chaoot" + }, + { + "links": "Ik gebruik meer sneltoetsen", + "rechts": "Ik ben van team muisgebruik" + }, + { + "links": "We hebben duidelijke afspraken over naamgeving van bestanden", + "rechts": "Mijn naamgeving van bestanden is veel logischer" + }, + { + "links": "Mappenstructuur", + "rechts": "Zoekfunctie" + }, + { + "links": "Ik maak eigen notities op één plek", + "rechts": "Ik maak overal notities en zoek me suf" + }, + { + "links": "Samenwerken in één document", + "rechts": "Concept_versie_3_def_final.docx mailen" + }, + { + "links": "Mijn TO-DO lijst is actueel", + "rechts": "Mijn TO-DO lijst is fictie" + }, + { + "links": "Ik gebruik tags en labels", + "rechts": "Ik stop alles in mapjes" + }, + { + "links": "Browser met 50+ open tabbladen", + "rechts": "Opgeruimde browser" + }, + { + "links": "Kennis zit in mijn hoofd", + "rechts": "Kennis staat in het systeem" + }, + { + "links": "Ik kom vandaag vooral halen", + "rechts": "Ik kom vandaag ook brengen" + } + ], + "timer": 10 + }, + "id": "default", + "name": "Standaard Set" + }, + { + "config": { + "buttonText": "Volgende", + "colors": { + "left": "#10b981", + "right": "#f59e0b" + }, + "finishText": "Bedankt!", + "fontSize": "2.5rem", + "stellingen": [ + { + "links": "Inbox Zero held", + "rechts": "1000+ ongelezen mails" + }, + { + "links": "Alles digitaal", + "rechts": "Mijn papieren notitieboek is heilig" + }, + { + "links": "Samenwerken in één document", + "rechts": "Concept_versie_3_def_final.docx mailen" + }, + { + "links": "Camera aan tijdens Teams", + "rechts": "Lekker onzichtbaar luisteren" + }, + { + "links": "Agenda blokken voor focus", + "rechts": "Mijn deur staat altijd open (digitaal)" + }, + { + "links": "Ik weet wat de AVG van me vraagt", + "rechts": "Privacy is voor de juristen" + } + ], + "timer": 3 + }, + "id": "pkm-workshop", + "name": "PKM Workshop 2025" + }, + { + "id": "set-1765866890733", + "name": "Kerstboom", + "config": { + "buttonText": "Volgende", + "colors": { + "left": "#10b981", + "right": "#f59e0b" + }, + "finishText": "Bedankt!", + "fontSize": "2.5rem", + "stellingen": [ + { + "links": "Inbox Zero held", + "rechts": "1000+ ongelezen mails" + }, + { + "links": "Alles digitaal", + "rechts": "Mijn papieren notitieboek is heilig" + }, + { + "links": "Samenwerken in één document", + "rechts": "Concept_versie_3_def_final.docx mailen" + }, + { + "links": "Camera aan tijdens Teams", + "rechts": "Lekker onzichtbaar luisteren" + }, + { + "links": "Agenda blokken voor focus", + "rechts": "Mijn deur staat altijd open (digitaal)" + }, + { + "links": "Ik weet wat de AVG van me vraagt", + "rechts": "Privacy is voor de juristen" + } + ], + "timer": 3 + } + } + ] +} \ No newline at end of file