[object Object]

← back to Customdigitalmurals

reconcile: merge prod sort+density toolbar upgrade with the slug-leak fix (redactVendorSlug + dual-key route) — prod was ahead on sort, Mac2 ahead on fix; both now present. node -c OK

301d03d8ca091c3876eb1d633c238ed9c6ee9d84 · 2026-05-29 23:22:28 -0700 · Steve Abrams

Files touched

Diff

commit 301d03d8ca091c3876eb1d633c238ed9c6ee9d84
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 29 23:22:28 2026 -0700

    reconcile: merge prod sort+density toolbar upgrade with the slug-leak fix (redactVendorSlug + dual-key route) — prod was ahead on sort, Mac2 ahead on fix; both now present. node -c OK
---
 server.js | 94 +++++++++++++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 74 insertions(+), 20 deletions(-)

diff --git a/server.js b/server.js
index fa6db38..0c9670d 100644
--- a/server.js
+++ b/server.js
@@ -99,12 +99,60 @@ app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1d' }));
 // Health check
 app.get('/health', (_req, res) => res.json({ ok: true, count: ALL.length }));
 
+// ── DW STANDARD sort helpers ─────────────────────────────────────────────
+// color bucket — group similar hues from the title + tags + aesthetic
+function colorBucket(p) {
+  const n = (String(p.title || '') + ' ' + (p.tags || []).join(' ') + ' ' + String(p.aesthetic || '')).toLowerCase();
+  if (/white|ivory|cream|pearl|champagne|opal|oyster|parchment|linen|cloud|dove|fog|mist|snow|bleach/.test(n)) return 0; // white
+  if (/black|onyx|ebony|charcoal|carbon|graphite|midnight|jet/.test(n)) return 6; // black
+  if (/gray|grey|pewter|slate|silver|platinum|\bash\b|smoke|stone|shadow|steel/.test(n)) return 5; // gray
+  if (/green|sage|olive|moss|forest|jade|mint|seafoam|teal|emerald/.test(n)) return 2; // green
+  if (/blue|navy|\bsky\b|azure|cobalt|indigo|cerulean|aqua/.test(n)) return 3; // blue
+  if (/plum|mauve|purple|burgundy|blush|pink|lilac|violet|magenta|rose/.test(n)) return 4; // purple/pink
+  return 1; // warm/brown/tan/gold/rust/red/orange/yellow/etc.
+}
+// style bucket — group by stylistic family via tag/title match
+function styleBucket(p) {
+  const n = (String(p.title || '') + ' ' + (p.tags || []).join(' ') + ' ' + String(p.product_type || '')).toLowerCase();
+  if (/traditional|damask|toile|chinoiserie|classic|victorian|baroque|ornate/.test(n)) return 0;
+  if (/floral|botanical|leaf|leaves|flower|tropical|garden|foliage|vine/.test(n)) return 1;
+  if (/geometric|stripe|chevron|trellis|lattice|grid|abstract|art deco|modern|contemporary/.test(n)) return 2;
+  if (/animal|insect|bird|fauna|wildlife|scenic|landscape|mural/.test(n)) return 3;
+  return 4; // other / textural
+}
+function sortProducts(list, sort) {
+  const skuOf = (p) => String(p.handle || '');
+  const titleOf = (p) => String(p.title || '');
+  const priceOf = (p) => { const v = parseFloat(p.price); return isNaN(v) ? 0 : v; };
+  switch (sort) {
+    case 'newest':
+      return list; // server's natural order — no re-sort
+    case 'color':
+      return [...list].sort((a, b) => { const d = colorBucket(a) - colorBucket(b); return d !== 0 ? d : titleOf(a).localeCompare(titleOf(b)); });
+    case 'style':
+      return [...list].sort((a, b) => { const d = styleBucket(a) - styleBucket(b); return d !== 0 ? d : titleOf(a).localeCompare(titleOf(b)); });
+    case 'sku':
+      return [...list].sort((a, b) => skuOf(a).localeCompare(skuOf(b), undefined, { numeric: true }));
+    case 'title':
+    case 'az': // legacy alias
+      return [...list].sort((a, b) => titleOf(a).localeCompare(titleOf(b)));
+    case 'za': // legacy alias (Title Z→A) — kept for old localStorage values
+      return [...list].sort((a, b) => titleOf(b).localeCompare(titleOf(a)));
+    case 'price_asc':
+      return [...list].sort((a, b) => priceOf(a) - priceOf(b));
+    case 'price_desc':
+      return [...list].sort((a, b) => priceOf(b) - priceOf(a));
+    default:
+      return list; // unknown key → natural order
+  }
+}
+
 // API — search + filter
 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 sort  = req.query.sort || 'newest';
   const page  = Math.max(1, parseInt(req.query.page) || 1);
   const per   = Math.min(120, Math.max(12, parseInt(req.query.per) || 60));
 
@@ -125,11 +173,8 @@ app.get('/api/products', (req, res) => {
     results = results.filter(p => String(p.aesthetic || '').toLowerCase() === aesthetic);
   }
 
-  // Sort
-  if (sort === 'az')    results = [...results].sort((a, b) => a.title.localeCompare(b.title));
-  if (sort === 'za')    results = [...results].sort((a, b) => b.title.localeCompare(a.title));
-  if (sort === 'price_asc')  results = [...results].sort((a, b) => parseFloat(a.price||0) - parseFloat(b.price||0));
-  if (sort === 'price_desc') results = [...results].sort((a, b) => parseFloat(b.price||0) - parseFloat(a.price||0));
+  // Sort — DW STANDARD sort set (Newest is default / server's natural order)
+  results = sortProducts(results, sort);
 
   const total = results.length;
   const pages = Math.ceil(total / per);
@@ -348,14 +393,16 @@ body{background:var(--bg);color:var(--ink);font-family:'Space Grotesk',system-ui
 .hero-cta:hover{background:transparent;color:#fff;}
 
 /* ── Grid toolbar ── */
-.toolbar{border-bottom:1.5px solid var(--line);padding:16px 32px;display:flex;align-items:center;gap:16px;flex-wrap:wrap;background:var(--bg);}
-.toolbar input[type=text]{flex:1;min-width:200px;border:1.5px solid var(--line);background:var(--bg);color:var(--ink);font-family:inherit;font-size:13px;padding:9px 14px;outline:none;border-radius:0;}
+.toolbar{border-bottom:1.5px solid var(--line);padding:16px 32px;display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;background:var(--bg);}
+.toolbar .tb-sort{display:flex;align-items:center;gap:12px;flex:1;min-width:260px;}
+.toolbar .tb-density{display:flex;align-items:center;gap:12px;flex:1;min-width:260px;justify-content:flex-end;}
+.toolbar input[type=text]{flex:1;min-width:160px;border:1.5px solid var(--line);background:var(--bg);color:var(--ink);font-family:inherit;font-size:13px;padding:9px 14px;outline:none;border-radius:0;}
 .toolbar input[type=text]:focus{border-color:var(--ink);}
 .toolbar select{border:1.5px solid var(--line);background:var(--bg);color:var(--ink);font-family:inherit;font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;padding:9px 14px;outline:none;cursor:pointer;border-radius:0;}
 .toolbar select:focus{border-color:var(--ink);}
 .toolbar .count{font-size:11px;color:var(--ink-faint);letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;}
 .density-label{font-size:11px;color:var(--ink-faint);letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;}
-.density-slider{width:80px;accent-color:var(--ink);}
+.density-slider{cursor:pointer;flex:1;min-width:140px;max-width:200px;accent-color:var(--ink);}
 
 /* ── Product grid ── */
 .grid-wrap{padding:32px;}
@@ -507,16 +554,23 @@ ${headerHtml('home')}
 <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">
-    <option value="az">Title A → Z</option>
-    <option value="za">Title Z → A</option>
-    <option value="price_asc">Price: Low</option>
-    <option value="price_desc">Price: High</option>
-  </select>
-  <span class="density-label">Density</span>
-  <input type="range" id="density" class="density-slider" min="2" max="6" value="4" step="1" aria-label="Grid density">
-  <span class="count" id="count"></span>
+  <div class="tb-sort">
+    <input type="text" id="search" placeholder="Search by title, keyword..." autocomplete="off">
+    <select id="sort" aria-label="Sort products">
+      <option value="newest">Newest</option>
+      <option value="color">Color</option>
+      <option value="style">Style</option>
+      <option value="sku">SKU A → Z</option>
+      <option value="title">Title A → Z</option>
+      <option value="price_asc">Price ↑</option>
+      <option value="price_desc">Price ↓</option>
+    </select>
+  </div>
+  <div class="tb-density">
+    <span class="density-label">Density</span>
+    <input type="range" id="density" class="density-slider" min="2" max="6" value="4" step="1" aria-label="Grid density">
+    <span class="count" id="count"></span>
+  </div>
 </div>
 <div class="grid-wrap">
   <div class="product-grid" id="grid"></div>
@@ -534,7 +588,7 @@ ${footerHtml()}
 
 <script>
 var page=1, per=60, total=0, pages=0;
-var q='', sort='az', aesthetic='all';
+var q='', sort='newest', 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){}
 

← c0e645f fix: strip vendor tokens from product slug (redactVendorSlug  ·  back to Customdigitalmurals  ·  fix: strip bare '-arte' (Arte International abbrev) from pro 2047dc2 →