[object Object]

← back to Selfadhesivewallpaper

wire sort param end-to-end + count niche total in /api/facets

a7835f51a93dfa28f52c66674d5988d2a04c6903 · 2026-05-19 07:52:52 -0700 · Steve Abrams

- server: add sortProducts() honoring ?sort= (sku/title/color/style/price)
- server: /api/products applies sort; /api/facets total = PRODUCTS_NICHE.length
- frontend: loadGridPage sends state.sort; hydrate saved sort before first load

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a7835f51a93dfa28f52c66674d5988d2a04c6903
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 19 07:52:52 2026 -0700

    wire sort param end-to-end + count niche total in /api/facets
    
    - server: add sortProducts() honoring ?sort= (sku/title/color/style/price)
    - server: /api/products applies sort; /api/facets total = PRODUCTS_NICHE.length
    - frontend: loadGridPage sends state.sort; hydrate saved sort before first load
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/index.html | 14 ++++++++++++++
 server.js         | 45 +++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index 98985f6..65a1ec8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -540,6 +540,7 @@ async function loadGridPage() {
   const params = new URLSearchParams({ page:state.page, limit:24 });
   if (state.q) params.set('q', state.q);
   if (state.facet !== 'all') params.set('aesthetic', state.facet);
+  if (state.sort && state.sort !== 'newest') params.set('sort', state.sort);
   let data;
   try {
     const r = await fetch('/api/products?' + params);
@@ -597,6 +598,19 @@ function setTheme(t){ document.documentElement.dataset.theme = t; try { localSto
 setTheme(document.documentElement.dataset.theme || 'light');
 tb.addEventListener('click', () => setTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'));
 
+// Hydrate saved sort BEFORE the first grid load so a returning visitor's
+// chosen order is applied on initial render (not just after a change event).
+(function(){
+  try {
+    const sortSel = document.getElementById('sortSelect');
+    const saved = localStorage.getItem(location.hostname.replace(/\./g, '_') + '_sort');
+    if (saved) {
+      state.sort = saved;
+      if (sortSel) sortSel.value = saved;
+    }
+  } catch(e){}
+})();
+
 loadFacets();
 loadGridPage();
 </script>
diff --git a/server.js b/server.js
index 294f45b..c7ca2f3 100644
--- a/server.js
+++ b/server.js
@@ -52,16 +52,56 @@ const PRODUCTS_ORIG = PRODUCTS;
 Object.defineProperty(global, '__products_niche_applied', { value: true });
 
 
+// DW STANDARD sort — honors the ?sort= param sent by the frontend select.
+const COLOR_ORDER = ['white','cream','beige','tan','brown','gold','yellow','orange','red','pink','purple','blue','green','grey','gray','silver','black'];
+function colorRank(p) {
+  const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
+  for (let i = 0; i < COLOR_ORDER.length; i++) if (blob.includes(COLOR_ORDER[i])) return i;
+  return COLOR_ORDER.length;
+}
+function styleKey(p) {
+  const blob = ((p.aesthetic || '') + ' ' + (p.title || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
+  const STYLES = ['traditional','modern','floral','damask','geometric','abstract','botanical','chinoiserie','natural','striped'];
+  for (const s of STYLES) if (blob.includes(s)) return s;
+  return p.aesthetic || 'zzz';
+}
+function sortProducts(list, mode) {
+  if (mode === 'sku')   return [...list].sort((a,b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
+  if (mode === 'title') return [...list].sort((a,b) => String(a.title || '').localeCompare(String(b.title || '')));
+  if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
+  if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
+  if (mode === 'price-asc')  return [...list].sort((a,b) => (Number(a.price) || 0) - (Number(b.price) || 0));
+  if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.price) || 0) - (Number(a.price) || 0));
+  return list; // 'newest' = server's natural order
+}
+
 const app = express();
 app.use(express.json({ limit: "256kb" }));
 const cfg = require('../_shared/site-config').load(__dirname);
 require("./_universal-contact")(app, cfg.contact);
 require("./_universal-auth")(app, cfg.auth);
 app.locals.siteConfig = cfg;
+
+// Guard: never serve snapshot/backup files from the static root.
+app.use((req, res, next) => {
+  if (/\.(bak|pre-[^/]*)(\.[^/]*)?$/i.test(req.path) || /\.(bak|pre-)/i.test(req.path)) {
+    return res.status(404).send('Not found');
+  }
+  next();
+});
 app.use(express.static(path.join(__dirname, 'public')));
 
+// Clean-URL routes for extension-less nav links.
+const CLEAN_PAGES = { '/about': 'history.html', '/history': 'history.html', '/care': 'care.html', '/trade': 'trade.html', '/sourcing': 'sourcing.html', '/vocabulary': 'vocabulary.html' };
+for (const [route, file] of Object.entries(CLEAN_PAGES)) {
+  app.get(route, (req, res, next) => {
+    const fp = path.join(__dirname, 'public', file);
+    fs.access(fp, fs.constants.R_OK, err => err ? next() : res.sendFile(fp));
+  });
+}
+
 app.get('/api/products', (req, res) => {
-  const { q, aesthetic, vendor, page = 1, limit = 24 } = req.query;
+  const { q, aesthetic, vendor, sort = 'newest', page = 1, limit = 24 } = req.query;
   let list = PRODUCTS_NICHE;
   if (q) {
     const needle = q.toLowerCase();
@@ -69,6 +109,7 @@ app.get('/api/products', (req, res) => {
   }
   if (aesthetic && aesthetic !== 'all') list = list.filter(p => p.aesthetic === aesthetic);
   if (vendor && vendor !== 'all') list = list.filter(p => p.vendor === vendor);
+  list = sortProducts(list, sort);
   const total = list.length;
   const pageNum = Math.max(1, parseInt(page));
   const lim = Math.min(60, parseInt(limit));
@@ -92,7 +133,7 @@ app.get('/api/facets', (req, res) => {
     aesthetics[p.aesthetic] = (aesthetics[p.aesthetic] || 0) + 1;
     vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
   }
-  res.json({ aesthetics, vendors, total: PRODUCTS.length });
+  res.json({ aesthetics, vendors, total: PRODUCTS_NICHE.length });
 });
 
 app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS_NICHE.length, dropped: DROPPED }));

← b391155 hero-4grid: relocate json from data/ to public/ for Express  ·  back to Selfadhesivewallpaper  ·  untrack snapshot backups + broaden .gitignore c3533c4 →