patx/frontlineboats
add editable gallery manifest
Commit 0932241 · patx · 2026-07-30T22:52:18-04:00
Comments
No comments yet.
Diff
diff --git a/docs/gallery/gallery.json b/docs/gallery/gallery.json
new file mode 100644
index 0000000..4d3f361
--- /dev/null
+++ b/docs/gallery/gallery.json
@@ -0,0 +1,252 @@
+{
+ "version": 1,
+ "categories": [
+ { "id": "electronics", "label": "Electronics" },
+ { "id": "wiring", "label": "Wiring & Repair" },
+ { "id": "outboard", "label": "Outboard" },
+ { "id": "detailing", "label": "Detailing" },
+ { "id": "audio", "label": "Audio & Lighting" }
+ ],
+ "images": [
+ {
+ "id": "electronics-seafox-helm-raymarine",
+ "src": "gallery/electronics-seafox-helm-raymarine.jpg",
+ "thumb": "gallery/thumbs/electronics-seafox-helm-raymarine.jpg",
+ "category": "electronics",
+ "caption": "Raymarine helm build",
+ "alt": "Sea Fox center console helm with a Raymarine multifunction display, analog gauges and a custom backlit switch panel"
+ },
+ {
+ "id": "electronics-garmin-twin-display-acrylic",
+ "src": "gallery/electronics-garmin-twin-display-acrylic.jpg",
+ "thumb": "gallery/thumbs/electronics-garmin-twin-display-acrylic.jpg",
+ "category": "electronics",
+ "caption": "Custom acrylic helm panel",
+ "alt": "Twin Garmin displays set into a custom acrylic helm panel with a Fusion stereo and Lenco trim controls"
+ },
+ {
+ "id": "electronics-sea-pro-twin-garmin-vhf",
+ "src": "gallery/electronics-sea-pro-twin-garmin-vhf.jpg",
+ "thumb": "gallery/thumbs/electronics-sea-pro-twin-garmin-vhf.jpg",
+ "category": "electronics",
+ "caption": "Twin display & VHF",
+ "alt": "Sea Pro helm with twin Garmin displays, a VHF radio and a refreshed gauge cluster"
+ },
+ {
+ "id": "electronics-action-craft-simrad-helm",
+ "src": "gallery/electronics-action-craft-simrad-helm.jpg",
+ "thumb": "gallery/thumbs/electronics-action-craft-simrad-helm.jpg",
+ "category": "electronics",
+ "caption": "Simrad flats-boat helm",
+ "alt": "Action Craft helm fitted with a Simrad display, stereo head unit and new switch panel"
+ },
+ {
+ "id": "electronics-wellcraft-garmin-upgrade",
+ "src": "gallery/electronics-wellcraft-garmin-upgrade.jpg",
+ "thumb": "gallery/thumbs/electronics-wellcraft-garmin-upgrade.jpg",
+ "category": "electronics",
+ "caption": "Garmin MFD upgrade",
+ "alt": "Wellcraft helm upgraded with a Garmin multifunction display and a secondary engine data screen"
+ },
+ {
+ "id": "electronics-garmin-displays-switch-panel",
+ "src": "gallery/electronics-garmin-displays-switch-panel.jpg",
+ "thumb": "gallery/thumbs/electronics-garmin-displays-switch-panel.jpg",
+ "category": "electronics",
+ "caption": "Labeled switch panel",
+ "alt": "Twin Garmin displays beside a labeled stainless switch panel and Suzuki trim controls"
+ },
+ {
+ "id": "electronics-garmin-yamaha-helm-rebuild",
+ "src": "gallery/electronics-garmin-yamaha-helm-rebuild.jpg",
+ "thumb": "gallery/thumbs/electronics-garmin-yamaha-helm-rebuild.jpg",
+ "category": "electronics",
+ "caption": "Full helm rebuild",
+ "alt": "Helm rebuild with a Garmin display, Yamaha binnacle controls, tachometer and new push-button panel"
+ },
+ {
+ "id": "electronics-gauge-cluster-switch-panel",
+ "src": "gallery/electronics-gauge-cluster-switch-panel.jpg",
+ "thumb": "gallery/thumbs/electronics-gauge-cluster-switch-panel.jpg",
+ "category": "electronics",
+ "caption": "Gauge cluster refit",
+ "alt": "Restored gauge cluster and lit switch panel on a center console dash"
+ },
+ {
+ "id": "wiring-rigging-board-bus-bars",
+ "src": "gallery/wiring-rigging-board-bus-bars.jpg",
+ "thumb": "gallery/thumbs/wiring-rigging-board-bus-bars.jpg",
+ "category": "wiring",
+ "caption": "Custom rigging board",
+ "alt": "Custom rigging board with dual fuse blocks, bus bars, NMEA 2000 drops and amplifiers"
+ },
+ {
+ "id": "wiring-console-fuse-panel-nmea2000",
+ "src": "gallery/wiring-console-fuse-panel-nmea2000.jpg",
+ "thumb": "gallery/thumbs/wiring-console-fuse-panel-nmea2000.jpg",
+ "category": "wiring",
+ "caption": "Console rewire",
+ "alt": "Console rewire with a labeled fuse block, ignition-protected breakers and an NMEA 2000 backbone"
+ },
+ {
+ "id": "wiring-full-rewire-panel-amp",
+ "src": "gallery/wiring-full-rewire-panel-amp.jpg",
+ "thumb": "gallery/thumbs/wiring-full-rewire-panel-amp.jpg",
+ "category": "wiring",
+ "caption": "Complete rewire",
+ "alt": "Complete rewire on a backing board with dual fuse blocks, labeled breakers, bus bars and an amplifier"
+ },
+ {
+ "id": "wiring-bench-built-fuse-block",
+ "src": "gallery/wiring-bench-built-fuse-block.jpg",
+ "thumb": "gallery/thumbs/wiring-bench-built-fuse-block.jpg",
+ "category": "wiring",
+ "caption": "Bench-built distribution",
+ "alt": "Bench-built distribution panel with a Blue Sea fuse block and every circuit individually labeled"
+ },
+ {
+ "id": "wiring-switch-panel-fusing",
+ "src": "gallery/wiring-switch-panel-fusing.jpg",
+ "thumb": "gallery/thumbs/wiring-switch-panel-fusing.jpg",
+ "category": "wiring",
+ "caption": "New switch panel",
+ "alt": "New helm switch panel with push-button switches, a USB charge port and proper circuit fusing"
+ },
+ {
+ "id": "wiring-console-harness-cleanup",
+ "src": "gallery/wiring-console-harness-cleanup.jpg",
+ "thumb": "gallery/thumbs/wiring-console-harness-cleanup.jpg",
+ "category": "wiring",
+ "caption": "Harness cleanup",
+ "alt": "Console harness cleanup with a new fuse block, bus bars and loomed, secured cable runs"
+ },
+ {
+ "id": "repair-bilge-pumps-seachest-battery",
+ "src": "gallery/repair-bilge-pumps-seachest-battery.jpg",
+ "thumb": "gallery/thumbs/repair-bilge-pumps-seachest-battery.jpg",
+ "category": "wiring",
+ "caption": "Pumps & sea chest",
+ "alt": "Bilge compartment with washdown pump, sea chest plumbing, battery switch and protected battery cabling"
+ },
+ {
+ "id": "outboard-service-oil-change",
+ "src": "gallery/outboard-service-oil-change.jpg",
+ "thumb": "gallery/thumbs/outboard-service-oil-change.jpg",
+ "category": "outboard",
+ "caption": "Scheduled service",
+ "alt": "Outboard tilted up for a scheduled oil and filter service on a bay boat"
+ },
+ {
+ "id": "outboard-gearcase-lube-service",
+ "src": "gallery/outboard-gearcase-lube-service.jpg",
+ "thumb": "gallery/thumbs/outboard-gearcase-lube-service.jpg",
+ "category": "outboard",
+ "caption": "Gearcase service",
+ "alt": "Lower unit gearcase lube drained into a pan and refilled during routine outboard maintenance"
+ },
+ {
+ "id": "outboard-powerhead-repair",
+ "src": "gallery/outboard-powerhead-repair.jpg",
+ "thumb": "gallery/thumbs/outboard-powerhead-repair.jpg",
+ "category": "outboard",
+ "caption": "Powerhead diagnostics",
+ "alt": "Outboard with the cowling removed for powerhead diagnostics and repair"
+ },
+ {
+ "id": "outboard-yamaha-70-rigging",
+ "src": "gallery/outboard-yamaha-70-rigging.jpg",
+ "thumb": "gallery/thumbs/outboard-yamaha-70-rigging.jpg",
+ "category": "outboard",
+ "caption": "Repower & rigging",
+ "alt": "Yamaha 70 outboard rigged on a tender with new controls, fuel line and battery box"
+ },
+ {
+ "id": "outboard-starter-motor-replacement",
+ "src": "gallery/outboard-starter-motor-replacement.jpg",
+ "thumb": "gallery/thumbs/outboard-starter-motor-replacement.jpg",
+ "category": "outboard",
+ "caption": "Starter replacement",
+ "alt": "New starter motor and flywheel exposed during an outboard starting-system repair"
+ },
+ {
+ "id": "detailing-seavee-hull-gloss",
+ "src": "gallery/detailing-seavee-hull-gloss.jpg",
+ "thumb": "gallery/thumbs/detailing-seavee-hull-gloss.jpg",
+ "category": "detailing",
+ "caption": "Hull cut & polish",
+ "alt": "SeaVee hull polished to a mirror gloss alongside a Miami dock"
+ },
+ {
+ "id": "detailing-sea-ray-hull-polish",
+ "src": "gallery/detailing-sea-ray-hull-polish.jpg",
+ "thumb": "gallery/thumbs/detailing-sea-ray-hull-polish.jpg",
+ "category": "detailing",
+ "caption": "Compound & seal",
+ "alt": "Sea Ray blue hull compounded, polished and sealed on the hard"
+ },
+ {
+ "id": "detailing-motor-yacht-exterior",
+ "src": "gallery/detailing-motor-yacht-exterior.jpg",
+ "thumb": "gallery/thumbs/detailing-motor-yacht-exterior.jpg",
+ "category": "detailing",
+ "caption": "Full exterior package",
+ "alt": "Motor yacht topsides and stainless detailed and polished from the waterline up"
+ },
+ {
+ "id": "detailing-yacht-topsides-coating",
+ "src": "gallery/detailing-yacht-topsides-coating.jpg",
+ "thumb": "gallery/thumbs/detailing-yacht-topsides-coating.jpg",
+ "category": "detailing",
+ "caption": "Ceramic coating",
+ "alt": "Dark yacht topsides after a full exterior detail and protective ceramic coating"
+ },
+ {
+ "id": "detailing-console-gelcoat-gloss",
+ "src": "gallery/detailing-console-gelcoat-gloss.jpg",
+ "thumb": "gallery/thumbs/detailing-console-gelcoat-gloss.jpg",
+ "category": "detailing",
+ "caption": "Gelcoat restoration",
+ "alt": "Console and gunwale gelcoat restored to a deep even gloss"
+ },
+ {
+ "id": "detailing-hull-side-waterline-gloss",
+ "src": "gallery/detailing-hull-side-waterline-gloss.jpg",
+ "thumb": "gallery/thumbs/detailing-hull-side-waterline-gloss.jpg",
+ "category": "detailing",
+ "caption": "Waterline finish",
+ "alt": "Hull side and waterline finished to a clean gloss at the dock"
+ },
+ {
+ "id": "detailing-yacht-hull-mirror-finish",
+ "src": "gallery/detailing-yacht-hull-mirror-finish.jpg",
+ "thumb": "gallery/thumbs/detailing-yacht-hull-mirror-finish.jpg",
+ "category": "detailing",
+ "caption": "Mirror finish",
+ "alt": "Grey yacht hull polished to a mirror finish reflecting the teak deck"
+ },
+ {
+ "id": "lighting-night-helm-simrad-led",
+ "src": "gallery/lighting-night-helm-simrad-led.jpg",
+ "thumb": "gallery/thumbs/lighting-night-helm-simrad-led.jpg",
+ "category": "audio",
+ "caption": "LED helm lighting",
+ "alt": "Night helm with a Simrad chartplotter, Fusion stereo and green LED-lit speakers"
+ },
+ {
+ "id": "audio-boss-head-unit-switch-panel",
+ "src": "gallery/audio-boss-head-unit-switch-panel.jpg",
+ "thumb": "gallery/thumbs/audio-boss-head-unit-switch-panel.jpg",
+ "category": "audio",
+ "caption": "Head unit install",
+ "alt": "Marine-grade Boss Audio head unit mounted beside a lit stainless switch panel at the helm"
+ },
+ {
+ "id": "audio-amplifier-fuse-block-install",
+ "src": "gallery/audio-amplifier-fuse-block-install.jpg",
+ "thumb": "gallery/thumbs/audio-amplifier-fuse-block-install.jpg",
+ "category": "audio",
+ "caption": "Amplifier & fusing",
+ "alt": "Marine amplifier and fuse block mounted under the console with loomed, strain-relieved cabling"
+ }
+ ]
+}
diff --git a/docs/index.html b/docs/index.html
index 8ee51c4..d3afea2 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -2128,7 +2128,8 @@
<button class="filter" type="button" data-filter="audio" aria-pressed="false">Audio & Lighting <span class="filter-count">3</span></button>
</div>
- <div class="shots" id="shots" data-reveal>
+ <div class="shots" id="shots" data-gallery-manifest="gallery/gallery.json" data-reveal>
+ <template id="legacy-gallery-items">
<!-- Electronics -->
<button class="shot" type="button" data-cat="electronics" data-full="gallery/electronics-seafox-helm-raymarine.jpg">
@@ -2285,12 +2286,14 @@
<span class="shot-tag">Amplifier & fusing</span>
</button>
+ </template>
</div>
- <p class="gallery-empty" id="gallery-empty" hidden>No photos in that trade yet.</p>
+ <p class="gallery-empty" id="gallery-empty" aria-live="polite" hidden>No photos in that trade yet.</p>
+ <noscript><p class="gallery-empty">The project gallery requires JavaScript to load.</p></noscript>
<div class="gallery-more">
- <button class="btn btn--ghost" type="button" id="gallery-toggle" aria-controls="shots">Show all 30 photos</button>
+ <button class="btn btn--ghost" type="button" id="gallery-toggle" aria-controls="shots">Show all photos</button>
</div>
</section>
@@ -2867,8 +2870,9 @@
/* ── Gallery: filter + show more ────────────────────────────────────── */
var shotsGrid = document.getElementById('shots');
- var shots = Array.prototype.slice.call(shotsGrid.querySelectorAll('.shot'));
- var filterButtons = Array.prototype.slice.call(document.querySelectorAll('.filter'));
+ var filters = document.querySelector('.filters');
+ var shots = [];
+ var filterButtons = [];
var galleryToggle = document.getElementById('gallery-toggle');
var galleryEmpty = document.getElementById('gallery-empty');
var INITIAL_SHOWN = 12;
@@ -2877,6 +2881,43 @@
var expanded = false;
var visibleShots = [];
+ function makeFilter(filter, label, count, pressed) {
+ var button = document.createElement('button');
+ var countLabel = document.createElement('span');
+ button.className = 'filter';
+ button.type = 'button';
+ button.dataset.filter = filter;
+ button.setAttribute('aria-pressed', String(pressed));
+ button.appendChild(document.createTextNode(label + ' '));
+ countLabel.className = 'filter-count';
+ countLabel.textContent = count;
+ button.appendChild(countLabel);
+ return button;
+ }
+
+ function makeShot(item) {
+ var shot = document.createElement('button');
+ var image = document.createElement('img');
+ var caption = document.createElement('span');
+ shot.className = 'shot';
+ shot.type = 'button';
+ shot.dataset.cat = item.category || '';
+ shot.dataset.full = item.src;
+ image.className = 'shot-img';
+ image.src = item.thumb;
+ image.alt = item.alt;
+ image.loading = 'lazy';
+ caption.className = 'shot-tag';
+ caption.textContent = item.caption;
+ shot.appendChild(image);
+ shot.appendChild(caption);
+ shot.addEventListener('click', function () {
+ var index = visibleShots.indexOf(shot);
+ if (index !== -1) { openLightbox(index); }
+ });
+ return shot;
+ }
+
function renderGallery() {
var matching = shots.filter(function (shot) {
return currentFilter === 'all' || shot.dataset.cat === currentFilter;
@@ -2906,16 +2947,43 @@
}
}
- filterButtons.forEach(function (button) {
- button.addEventListener('click', function () {
- currentFilter = button.dataset.filter;
- expanded = false;
- filterButtons.forEach(function (other) {
- other.setAttribute('aria-pressed', String(other === button));
+ function startGallery(manifest) {
+ if (!manifest || manifest.version !== 1 ||
+ !Array.isArray(manifest.categories) || !Array.isArray(manifest.images)) {
+ throw new Error('Invalid gallery manifest');
+ }
+
+ shotsGrid.innerHTML = '';
+ filters.innerHTML = '';
+ manifest.images.forEach(function (item) {
+ if (!item || !item.id || !item.src || !item.thumb || !item.caption || !item.alt) {
+ throw new Error('Invalid gallery image');
+ }
+ var shot = makeShot(item);
+ shotsGrid.appendChild(shot);
+ shots.push(shot);
+ });
+
+ filterButtons.push(makeFilter('all', 'All Work', shots.length, true));
+ manifest.categories.forEach(function (category) {
+ var count = manifest.images.filter(function (item) {
+ return item.category === category.id;
+ }).length;
+ filterButtons.push(makeFilter(category.id, category.label, count, false));
+ });
+ filterButtons.forEach(function (button) {
+ filters.appendChild(button);
+ button.addEventListener('click', function () {
+ currentFilter = button.dataset.filter;
+ expanded = false;
+ filterButtons.forEach(function (other) {
+ other.setAttribute('aria-pressed', String(other === button));
+ });
+ renderGallery();
});
- renderGallery();
});
- });
+ renderGallery();
+ }
galleryToggle.addEventListener('click', function () {
expanded = !expanded;
@@ -2925,7 +2993,19 @@
}
});
- renderGallery();
+ fetch(shotsGrid.dataset.galleryManifest, { credentials: 'same-origin' })
+ .then(function (response) {
+ if (!response.ok) { throw new Error('Gallery request failed'); }
+ return response.json();
+ })
+ .then(startGallery)
+ .catch(function () {
+ shotsGrid.innerHTML = '';
+ filters.innerHTML = '';
+ galleryToggle.hidden = true;
+ galleryEmpty.textContent = 'The project gallery is temporarily unavailable. Please try again soon.';
+ galleryEmpty.hidden = false;
+ });
/* ── Lightbox ───────────────────────────────────────────────────────── */
var lightbox = document.getElementById('lightbox');
@@ -2973,13 +3053,6 @@
if (lastFocused) { lastFocused.focus(); }
}
- shots.forEach(function (shot) {
- shot.addEventListener('click', function () {
- var index = visibleShots.indexOf(shot);
- if (index !== -1) { openLightbox(index); }
- });
- });
-
closeBtn.addEventListener('click', closeLightbox);
prevBtn.addEventListener('click', function () { showPhoto(activeIndex - 1); });
nextBtn.addEventListener('click', function () { showPhoto(activeIndex + 1); });