patx/frontlineboats

add editable gallery manifest

Commit 0932241 · patx · 2026-07-30T22:52:18-04:00

Changeset
0932241d301cce5967ce8ab369f8753280bd663c
Parents
84be43b0dc207ef875cd1bedb2f45bca412ded3b

View source at this commit

Comments

No comments yet.

Log in to comment

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 &amp; 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 &amp; 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); });