From d0200f397539d4684b51456abff412205bc516c8 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 1 May 2026 14:37:36 -0400 Subject: [PATCH] Updated layout, added ability to make new sections, added new block types, increased layout size by 30% --- app.js | 71 ++++++++- blocks.css | 160 +++++++++++++++++++ builder.html | 103 ++++++++++++- index.html | 2 +- js/block-renderer.js | 175 ++++++++++++++++++++- js/builder.js | 355 ++++++++++++++++++++++++++++++++++++++++--- main.css | 10 +- 7 files changed, 841 insertions(+), 35 deletions(-) diff --git a/app.js b/app.js index b7cdbb5..95b6422 100644 --- a/app.js +++ b/app.js @@ -60,6 +60,71 @@ class ProjectFriction { if (!Array.isArray(this.data.sections) || this.data.sections.length === 0) { this.loadDefaultConfiguration(); } + + this.mergeBuilderSectionsFromStorage(); + } + + mergeBuilderSectionsFromStorage() { + if (typeof localStorage === 'undefined') return; + + var raw = localStorage.getItem('pf-builder-sections'); + if (!raw) return; + + try { + var parsed = JSON.parse(raw); + var order = Array.isArray(parsed.order) ? parsed.order : Object.keys(parsed.sections || {}); + var sectionMeta = parsed.sections || {}; + if (!Array.isArray(this.data.sections)) this.data.sections = []; + + var existingById = {}; + this.data.sections.forEach(function(section) { + existingById[section.id] = section; + }); + + order.forEach(function(id) { + var meta = sectionMeta[id]; + if (!meta) return; + + var 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 + }; + + if (existingById[id]) { + existingById[id].title = meta.title || existingById[id].title; + existingById[id].subtitle = meta.subtitle || existingById[id].subtitle; + existingById[id].description = meta.description || existingById[id].description; + existingById[id].icon = meta.icon || existingById[id].icon; + existingById[id].color = meta.color || existingById[id].color; + existingById[id].stats = stats; + } else { + var newSection = { + id: id, + title: meta.title || id, + subtitle: meta.subtitle || '', + icon: meta.icon || 'πŸ“š', + color: meta.color || '#7f8c8d', + description: meta.description || '', + stats: stats + }; + this.data.sections.push(newSection); + existingById[id] = newSection; + } + }, this); + + // Keep display order aligned with builder tabs for kiosk consistency. + var reordered = []; + order.forEach(function(id) { + if (existingById[id]) reordered.push(existingById[id]); + }); + this.data.sections.forEach(function(section) { + if (order.indexOf(section.id) === -1) reordered.push(section); + }); + this.data.sections = reordered; + } catch (error) { + console.warn('Could not parse builder section metadata from localStorage.'); + } } async loadJSON(url) { @@ -434,6 +499,10 @@ class ProjectFriction { } initializeSectionInteractions(sectionId) { + if (typeof BlockRenderer !== 'undefined' && typeof BlockRenderer.initGalleries === 'function') { + BlockRenderer.initGalleries(document.getElementById('sectionContainer')); + } + // Wire map-button blocks to the Leaflet map const self = this; document.querySelectorAll('.block-map-btn').forEach(function(btn) { @@ -526,7 +595,7 @@ class ProjectFriction { if (sidebar && toggleIcon) { sidebar.classList.toggle('collapsed'); - toggleIcon.textContent = sidebar.classList.contains('collapsed') ? 'β–Ά' : 'β—€'; + toggleIcon.textContent = sidebar.classList.contains('collapsed') ? 'β—€' : 'β–Ά'; } } diff --git a/blocks.css b/blocks.css index e59e71c..079b268 100644 --- a/blocks.css +++ b/blocks.css @@ -77,11 +77,85 @@ /* ── Quote Block ─────────────────────────────────────────────────────────── */ /* Uses existing .quote-block, .quote-text, .quote-attribution from components.css */ +/* ── Fact Briefing Panel ─────────────────────────────────────────────────── */ +.block-fact-panel { + margin: 1.25rem 0; +} + +.block-fact-panel .fact-panel-list { + margin: 0; + padding: 0; +} + +/* ── Timeline Card ───────────────────────────────────────────────────────── */ +.block-timeline-card { + background: var(--color-white); + border: 1px solid var(--color-gray); + border-radius: var(--radius-md); + padding: 1rem 1.1rem; + margin: 1.25rem 0; +} + +.block-timeline-header { + display: flex; + align-items: flex-start; + gap: 0.55rem; + margin-bottom: 0.45rem; + flex-wrap: wrap; +} + +.block-timeline-date { + background: var(--color-accent); + color: var(--color-white); + padding: 0.18rem 0.45rem; + border-radius: var(--radius-sm); + font-size: 12px; + line-height: 1.2; + font-weight: 600; + font-family: var(--font-secondary); + white-space: nowrap; + margin-top: 0.05rem; +} + +.block-timeline-title { + margin: 0; + color: var(--color-primary); + font-size: 1.55rem; + line-height: 1.2; + font-family: var(--font-primary); + flex: 1; + min-width: 220px; +} + +.block-timeline-text { + margin: 0; + margin-left: calc(0.45rem + 0.55rem + 52px); + font-size: 1.1rem; + line-height: 1.45; + color: var(--color-primary); + font-family: var(--font-primary); +} + +@media (max-width: 640px) { + .block-timeline-text { + margin-left: 0; + } +} + /* ── Gallery Block ───────────────────────────────────────────────────────── */ .block-gallery { margin: 1.25rem 0; } +.gallery-empty { + padding: 0.75rem; + border: 1px dashed var(--color-gray); + border-radius: var(--radius-sm); + color: var(--color-dark-gray); + font-family: var(--font-secondary); + font-size: 13px; +} + .gallery-scroll { display: flex; gap: 0.75rem; @@ -127,6 +201,92 @@ right: 0; } +/* ── Carousel Gallery Mode ────────────────────────────────────────────────── */ +.gallery-mode-carousel .gallery-carousel { + position: relative; +} + +.gallery-mode-carousel .gallery-slides { + position: relative; + min-height: 220px; + border-radius: var(--radius-sm); + overflow: hidden; + box-shadow: var(--shadow-sm); + background: #0f1720; +} + +.gallery-mode-carousel .gallery-slide { + display: none; + margin: 0; + position: relative; +} + +.gallery-mode-carousel .gallery-slide.is-active { + display: block; +} + +.gallery-mode-carousel .gallery-slide img { + width: 100%; + height: 260px; + object-fit: cover; + display: block; +} + +.gallery-slide-caption { + position: absolute; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(to top, rgba(0,0,0,0.78), rgba(0,0,0,0)); + color: #fff; + font-size: 12px; + line-height: 1.45; + padding: 0.75rem; + font-family: var(--font-secondary); +} + +.gallery-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 34px; + height: 34px; + border: none; + border-radius: var(--radius-full); + background: rgba(0, 0, 0, 0.42); + color: #fff; + font-size: 24px; + line-height: 1; + cursor: pointer; +} + +.gallery-nav:hover { + background: rgba(0, 0, 0, 0.62); +} + +.gallery-nav-prev { left: 0.5rem; } +.gallery-nav-next { right: 0.5rem; } + +.gallery-dots { + margin-top: 0.6rem; + display: flex; + justify-content: center; + gap: 0.4rem; +} + +.gallery-dot { + width: 9px; + height: 9px; + border-radius: var(--radius-full); + border: none; + background: #cbd5e1; + cursor: pointer; +} + +.gallery-dot.is-active { + background: var(--color-primary); +} + /* ── 4-Image Grid Block ──────────────────────────────────────────────────── */ .block-grid4 { margin: 1.25rem 0; diff --git a/builder.html b/builder.html index 14fbd76..bc1fe96 100644 --- a/builder.html +++ b/builder.html @@ -147,6 +147,24 @@ .tab-icon { font-size: 16px; } + .new-section-btn { + width: 100%; + margin-top: 0.5rem; + padding: 0.5rem 0.7rem; + border: 1px dashed #3498db; + border-radius: 6px; + background: #f5fbff; + color: #2b6ea4; + font-size: 13px; + cursor: pointer; + transition: all 0.15s; + } + + .new-section-btn:hover { + background: #eaf4fb; + border-color: #2f8dd8; + } + /* ── Block Palette ───────────────────────────────────────────────── */ .palette-blocks { display: flex; @@ -321,18 +339,33 @@ .canvas-block-preview { padding: 1rem 1.25rem; - pointer-events: none; + pointer-events: auto; max-height: 280px; overflow: hidden; position: relative; } + .canvas-block-preview button, + .canvas-block-preview a, + .canvas-block-preview input, + .canvas-block-preview select, + .canvas-block-preview textarea { + pointer-events: none; + } + + .canvas-block-preview [data-gallery-prev], + .canvas-block-preview [data-gallery-next], + .canvas-block-preview [data-gallery-dot] { + pointer-events: auto; + } + .canvas-block-preview::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 30px; background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.8)); + pointer-events: none; } /* ── Insert Strip ────────────────────────────────────────────────── */ @@ -412,7 +445,8 @@ color: #2c3e50; } - #modalClose { + #modalClose, + #sectionWizardClose { background: none; border: none; font-size: 20px; @@ -422,7 +456,8 @@ padding: 0 0.2rem; } - #modalClose:hover { color: #2c3e50; } + #modalClose:hover, + #sectionWizardClose:hover { color: #2c3e50; } .modal-body { padding: 1.25rem; @@ -482,6 +517,12 @@ gap: 0.75rem; } + .form-row-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + } + /* Gallery / Stats item rows */ .item-row { display: flex; @@ -595,6 +636,7 @@
Sections
+
@@ -605,6 +647,8 @@ + + @@ -646,6 +690,59 @@ + + + diff --git a/index.html b/index.html index 56b0c69..7afd010 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@ '; 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', '') + 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('Gallery Images', + 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('') + '
' @@ -289,6 +466,13 @@ class SectionBuilder { + ''; } + static factRow(index, text) { + return '
' + + '' + + '' + + '
'; + } + 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; diff --git a/main.css b/main.css index 07b8439..0717d5a 100644 --- a/main.css +++ b/main.css @@ -77,7 +77,7 @@ body { /* Sidebar Styling */ .sidebar { - width: 420px; + width: 35%; background: var(--color-white); box-shadow: var(--shadow-lg); display: flex; @@ -85,20 +85,21 @@ body { z-index: 100; transition: transform var(--transition-normal); position: relative; + order: 2; } .sidebar.collapsed { - transform: translateX(-370px); + transform: translateX(370px); } .sidebar-toggle { position: absolute; - right: -30px; + left: -30px; top: 20px; width: 30px; height: 60px; background: var(--color-white); - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + border-radius: var(--radius-sm) 0 0 var(--radius-sm); box-shadow: var(--shadow-md); display: flex; align-items: center; @@ -266,6 +267,7 @@ body { flex: 1; position: relative; background: var(--color-secondary); + order: 1; } #map {