/**
* Section Builder - Project Friction
* Block-based page builder for the 5 exhibit sections.
* Saves to localStorage; export/import as JSON.
*/
class SectionBuilder {
constructor() {
this.sections = this.buildDefaultSections();
this.sectionsStorageKey = 'pf-builder-sections';
this.currentSection = 'context';
this.editingIndex = null;
this.defaultContent = null;
this.init();
}
buildDefaultSections() {
return {
context: {
title: 'Historical Context',
subtitle: 'Coalition Building & Diplomacy',
icon: 'π',
color: '#d4a574',
description: 'Understand how Iraq\'s invasion of Kuwait triggered the largest military coalition since WWII and Canada\'s diplomatic role.',
stats: { articles: 7, storyMaps: 3, images: 45 },
blocks: []
},
rcaf: {
title: 'RCAF Desert Cats',
subtitle: '409 Squadron Air Operations',
icon: 'βοΈ',
color: '#3498db',
description: 'Follow the Desert Cats from defensive patrols to strike missions, including the TNC-45 engagement and bombing runs.',
stats: { articles: 8, storyMaps: 4, images: 60 },
blocks: []
},
rcn: {
title: 'RCN Task Group',
subtitle: 'Naval Blockade Operations',
icon: 'β',
color: '#2c3e50',
description: 'The transformation of HMCS Terra Nova into a guided-missile destroyer and enforcement of UN sanctions.',
stats: { articles: 7, storyMaps: 2, images: 35 },
blocks: []
},
army: {
title: 'Canadian Army',
subtitle: 'Medical & Security Forces',
icon: 'π₯',
color: '#27ae60',
description: '1 Canadian Field Hospital\'s life-saving work and security operations protecting coalition installations.',
stats: { articles: 7, storyMaps: 3, images: 40 },
blocks: []
},
legacy: {
title: 'After the War',
subtitle: 'Veterans & Long-term Impact',
icon: 'π',
color: '#c0392b',
description: 'Gulf War Syndrome, environmental cleanup, peacekeeping missions, and ongoing support for veterans.',
stats: { articles: 7, storyMaps: 2, images: 30 },
blocks: []
}
};
}
async init() {
try {
var res = await fetch('data/sections-content.json');
if (res.ok) this.defaultContent = await res.json();
} catch (e) { console.warn('Could not load default content'); }
this.loadSectionsManifest();
this.loadAllFromStorage();
this.renderSectionTabs();
this.bindGlobalEvents();
this.loadSection(this.sections.context ? 'context' : Object.keys(this.sections)[0]);
}
// ββ Storage ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
loadAllFromStorage() {
var self = this;
Object.keys(this.sections).forEach(function(id) {
var saved = localStorage.getItem('pf-section-' + id);
if (saved) {
try { self.sections[id].blocks = JSON.parse(saved); } catch(e) {}
} else if (self.defaultContent && self.defaultContent[id]) {
self.sections[id].blocks = JSON.parse(JSON.stringify(self.defaultContent[id].blocks));
}
});
}
loadSectionsManifest() {
var saved = localStorage.getItem(this.sectionsStorageKey);
if (!saved) return;
try {
var parsed = JSON.parse(saved);
var existing = this.sections;
var merged = {};
var order = Array.isArray(parsed.order) ? parsed.order : Object.keys(parsed.sections || {});
var sectionMeta = parsed.sections || {};
order.forEach(function(id) {
if (!sectionMeta[id]) return;
merged[id] = {
title: sectionMeta[id].title || id,
subtitle: sectionMeta[id].subtitle || '',
description: sectionMeta[id].description || '',
icon: sectionMeta[id].icon || 'π',
color: sectionMeta[id].color || '#7f8c8d',
stats: {
articles: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.articles, 10) || 0,
storyMaps: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.storyMaps, 10) || 0,
images: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.images, 10) || 0
},
blocks: (existing[id] && existing[id].blocks) || []
};
});
Object.keys(existing).forEach(function(id) {
if (!merged[id]) merged[id] = existing[id];
});
this.sections = merged;
} catch (e) {
console.warn('Could not parse saved section metadata');
}
}
saveSectionsManifest() {
var payload = { order: Object.keys(this.sections), sections: {} };
var self = this;
payload.order.forEach(function(id) {
payload.sections[id] = self.sectionMeta(id);
});
localStorage.setItem(this.sectionsStorageKey, JSON.stringify(payload));
}
sectionMeta(id) {
var section = this.sections[id] || {};
return {
title: section.title || id,
subtitle: section.subtitle || '',
description: section.description || '',
icon: section.icon || 'π',
color: section.color || '#7f8c8d',
stats: {
articles: parseInt(section.stats && section.stats.articles, 10) || 0,
storyMaps: parseInt(section.stats && section.stats.storyMaps, 10) || 0,
images: parseInt(section.stats && section.stats.images, 10) || 0
}
};
}
saveSection(id) {
localStorage.setItem('pf-section-' + id, JSON.stringify(this.sections[id].blocks));
this.saveSectionsManifest();
this.showToast(this.sections[id].title + ' saved!');
}
// ββ Section Loading ββββββββββββββββββββββββββββββββββββββββββββββββββββ
loadSection(id) {
this.currentSection = id;
var sec = this.sections[id];
document.querySelectorAll('.section-tab').forEach(function(t) {
t.classList.toggle('active', t.dataset.section === id);
});
document.getElementById('canvasSectionTitle').textContent = sec.icon + ' ' + sec.title;
var badge = document.getElementById('canvasBadge');
badge.textContent = sec.blocks.length + ' block' + (sec.blocks.length !== 1 ? 's' : '');
badge.style.background = sec.color;
this.renderCanvas();
}
// ββ Canvas Rendering βββββββββββββββββββββββββββββββββββββββββββββββββββ
renderCanvas() {
var canvas = document.getElementById('builderCanvas');
var blocks = this.sections[this.currentSection].blocks;
var self = this;
if (blocks.length === 0) {
canvas.innerHTML = '
'
+ '
No blocks yet.
'
+ '
Click a block type in the sidebar to add one.
'
+ '
';
return;
}
var html = blocks.map(function(block, index) {
return self.renderCanvasBlock(block, index, blocks.length);
}).join('');
html += ''
+ ''
+ '
';
canvas.innerHTML = html;
if (typeof BlockRenderer !== 'undefined' && typeof BlockRenderer.initGalleries === 'function') {
BlockRenderer.initGalleries(canvas);
}
this.bindCanvasEvents();
}
renderCanvasBlock(block, index, total) {
var preview = BlockRenderer.renderBlock(block);
var isFirst = index === 0;
var isLast = index === total - 1;
var typeLabel = this.blockTypeLabel(block.type);
return ''
+ '
'
+ '
' + typeLabel + ''
+ '
'
+ ''
+ ''
+ ''
+ ''
+ '
'
+ '
'
+ '
' + preview + '
'
+ '
'
+ ''
+ '
'
+ '
';
}
bindCanvasEvents() {
var self = this;
document.querySelectorAll('[data-action]').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = btn.dataset.action;
var idx = parseInt(btn.dataset.index);
if (action === 'edit') self.openEditModal(idx);
if (action === 'delete') self.deleteBlock(idx);
if (action === 'up') self.moveBlock(idx, -1);
if (action === 'down') self.moveBlock(idx, 1);
});
});
document.querySelectorAll('.insert-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
self.pendingInsertIndex = parseInt(btn.dataset.index);
self.highlightPalette();
});
});
}
highlightPalette() {
var palette = document.querySelector('.palette-blocks');
palette.classList.add('palette-highlight');
setTimeout(function() { palette.classList.remove('palette-highlight'); }, 1200);
palette.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// ββ Block CRUD βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
addBlock(type) {
var block = this.createDefaultBlock(type);
var blocks = this.sections[this.currentSection].blocks;
var atIdx = (this.pendingInsertIndex !== undefined) ? this.pendingInsertIndex : blocks.length;
this.pendingInsertIndex = undefined;
if (atIdx >= 0 && atIdx <= blocks.length) {
blocks.splice(atIdx, 0, block);
} else {
blocks.push(block);
}
this.renderCanvas();
var newIdx = blocks.indexOf(block);
this.openEditModal(newIdx);
}
createDefaultBlock(type) {
var id = 'b' + Date.now();
switch (type) {
case 'heading': return { id: id, type: type, level: 2, text: 'New Heading' };
case 'paragraph': return { id: id, type: type, text: 'Enter paragraph text here.' };
case 'image': return { id: id, type: type, src: '', alt: '', caption: '' };
case 'quote': return { id: id, type: type, text: 'Quote text here.', author: 'Source' };
case 'fact-panel': return {
id: id,
type: type,
title: 'Fact Briefing',
facts: [
{ text: 'Day 1: 4,000 U.S. troops' },
{ text: 'Day 30: 72,000 coalition troops' },
{ text: 'Day 60: 185,000 coalition troops' }
]
};
case 'timeline-card': return {
id: id,
type: type,
date: 'Sept 24',
title: 'Canadian Naval Task Group Arrives',
text: 'HMCS Terra Nova, Athabaskan, and Protecteur begin patrol operations in the central Persian Gulf.'
};
case 'gallery': return {
id: id,
type: type,
mode: 'carousel',
autoplay: true,
intervalSec: 5,
showDots: true,
showArrows: true,
images: [{ src: '', alt: '', caption: '' }]
};
case 'grid4': return { id: id, type: type, images: [
{ src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' },
{ src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' }
]};
case 'statistics': return { id: id, type: type, stats: [{ number: '0', label: 'Label' }, { number: '0', label: 'Label' }] };
case 'map-button': return { id: id, type: type, label: 'View on Map', lat: 29.3759, lng: 47.9774, zoom: 6 };
default: return { id: id, type: type };
}
}
deleteBlock(index) {
if (!confirm('Delete this block?')) return;
this.sections[this.currentSection].blocks.splice(index, 1);
this.renderCanvas();
}
moveBlock(index, direction) {
var blocks = this.sections[this.currentSection].blocks;
var newIndex = index + direction;
if (newIndex < 0 || newIndex >= blocks.length) return;
var tmp = blocks[index]; blocks[index] = blocks[newIndex]; blocks[newIndex] = tmp;
this.renderCanvas();
// Scroll moved block into view
var el = document.querySelector('[data-index="' + newIndex + '"]');
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// ββ Modal Editor βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
openEditModal(index) {
this.editingIndex = index;
var block = this.sections[this.currentSection].blocks[index];
document.getElementById('modalTitle').textContent = 'Edit: ' + this.blockTypeLabel(block.type);
document.getElementById('modalBody').innerHTML = this.buildModalForm(block);
document.getElementById('builderModal').classList.remove('hidden');
this.bindDynamicFormEvents(document.getElementById('modalBody'));
var first = document.getElementById('modalBody').querySelector('input, textarea, select');
if (first) first.focus();
}
buildModalForm(block) {
switch (block.type) {
case 'heading':
return this.formGroup('Level',
'')
+ this.formGroup('Text', '');
case 'paragraph':
return this.formGroup('Text', '');
case 'image':
return this.formGroup('Image URL', '')
+ this.formGroup('Alt Text (accessibility)', '')
+ this.formGroup('Caption (optional)', '');
case 'quote':
return this.formGroup('Quote Text', '')
+ this.formGroup('Attribution (optional)', '');
case 'fact-panel':
return this.formGroup('Panel Title', '')
+ this.formGroup('Facts',
''
+ ((block.facts || block.items || []).map(function(fact, i) {
var text = (typeof fact === 'string') ? fact : (fact && fact.text) || '';
return SectionBuilder.factRow(i, text);
}).join('') || SectionBuilder.factRow(0, ''))
+ '
'
+ '');
case 'timeline-card':
return this.formGroup('Date Label', '')
+ this.formGroup('Heading', '')
+ this.formGroup('Details', '');
case 'gallery':
return this.formGroup('Display Mode',
'')
+ this.formGroup('Auto-rotate',
'')
+ this.formGroup('Rotate Every (seconds)',
'')
+ this.formGroup('Show Dots',
'')
+ this.formGroup('Show Arrows',
'')
+ this.formGroup('Gallery Images',
''
+ (block.images || []).map(function(img, i) { return SectionBuilder.imageRow(i, img, true); }).join('')
+ '
'
+ '');
case 'grid4':
return this.formGroup('4-Image Grid',
''
+ [0,1,2,3].map(function(i) { return SectionBuilder.imageRow(i, (block.images || [])[i] || {}, false); }).join('')
+ '
');
case 'statistics':
return this.formGroup('Statistics',
''
+ (block.stats || []).map(function(s, i) { return SectionBuilder.statRow(i, s); }).join('')
+ '
'
+ '');
case 'map-button':
return this.formGroup('Button Label', '')
+ ''
+ this.formGroup('Latitude', '')
+ this.formGroup('Longitude', '')
+ this.formGroup('Zoom (1β18)', '')
+ '
';
default:
return 'Unknown block type.
';
}
}
static imageRow(index, img, removable) {
return '';
}
static statRow(index, stat) {
return ''
+ ''
+ ''
+ ''
+ '
';
}
static factRow(index, text) {
return ''
+ ''
+ ''
+ '
';
}
bindDynamicFormEvents(container) {
var self = this;
var addImgBtn = container.querySelector('.btn-add-img');
if (addImgBtn) {
addImgBtn.addEventListener('click', function() {
var items = container.querySelector('#galleryItems');
var newIdx = items.querySelectorAll('.item-row').length;
items.insertAdjacentHTML('beforeend', SectionBuilder.imageRow(newIdx, {}, true));
self.bindRemoveBtns(container);
});
}
var addStatBtn = container.querySelector('.btn-add-stat');
if (addStatBtn) {
addStatBtn.addEventListener('click', function() {
var items = container.querySelector('#statsItems');
var newIdx = items.querySelectorAll('.item-row').length;
items.insertAdjacentHTML('beforeend', SectionBuilder.statRow(newIdx, {}));
self.bindRemoveBtns(container);
});
}
var addFactBtn = container.querySelector('.btn-add-fact');
if (addFactBtn) {
addFactBtn.addEventListener('click', function() {
var items = container.querySelector('#factItems');
var newIdx = items.querySelectorAll('.item-row').length;
items.insertAdjacentHTML('beforeend', SectionBuilder.factRow(newIdx, ''));
self.bindRemoveBtns(container);
});
}
this.bindRemoveBtns(container);
}
bindRemoveBtns(container) {
container.querySelectorAll('.btn-remove-item').forEach(function(btn) {
btn.onclick = function() {
btn.closest('.item-row').remove();
container.querySelectorAll('.item-row').forEach(function(row, i) {
row.dataset.index = i;
row.querySelectorAll('[data-index]').forEach(function(el) { el.dataset.index = i; });
});
};
});
}
saveModalBlock() {
var block = this.sections[this.currentSection].blocks[this.editingIndex];
var body = document.getElementById('modalBody');
switch (block.type) {
case 'heading':
block.level = parseInt(body.querySelector('[name="level"]').value);
block.text = body.querySelector('[name="text"]').value;
break;
case 'paragraph':
block.text = body.querySelector('[name="text"]').value;
break;
case 'image':
block.src = body.querySelector('[name="src"]').value;
block.alt = body.querySelector('[name="alt"]').value;
block.caption = body.querySelector('[name="caption"]').value;
break;
case 'quote':
block.text = body.querySelector('[name="text"]').value;
block.author = body.querySelector('[name="author"]').value;
break;
case 'fact-panel':
block.title = body.querySelector('[name="title"]').value;
block.facts = this.readItemRows(body.querySelector('#factItems'), ['text'])
.filter(function(f) { return (f.text || '').trim() !== ''; });
break;
case 'timeline-card':
block.date = body.querySelector('[name="date"]').value;
block.title = body.querySelector('[name="title"]').value;
block.text = body.querySelector('[name="text"]').value;
break;
case 'gallery':
block.mode = body.querySelector('[name="mode"]').value || 'carousel';
block.autoplay = body.querySelector('[name="autoplay"]').value === 'true';
block.intervalSec = parseInt(body.querySelector('[name="intervalSec"]').value, 10) || 5;
block.intervalSec = Math.max(2, Math.min(block.intervalSec, 15));
block.showDots = body.querySelector('[name="showDots"]').value === 'true';
block.showArrows = body.querySelector('[name="showArrows"]').value === 'true';
block.images = this.readItemRows(body.querySelector('#galleryItems'), ['src','alt','caption']);
break;
case 'grid4':
block.images = this.readItemRows(body.querySelector('#grid4Items'), ['src','alt','caption']);
break;
case 'statistics':
block.stats = this.readItemRows(body.querySelector('#statsItems'), ['number','label']);
break;
case 'map-button':
block.label = body.querySelector('[name="label"]').value;
block.lat = parseFloat(body.querySelector('[name="lat"]').value);
block.lng = parseFloat(body.querySelector('[name="lng"]').value);
block.zoom = parseInt(body.querySelector('[name="zoom"]').value);
break;
}
this.closeModal();
this.renderCanvas();
}
readItemRows(container, fields) {
if (!container) return [];
return Array.from(container.querySelectorAll('.item-row')).map(function(row) {
var obj = {};
fields.forEach(function(field) {
var el = row.querySelector('[data-field="' + field + '"]');
obj[field] = el ? el.value : '';
});
return obj;
});
}
closeModal() {
document.getElementById('builderModal').classList.add('hidden');
this.editingIndex = null;
}
// ββ UI Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
renderSectionTabs() {
var self = this;
var container = document.getElementById('sectionTabs');
container.innerHTML = Object.entries(this.sections).map(function(entry) {
var id = entry[0], sec = entry[1];
return '';
}).join('');
container.querySelectorAll('.section-tab').forEach(function(btn) {
btn.addEventListener('click', function() { self.loadSection(btn.dataset.section); });
});
}
bindGlobalEvents() {
var self = this;
document.getElementById('saveBtn').addEventListener('click', function() {
if (self.currentSection) self.saveSection(self.currentSection);
});
document.getElementById('resetBtn').addEventListener('click', function() {
if (!self.currentSection) return;
if (!confirm('Reset to default content? Saved changes will be lost.')) return;
localStorage.removeItem('pf-section-' + self.currentSection);
if (self.defaultContent && self.defaultContent[self.currentSection]) {
self.sections[self.currentSection].blocks = JSON.parse(JSON.stringify(self.defaultContent[self.currentSection].blocks));
} else {
self.sections[self.currentSection].blocks = [];
}
self.renderCanvas();
self.showToast('Section reset to default.');
});
document.getElementById('exportBtn').addEventListener('click', function() { self.exportJSON(); });
document.getElementById('importBtn').addEventListener('click', function() {
document.getElementById('importFile').click();
});
document.getElementById('importFile').addEventListener('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
Object.keys(data).forEach(function(id) {
var entry = data[id] || {};
var blocks = Array.isArray(entry.blocks) ? entry.blocks : (Array.isArray(entry) ? entry : []);
if (!self.sections[id]) {
var meta = entry.meta || { title: id, icon: 'π', color: '#7f8c8d' };
self.sections[id] = {
title: meta.title || id,
subtitle: meta.subtitle || '',
description: meta.description || '',
icon: meta.icon || 'π',
color: meta.color || '#7f8c8d',
stats: {
articles: parseInt(meta.stats && meta.stats.articles, 10) || 0,
storyMaps: parseInt(meta.stats && meta.stats.storyMaps, 10) || 0,
images: parseInt(meta.stats && meta.stats.images, 10) || 0
},
blocks: []
};
} else if (entry.meta) {
self.sections[id] = Object.assign({}, self.sections[id], {
title: entry.meta.title || self.sections[id].title,
subtitle: entry.meta.subtitle || self.sections[id].subtitle,
description: entry.meta.description || self.sections[id].description,
icon: entry.meta.icon || self.sections[id].icon,
color: entry.meta.color || self.sections[id].color,
stats: {
articles: parseInt(entry.meta.stats && entry.meta.stats.articles, 10) || 0,
storyMaps: parseInt(entry.meta.stats && entry.meta.stats.storyMaps, 10) || 0,
images: parseInt(entry.meta.stats && entry.meta.stats.images, 10) || 0
}
});
}
self.sections[id].blocks = blocks;
localStorage.setItem('pf-section-' + id, JSON.stringify(blocks));
});
self.saveSectionsManifest();
self.renderSectionTabs();
if (!self.sections[self.currentSection]) self.currentSection = Object.keys(self.sections)[0];
self.loadSection(self.currentSection);
self.showToast('Content imported!');
} catch (err) {
alert('Invalid JSON file: ' + err.message);
}
};
reader.readAsText(file);
e.target.value = '';
});
document.querySelectorAll('.palette-block').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!self.currentSection) return;
self.addBlock(btn.dataset.type);
});
});
document.getElementById('newSectionBtn').addEventListener('click', function() {
self.openSectionWizard();
});
document.getElementById('modalSave').addEventListener('click', function() { self.saveModalBlock(); });
document.getElementById('modalCancel').addEventListener('click', function() { self.closeModal(); });
document.getElementById('modalClose').addEventListener('click', function() { self.closeModal(); });
document.querySelector('#builderModal .modal-overlay').addEventListener('click', function() { self.closeModal(); });
document.getElementById('sectionWizardSave').addEventListener('click', function() { self.createSectionFromWizard(); });
document.getElementById('sectionWizardCancel').addEventListener('click', function() { self.closeSectionWizard(); });
document.getElementById('sectionWizardClose').addEventListener('click', function() { self.closeSectionWizard(); });
document.querySelector('#sectionWizardModal .modal-overlay').addEventListener('click', function() { self.closeSectionWizard(); });
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
self.closeModal();
self.closeSectionWizard();
}
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
if (self.currentSection) self.saveSection(self.currentSection);
}
});
}
openSectionWizard() {
document.getElementById('wizardTitle').value = '';
document.getElementById('wizardSubtitle').value = '';
document.getElementById('wizardDescription').value = '';
document.getElementById('wizardIcon').value = 'π';
document.getElementById('wizardColor').value = '#7f8c8d';
document.getElementById('wizardArticles').value = '0';
document.getElementById('wizardStoryMaps').value = '0';
document.getElementById('wizardImages').value = '0';
document.getElementById('sectionWizardModal').classList.remove('hidden');
document.getElementById('wizardTitle').focus();
}
closeSectionWizard() {
document.getElementById('sectionWizardModal').classList.add('hidden');
}
createSectionFromWizard() {
var title = (document.getElementById('wizardTitle').value || '').trim();
if (!title) {
alert('Please enter a title for the new section.');
return;
}
var idBase = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
if (!idBase) idBase = 'section';
var id = idBase;
var i = 2;
while (this.sections[id]) {
id = idBase + '-' + i;
i++;
}
var color = (document.getElementById('wizardColor').value || '').trim() || '#7f8c8d';
if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) {
alert('Please enter a valid hex color (for example: #3498db).');
return;
}
this.sections[id] = {
title: title,
subtitle: (document.getElementById('wizardSubtitle').value || '').trim(),
description: (document.getElementById('wizardDescription').value || '').trim(),
icon: (document.getElementById('wizardIcon').value || '').trim() || 'π',
color: color,
stats: {
articles: parseInt(document.getElementById('wizardArticles').value, 10) || 0,
storyMaps: parseInt(document.getElementById('wizardStoryMaps').value, 10) || 0,
images: parseInt(document.getElementById('wizardImages').value, 10) || 0
},
blocks: []
};
this.saveSectionsManifest();
this.renderSectionTabs();
this.closeSectionWizard();
this.loadSection(id);
this.showToast('Section created. Add blocks and save when ready.');
}
exportJSON() {
var output = {};
Object.keys(this.sections).forEach(function(id) {
output[id] = {
meta: this.sectionMeta(id),
blocks: this.sections[id].blocks
};
}, this);
var blob = new Blob([JSON.stringify(output, null, 2)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'sections-content.json';
a.click();
URL.revokeObjectURL(url);
}
showToast(message) {
var toast = document.createElement('div');
toast.className = 'builder-toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(function() { toast.classList.add('show'); }, 10);
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() { toast.remove(); }, 300);
}, 2500);
}
blockTypeLabel(type) {
var labels = {
heading: 'H Heading', paragraph: 'ΒΆ Paragraph', image: 'πΌ Image',
quote: '" Quote', 'fact-panel': 'βΈ Fact Briefing Panel', 'timeline-card': 'π Timeline Card', gallery: 'β Gallery', grid4: 'β¦ Grid 4',
statistics: '# Statistics', 'map-button': 'π Map Button'
};
return labels[type] || type;
}
formGroup(label, inputHtml) {
return '' + inputHtml + '
';
}
esc(str) {
return String(str)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(//g, '>');
}
}
document.addEventListener('DOMContentLoaded', function() {
window.builder = new SectionBuilder();
});