ijsbreker/editor.js
Frank Meeuwsen 0775ee6161 feat: workshop sets management systeem toegevoegd
Nieuw sets systeem voor het beheren van meerdere workshop configuraties.

Nieuwe functionaliteit:
- Sets dropdown bovenaan editor voor eenvoudig switchen
- "Opslaan als Nieuwe Set" knop voor nieuwe configuraties
- Meerdere complete workshop sets opslaan en laden
- Elke set bevat eigen stellingen, kleuren, timer en teksten
- Automatische migratie van huidige config naar default set

Backend wijzigingen:
- GET/POST /api/sets endpoints toegevoegd
- Helper functies voor sets management en config updates
- Automatische initialisatie van sets.json bij eerste gebruik

Frontend wijzigingen:
- Sets sectie met dropdown en acties knoppen (editor.html)
- Styling met blauwe accent border (editor.css)
- Complete sets management logic (editor.js)
- Event handlers voor set selectie en opslaan

Data structuur:
- sets.json: database met alle workshop sets
- config.json: blijft actieve configuratie voor presentatie

Backwards compatible:
- Systeem werkt zonder sets.json (legacy fallback)
- Presentatie modus ongewijzigd (gebruikt config.json)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 07:36:55 +01:00

655 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Globale state
let config = null;
let stellingkastItems = [];
let dragSrcEl = null;
let allSets = null;
let currentSetId = 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');
// Stellingkast elementen
const openStellingkastBtn = document.getElementById('openStellingkast');
const closeStellingkastBtn = document.getElementById('closeStellingkast');
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 alle laad-acties onafhankelijk van elkaar
loadSets();
loadStellingkast();
// --- 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 = '<option value="">-- Nieuwe set --</option>';
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) {
console.error('Fout bij laden config:', error);
showStatus('Fout bij laden config: ' + error.message, 'error');
}
}
// Vul formulier met config data
function populateForm() {
if (!config) return;
// Basis instellingen (met fallback values)
timerInput.value = config.timer || 30;
fontSizeInput.value = config.fontSize || '3rem';
buttonTextInput.value = config.buttonText || 'Volgende Stelling';
finishTextInput.value = config.finishText || 'Einde!';
// Safety check voor colors object
const leftColor = (config.colors && config.colors.left) ? config.colors.left : '#3B82F6';
const rightColor = (config.colors && config.colors.right) ? config.colors.right : '#EF4444';
colorLeftInput.value = leftColor;
colorLeftTextInput.value = leftColor;
colorRightInput.value = rightColor;
colorRightTextInput.value = rightColor;
// Render stellingen
renderStatements();
}
// Render alle stellingen
function renderStatements() {
statementsList.innerHTML = '';
if (config.stellingen) {
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.setAttribute('draggable', 'true');
row.innerHTML = `
<div class="drag-handle" title="Sleep om te verplaatsen">☰</div>
<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);
}
});
// Drag and Drop listeners
addDragListeners(row);
}
// Drag and Drop functionaliteit
function addDragListeners(row) {
row.addEventListener('dragstart', handleDragStart);
row.addEventListener('dragover', handleDragOver);
row.addEventListener('drop', handleDrop);
row.addEventListener('dragend', handleDragEnd);
}
function handleDragStart(e) {
dragSrcEl = this;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', this.dataset.index);
this.classList.add('dragging');
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
const sourceIndex = parseInt(e.dataTransfer.getData('text/plain'));
const targetIndex = parseInt(this.dataset.index);
if (dragSrcEl !== this && !isNaN(sourceIndex) && !isNaN(targetIndex)) {
// Swap in array
const itemToMove = config.stellingen[sourceIndex];
config.stellingen.splice(sourceIndex, 1);
config.stellingen.splice(targetIndex, 0, itemToMove);
// Re-render
renderStatements();
showStatus('Volgorde aangepast', 'success');
}
return false;
}
function handleDragEnd(e) {
this.classList.remove('dragging');
const rows = document.querySelectorAll('.statement-row');
rows.forEach(row => row.classList.remove('dragging'));
}
// 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();
// Scroll naar beneden
newRow.scrollIntoView({ behavior: 'smooth' });
showStatus('Nieuwe stelling toegevoegd', 'success');
}
// Update stelling count
function updateCount() {
if (!config || !config.stellingen) return;
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;
}
// 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: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const result = await response.json();
if (result.success) {
showStatus('✅ Config succesvol opgeslagen!', 'success');
setTimeout(() => loadConfig(), 500);
} else {
showStatus(`❌ Fout: ${result.error}`, 'error');
}
} catch (error) {
console.error('Fout bij opslaan:', error);
showStatus('❌ Fout bij opslaan config', 'error');
}
}
// --- Stellingkast Functionaliteit ---
async function loadStellingkast() {
const listContainer = document.getElementById('stellingkastList');
if (!listContainer) return; // Safety check
listContainer.innerHTML = '<div style="padding:1rem; text-align:center; color:#666;">Stellingen laden...</div>';
try {
console.log('Fetching stellingkast...');
const response = await fetch('/stellingkast.json?t=' + Date.now());
if (response.ok) {
stellingkastItems = await response.json();
console.log('Stellingkast loaded:', stellingkastItems.length, 'items');
renderStellingkastList(stellingkastItems);
} else {
console.error('Kon stellingkast.json niet laden', response.status);
listContainer.innerHTML = '<div style="padding:1rem; text-align:center; color:red;">Kon stellingkast niet laden (' + response.status + ').</div>';
}
} catch (error) {
console.error('Fout bij laden stellingkast:', error);
listContainer.innerHTML = '<div style="padding:1rem; text-align:center; color:red;">Fout: ' + error.message + '</div>';
}
}
function renderStellingkastList(items) {
if (!stellingkastList) return;
stellingkastList.innerHTML = '';
if (!items || items.length === 0) {
stellingkastList.innerHTML = '<div style="padding:1rem; text-align:center; color:#666;">Geen stellingen gevonden.</div>';
return;
}
items.forEach((item, index) => {
const el = document.createElement('div');
el.className = 'stellingkast-item';
el.innerHTML = `
<div class="stelling-text">
<div>
<span class="stelling-label">Links</span>
${item.links}
</div>
<div style="text-align: right;">
<span class="stelling-label">Rechts</span>
${item.rechts}
</div>
</div>
<button class="btn-import" data-index="${index}">+ Toevoegen</button>
`;
// Add event listener
const btn = el.querySelector('.btn-import');
if (btn) {
btn.addEventListener('click', () => {
importStelling(item);
});
}
stellingkastList.appendChild(el);
});
}
function importStelling(item) {
if (!config) config = { stellingen: [] };
if (!config.stellingen) config.stellingen = [];
// Voeg toe aan config
config.stellingen.push({ ...item }); // Kopie om referentie issues te voorkomen
// Render opnieuw
renderStatements();
// Scroll naar beneden
const newRow = statementsList.lastElementChild;
if (newRow) {
newRow.scrollIntoView({ behavior: 'smooth' });
}
// Feedback
showStatus('Stelling geïmporteerd!', 'success');
}
// Event listeners
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', () => {
stellingkastPanel.classList.add('open');
});
}
if (closeStellingkastBtn) {
closeStellingkastBtn.addEventListener('click', () => {
stellingkastPanel.classList.remove('open');
});
}
// Zoek functionaliteit
if (searchStellingkastInput) {
searchStellingkastInput.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
if (stellingkastItems) {
const filtered = stellingkastItems.filter(item =>
(item.links && item.links.toLowerCase().includes(term)) ||
(item.rechts && item.rechts.toLowerCase().includes(term))
);
renderStellingkastList(filtered);
}
});
}
// Klik buiten panel om te sluiten
document.addEventListener('click', (e) => {
if (stellingkastPanel && stellingkastPanel.classList.contains('open') &&
!stellingkastPanel.contains(e.target) &&
e.target !== openStellingkastBtn) {
stellingkastPanel.classList.remove('open');
}
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + S = opslaan
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveConfig();
}
// Escape sluit panel
if (e.key === 'Escape' && stellingkastPanel && stellingkastPanel.classList.contains('open')) {
stellingkastPanel.classList.remove('open');
}
});