[object Object]

← back to Customdigitalmurals

homepage rails: 6 aesthetic rail sections + pills strip

0aa321f9ca15bd7005e0fe99ca7c60795d53e848 · 2026-05-26 08:14:02 -0700 · Steve

Adds /api/sliders endpoint, aesthetic filter on /api/products, aesthetic
counts on /api/facets. homePage() now renders 4 rail sections (green/
botanical/commercial/blue, 8 cards each, View all →) above the shop +
horizontal pill strip wired to setAesthetic(). Click any pill or 'View all'
re-renders the grid filtered by that aesthetic and scrolls to #shop.
Pattern from hollywood-wallcoverings 3a7da85 + blockprintedwallpaper.

Bucket distribution: green 1638 / botanical 1537 / commercial 422 / blue
24 / white 20 / architectural 14 (all 6 rails fire, all >= 4 items).

Files touched

Diff

commit 0aa321f9ca15bd7005e0fe99ca7c60795d53e848
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue May 26 08:14:02 2026 -0700

    homepage rails: 6 aesthetic rail sections + pills strip
    
    Adds /api/sliders endpoint, aesthetic filter on /api/products, aesthetic
    counts on /api/facets. homePage() now renders 4 rail sections (green/
    botanical/commercial/blue, 8 cards each, View all →) above the shop +
    horizontal pill strip wired to setAesthetic(). Click any pill or 'View all'
    re-renders the grid filtered by that aesthetic and scrolls to #shop.
    Pattern from hollywood-wallcoverings 3a7da85 + blockprintedwallpaper.
    
    Bucket distribution: green 1638 / botanical 1537 / commercial 422 / blue
    24 / white 20 / architectural 14 (all 6 rails fire, all >= 4 items).
---
 server.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 94 insertions(+), 3 deletions(-)

diff --git a/server.js b/server.js
index ad682d5..04fba2c 100644
--- a/server.js
+++ b/server.js
@@ -73,6 +73,7 @@ app.get('/health', (_req, res) => res.json({ ok: true, count: ALL.length }));
 app.get('/api/products', (req, res) => {
   const q     = (req.query.q || '').toLowerCase().trim();
   const vendor = (req.query.vendor || '').toLowerCase().trim();
+  const aesthetic = (req.query.aesthetic || '').toLowerCase().trim();
   const sort  = req.query.sort || 'az';
   const page  = Math.max(1, parseInt(req.query.page) || 1);
   const per   = Math.min(120, Math.max(12, parseInt(req.query.per) || 60));
@@ -90,6 +91,9 @@ app.get('/api/products', (req, res) => {
   if (vendor) {
     results = results.filter(p => (p.vendor || '').toLowerCase() === vendor);
   }
+  if (aesthetic && aesthetic !== 'all') {
+    results = results.filter(p => String(p.aesthetic || '').toLowerCase() === aesthetic);
+  }
 
   // Sort
   if (sort === 'az')    results = [...results].sort((a, b) => a.title.localeCompare(b.title));
@@ -104,6 +108,16 @@ app.get('/api/products', (req, res) => {
   res.json({ total, pages, page, per, products: slice });
 });
 
+// Aesthetic rails — used by homepage rail-sections render.
+app.get('/api/sliders', (_req, res) => {
+  const out = [];
+  for (const a of SITE_RAILS) {
+    const items = ALL.filter(p => String(p.aesthetic || '').toLowerCase() === a.toLowerCase()).slice(0, 12);
+    if (items.length >= 4) out.push({ aesthetic: a, items });
+  }
+  res.json({ rails: out });
+});
+
 // Vendor list (LEGACY — kept for back-compat; new code should use /api/facets).
 // Vendor names are admin-only metadata and must NOT render on customer-facing UI.
 app.get('/api/vendors', (_req, res) => {
@@ -138,9 +152,11 @@ app.get('/api/facets', (req, res) => {
 
   const vendors = {};
   const productTypes = {};
+  const aesthetics = {};
   filtered.forEach(p => {
     if (p.vendor) vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
     if (p.product_type) productTypes[p.product_type] = (productTypes[p.product_type] || 0) + 1;
+    if (p.aesthetic) aesthetics[String(p.aesthetic).toLowerCase()] = (aesthetics[String(p.aesthetic).toLowerCase()] || 0) + 1;
   });
 
   const toList = obj => Object.entries(obj)
@@ -151,6 +167,7 @@ app.get('/api/facets', (req, res) => {
     total: filtered.length,
     vendors: toList(vendors),
     product_types: toList(productTypes),
+    aesthetics,
   });
 });
 
@@ -427,7 +444,29 @@ ${headerHtml('home')}
   </div>
 </section>
 
+<!-- Aesthetic rails (added 2026-05-26 fleet rails sweep) -->
+<style>
+  .cdm-rails { max-width: 1400px; margin: 0 auto; padding: 48px 32px 0; }
+  .rail-section { margin-bottom: 56px; }
+  .rail-head { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:18px; padding-bottom:10px; border-bottom:1px solid var(--line); }
+  .rail-title-h { font-size: 22px; letter-spacing: .18em; text-transform: uppercase; font-weight: 700; margin: 0; color: var(--ink); }
+  .rail-all { font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-soft); font-weight: 600; cursor: pointer; background: transparent; border: 0; padding: 0; font-family: inherit; }
+  .rail-all:hover { color: var(--ink); }
+  .rail-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 18px; }
+  .rail-card { display: block; aspect-ratio: 1/1; overflow: hidden; position: relative; cursor: pointer; }
+  .rail-card img { width: 100%; height: 100%; object-fit: cover; object-position: center 12%; transition: transform .4s ease; }
+  .rail-card:hover img { transform: scale(1.04); }
+  .rail-card-label { position: absolute; bottom: 0; left: 0; right: 0; padding: 12px 14px; background: linear-gradient(to top, rgba(0,0,0,0.7), transparent); color: #fff; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
+  .cdm-pills { display: flex; gap: 10px; flex-wrap: wrap; padding: 18px 32px 0; max-width: 1400px; margin: 0 auto; }
+  .cdm-pill { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-size: 11px; letter-spacing: .16em; text-transform: uppercase; font-weight: 600; cursor: pointer; border-radius: 0; transition: border-color .15s, color .15s, background .15s; }
+  .cdm-pill:hover { border-color: var(--ink); color: var(--ink); }
+  .cdm-pill.active { border-color: var(--ink); color: var(--bg); background: var(--ink); }
+  .cdm-pill span { font-size: 10px; opacity: .7; font-weight: 700; }
+</style>
+<section class="cdm-rails" id="railSections" aria-label="Browse by aesthetic"></section>
+
 <div id="shop">
+<nav class="cdm-pills" id="cdmPills" aria-label="Filter by aesthetic"></nav>
 <div class="toolbar">
   <input type="text" id="search" placeholder="Search by title, keyword..." autocomplete="off">
   <select id="sort">
@@ -456,8 +495,9 @@ ${footerHtml()}
 
 <script>
 var page=1, per=60, total=0, pages=0;
-var q='', sort='az';
-var LS_SORT='cdm-sort', LS_DENSITY='cdm-density';
+var q='', sort='az', aesthetic='all';
+var LS_SORT='cdm-sort', LS_DENSITY='cdm-density', LS_AESTHETIC='cdm-aesthetic';
+try { var savedA = localStorage.getItem(LS_AESTHETIC); if (savedA) aesthetic = savedA; } catch(e){}
 
 // Hydrate saved sort + density BEFORE the first grid load.
 try {
@@ -483,7 +523,7 @@ document.getElementById('density').addEventListener('input',function(){
 setCols(savedCols);
 
 function load(){
-  var url='/api/products?page='+page+'&per='+per+'&q='+encodeURIComponent(q)+'&sort='+sort;
+  var url='/api/products?page='+page+'&per='+per+'&q='+encodeURIComponent(q)+'&sort='+sort+'&aesthetic='+encodeURIComponent(aesthetic);
   fetch(url).then(r=>r.json()).then(data=>{
     total=data.total; pages=data.pages;
     document.getElementById('count').textContent=total.toLocaleString()+' murals';
@@ -526,6 +566,57 @@ document.getElementById('sort').addEventListener('change',function(){
   load();
 });
 
+// ── Aesthetic rails + pills (fleet rails sweep 2026-05-26) ───────────────
+function setAesthetic(a){
+  aesthetic = a || 'all';
+  try { localStorage.setItem(LS_AESTHETIC, aesthetic); } catch(e){}
+  page = 1;
+  document.querySelectorAll('#cdmPills .cdm-pill').forEach(function(p){
+    p.classList.toggle('active', p.dataset.aesthetic === aesthetic);
+  });
+  load();
+  var shop = document.getElementById('shop');
+  if (shop) shop.scrollIntoView({ behavior: 'smooth', block: 'start' });
+}
+function renderPills(facets){
+  var bar = document.getElementById('cdmPills');
+  if (!bar) return;
+  var aesth = (facets && facets.aesthetics) || {};
+  var total = (facets && facets.total) || 0;
+  var entries = Object.entries(aesth).filter(function(e){ return e[0] && e[1]>=4; }).sort(function(a,b){ return b[1]-a[1]; }).slice(0,8);
+  var html = '<button type="button" class="cdm-pill'+(aesthetic==='all'?' active':'')+'" data-aesthetic="all">All <span>'+total.toLocaleString()+'</span></button>';
+  entries.forEach(function(e){
+    html += '<button type="button" class="cdm-pill'+(aesthetic===e[0]?' active':'')+'" data-aesthetic="'+esc(e[0])+'">'+esc(e[0].charAt(0).toUpperCase()+e[0].slice(1))+' <span>'+e[1].toLocaleString()+'</span></button>';
+  });
+  bar.innerHTML = html;
+  bar.querySelectorAll('.cdm-pill').forEach(function(p){
+    p.addEventListener('click', function(){ setAesthetic(p.dataset.aesthetic); });
+  });
+}
+function renderRailSections(rails){
+  var host = document.getElementById('railSections');
+  if (!host || !rails || !rails.length) return;
+  host.innerHTML = rails.slice(0,4).map(function(r){
+    var cards = (r.items||[]).slice(0,8).map(function(p){
+      var img = p.images && p.images[0] ? p.images[0] : '';
+      var href = '/product/'+encodeURIComponent(p.handle||'');
+      return '<a class="rail-card" href="'+esc(href)+'">'+(img?'<img src="'+esc(img)+'" alt="'+esc(p.title||'')+'" loading="lazy">':'')+'<span class="rail-card-label">'+esc(p.title||'')+'</span></a>';
+    }).join('');
+    var label = r.aesthetic.charAt(0).toUpperCase()+r.aesthetic.slice(1);
+    return '<section class="rail-section"><div class="rail-head"><h2 class="rail-title-h">'+esc(label)+'</h2><button type="button" class="rail-all" data-aesthetic="'+esc(r.aesthetic)+'">View all →</button></div><div class="rail-grid">'+cards+'</div></section>';
+  }).join('');
+  host.querySelectorAll('.rail-all').forEach(function(b){
+    b.addEventListener('click', function(){ setAesthetic(b.dataset.aesthetic); });
+  });
+}
+Promise.all([
+  fetch('/api/facets').then(function(r){return r.json();}).catch(function(){return null;}),
+  fetch('/api/sliders').then(function(r){return r.json();}).catch(function(){return null;})
+]).then(function(rs){
+  if (rs[0]) renderPills(rs[0]);
+  if (rs[1] && rs[1].rails) renderRailSections(rs[1].rails);
+});
+
 load();
 </script>
 </body></html>`;

← 55825b0 wire api-vendor-redact middleware (close vendors-facet leak)  ·  back to Customdigitalmurals  ·  scrub vendor name leaks from products.json source (3132 prod c8239a8 →