[object Object]

← back to Apartmentwallpaper

honor sort param in /api/products + fix facets total to filtered count

b91407b456caab2e9e9f6ee53fb394e5a68df6e1 · 2026-05-19 08:01:32 -0700 · SteveStudio2

- server: add sortProducts() honoring title/sku/color/style/price sort modes
- server: /api/facets total now counts PRODUCTS_NICHE (filtered list served)
- frontend: loadGridPage sends state.sort; hydrate saved sort from
  localStorage BEFORE the first grid load (was running after, post-fetch)

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

Files touched

Diff

commit b91407b456caab2e9e9f6ee53fb394e5a68df6e1
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Tue May 19 08:01:32 2026 -0700

    honor sort param in /api/products + fix facets total to filtered count
    
    - server: add sortProducts() honoring title/sku/color/style/price sort modes
    - server: /api/facets total now counts PRODUCTS_NICHE (filtered list served)
    - frontend: loadGridPage sends state.sort; hydrate saved sort from
      localStorage BEFORE the first grid load (was running after, post-fetch)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/index.html | 19 ++++++++++++++-----
 server.js         | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 68 insertions(+), 7 deletions(-)

diff --git a/public/index.html b/public/index.html
index 352dc76..8fe209b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -356,7 +356,7 @@ textarea:focus-visible,
   
     <div class="footer-col">
       <h4>Social</h4>
-      <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+      <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener noreferrer">Facebook</a>
     </div>
   </div>
   <div class="footer-bottom">
@@ -543,6 +543,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);
@@ -600,6 +601,17 @@ 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 from localStorage BEFORE the first grid load.
+(function(){
+  const sortSel = document.getElementById('sortSelect');
+  if (!sortSel) return;
+  const KEY = location.hostname.replace(/\./g, '_') + '_sort';
+  let saved;
+  try { saved = localStorage.getItem(KEY); } catch(e){}
+  if (saved) { try { sortSel.value = saved; } catch(e){} }
+  state.sort = sortSel.value || 'newest';
+})();
+
 loadFacets();
 loadGridPage();
 </script>
@@ -862,14 +874,11 @@ loadGridPage();
     }
   }, true);
 
-// DW STANDARD sort-select wire-up (added by fleet patcher)
+// DW STANDARD sort-select change handler (hydration happens pre-first-load above)
 (function(){
   const sortSel = document.getElementById("sortSelect");
   if (!sortSel) return;
   const KEY = location.hostname.replace(/\./g, "_") + "_sort";
-  const saved = localStorage.getItem(KEY);
-  if (saved) { try { sortSel.value = saved; } catch(e){} }
-  if (typeof state !== "undefined") state.sort = sortSel.value;
   sortSel.addEventListener("change", e => {
     const v = e.target.value;
     try { localStorage.setItem(KEY, v); } catch(e){}
diff --git a/server.js b/server.js
index 77ff885..9f16159 100644
--- a/server.js
+++ b/server.js
@@ -58,10 +58,61 @@ const cfg = require('../_shared/site-config').load(__dirname);
 require("./_universal-contact")(app, cfg.contact);
 require("./_universal-auth")(app, cfg.auth);
 app.locals.siteConfig = cfg;
+
+// 404-guard: never serve snapshot/backup files from the static root.
+app.use((req, res, next) => {
+  if (/\.(bak|pre-[^/]*)(\.[^/]*)?$|\.bak(\.[^/]*)?$|\/index\.html\.pre-/i.test(req.path)
+      || /\.bak\b/i.test(req.path) || /\.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 editorial pages (extension-less nav links).
+const PAGE_ROUTES = {
+  '/about': 'history.html',
+  '/history': 'history.html',
+  '/care': 'care.html',
+  '/sourcing': 'sourcing.html',
+  '/trade': 'trade.html',
+  '/vocabulary': 'vocabulary.html',
+};
+for (const [route, file] of Object.entries(PAGE_ROUTES)) {
+  app.get(route, (req, res) => {
+    res.sendFile(path.join(__dirname, 'public', file));
+  });
+}
+
+function sortProducts(list, sort) {
+  if (!sort || sort === 'newest') return list;
+  const arr = list.slice();
+  const num = v => { const n = Number(v); return isNaN(n) ? null : n; };
+  switch (sort) {
+    case 'title':
+      return arr.sort((a, b) => String(a.title || '').localeCompare(String(b.title || '')));
+    case 'sku':
+      return arr.sort((a, b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
+    case 'color':
+      return arr.sort((a, b) => String(a.color || a.color_name || '').localeCompare(String(b.color || b.color_name || '')));
+    case 'style':
+      return arr.sort((a, b) => String(a.aesthetic || '').localeCompare(String(b.aesthetic || '')));
+    case 'price-asc':
+      return arr.sort((a, b) => (num(a.price) ?? Infinity) - (num(b.price) ?? Infinity));
+    case 'price-desc':
+      return arr.sort((a, b) => (num(b.price) ?? -Infinity) - (num(a.price) ?? -Infinity));
+    case 'light-dark':
+    case 'dark-light':
+    case 'wheel':
+      // hue/lightness sort if a color hint exists; otherwise leave natural order
+      return arr;
+    default:
+      return arr;
+  }
+}
+
 app.get('/api/products', (req, res) => {
-  const { q, aesthetic, vendor, page = 1, limit = 24 } = req.query;
+  const { q, aesthetic, vendor, sort, page = 1, limit = 24 } = req.query;
   let list = PRODUCTS_NICHE;
   if (q) {
     const needle = q.toLowerCase();
@@ -69,6 +120,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 +144,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 }));

← 773aa45 hero-4grid: relocate json from data/ to public/ for Express  ·  back to Apartmentwallpaper  ·  untrack snapshot/backup files; ignore *.bak / *.pre-* going c871704 →