Updated layout, added ability to make new sections, added new block types, increased layout size by 30%

This commit is contained in:
nathan
2026-05-01 14:37:36 -04:00
parent a472255a94
commit d0200f3975
7 changed files with 841 additions and 35 deletions

View File

@@ -6,29 +6,75 @@
class SectionBuilder {
constructor() {
this.sections = {
context: { title: 'Historical Context', icon: '🌍', color: '#d4a574', blocks: [] },
rcaf: { title: 'RCAF Desert Cats', icon: '✈️', color: '#3498db', blocks: [] },
rcn: { title: 'RCN Task Group', icon: '⚓', color: '#2c3e50', blocks: [] },
army: { title: 'Canadian Army', icon: '🏥', color: '#27ae60', blocks: [] },
legacy: { title: 'After the War', icon: '🍁', color: '#c0392b', blocks: [] }
};
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('context');
this.loadSection(this.sections.context ? 'context' : Object.keys(this.sections)[0]);
}
// ── Storage ────────────────────────────────────────────────────────────
@@ -45,8 +91,72 @@ class SectionBuilder {
});
}
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!');
}
@@ -92,6 +202,9 @@ class SectionBuilder {
+ '</div>';
canvas.innerHTML = html;
if (typeof BlockRenderer !== 'undefined' && typeof BlockRenderer.initGalleries === 'function') {
BlockRenderer.initGalleries(canvas);
}
this.bindCanvasEvents();
}
@@ -173,7 +286,33 @@ class SectionBuilder {
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 'gallery': return { id: id, type: type, images: [{ src: '', alt: '', caption: '' }] };
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: '' }
@@ -237,8 +376,46 @@ class SectionBuilder {
return this.formGroup('Quote Text', '<textarea name="text" rows="4">' + this.esc(block.text || '') + '</textarea>')
+ this.formGroup('Attribution (optional)', '<input type="text" name="author" value="' + this.esc(block.author || '') + '" placeholder="Name, Title, Date">');
case 'fact-panel':
return this.formGroup('Panel Title', '<input type="text" name="title" value="' + this.esc(block.title || '') + '" placeholder="e.g. Desert Shield Force Buildup">')
+ this.formGroup('Facts',
'<div id="factItems">'
+ ((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, ''))
+ '</div>'
+ '<button type="button" class="add-item-btn btn-add-fact">+ Add Fact</button>');
case 'timeline-card':
return this.formGroup('Date Label', '<input type="text" name="date" value="' + this.esc(block.date || '') + '" placeholder="e.g. Sept 24">')
+ this.formGroup('Heading', '<input type="text" name="title" value="' + this.esc(block.title || '') + '" placeholder="Timeline event title">')
+ this.formGroup('Details', '<textarea name="text" rows="4">' + this.esc(block.text || '') + '</textarea>');
case 'gallery':
return this.formGroup('Gallery Images',
return this.formGroup('Display Mode',
'<select name="mode">'
+ '<option value="carousel"' + ((block.mode || 'carousel') === 'carousel' ? ' selected' : '') + '>Carousel (single image, rotates)</option>'
+ '<option value="strip"' + ((block.mode || 'carousel') === 'strip' ? ' selected' : '') + '>Strip (horizontal scroll)</option>'
+ '</select>')
+ this.formGroup('Auto-rotate',
'<select name="autoplay">'
+ '<option value="true"' + (block.autoplay === false ? '' : ' selected') + '>On</option>'
+ '<option value="false"' + (block.autoplay === false ? ' selected' : '') + '>Off</option>'
+ '</select>')
+ this.formGroup('Rotate Every (seconds)',
'<input type="number" name="intervalSec" min="2" max="15" value="' + (parseInt(block.intervalSec, 10) || 5) + '">')
+ this.formGroup('Show Dots',
'<select name="showDots">'
+ '<option value="true"' + (block.showDots === false ? '' : ' selected') + '>On</option>'
+ '<option value="false"' + (block.showDots === false ? ' selected' : '') + '>Off</option>'
+ '</select>')
+ this.formGroup('Show Arrows',
'<select name="showArrows">'
+ '<option value="true"' + (block.showArrows === false ? '' : ' selected') + '>On</option>'
+ '<option value="false"' + (block.showArrows === false ? ' selected' : '') + '>Off</option>'
+ '</select>')
+ this.formGroup('Gallery Images',
'<div id="galleryItems">'
+ (block.images || []).map(function(img, i) { return SectionBuilder.imageRow(i, img, true); }).join('')
+ '</div>'
@@ -289,6 +466,13 @@ class SectionBuilder {
+ '</div>';
}
static factRow(index, text) {
return '<div class="item-row" data-index="' + index + '">'
+ '<input type="text" data-field="text" data-index="' + index + '" value="' + (text || '') + '" placeholder="Fact line (e.g. Day 90: 320,000 coalition troops)">'
+ '<button type="button" class="btn-remove-item">&#x2715;</button>'
+ '</div>';
}
bindDynamicFormEvents(container) {
var self = this;
@@ -312,6 +496,16 @@ class SectionBuilder {
});
}
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);
}
@@ -348,7 +542,23 @@ class SectionBuilder {
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':
@@ -437,12 +647,46 @@ class SectionBuilder {
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
Object.keys(self.sections).forEach(function(id) {
if (data[id] && data[id].blocks) {
self.sections[id].blocks = data[id].blocks;
localStorage.setItem('pf-section-' + id, JSON.stringify(data[id].blocks));
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) {
@@ -460,13 +704,25 @@ class SectionBuilder {
});
});
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('.modal-overlay').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();
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);
@@ -474,10 +730,73 @@ class SectionBuilder {
});
}
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] = { blocks: this.sections[id].blocks };
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);
@@ -503,7 +822,7 @@ class SectionBuilder {
blockTypeLabel(type) {
var labels = {
heading: 'H Heading', paragraph: '¶ Paragraph', image: '🖼 Image',
quote: '" Quote', gallery: '⊞ Gallery', grid4: '▦ Grid 4',
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;