← 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 →