[object Object]

← back to Screenprintedwallpaper

wire up product sort: server handles sort param, client sends it, hydrate saved sort before first render, drop dead price options

53bab66969f5686cfe7c880bae96a356c089715a · 2026-05-18 20:52:38 -0700 · Steve Abrams

Files touched

Diff

commit 53bab66969f5686cfe7c880bae96a356c089715a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 18 20:52:38 2026 -0700

    wire up product sort: server handles sort param, client sends it, hydrate saved sort before first render, drop dead price options
---
 public/index.html | 15 ++++++++++++---
 server.js         | 30 +++++++++++++++++++++++++++++-
 2 files changed, 41 insertions(+), 4 deletions(-)

diff --git a/public/index.html b/public/index.html
index 977eddd..50f7590 100644
--- a/public/index.html
+++ b/public/index.html
@@ -315,8 +315,6 @@ textarea:focus-visible,
       <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>
     <label>Grid</label>
     <input type="range" id="densitySlider" min="4" max="12" step="1" value="6" aria-label="Grid columns">
@@ -427,7 +425,17 @@ textarea:focus-visible,
 </script>
 
 <script>
-const state = { q:'', facet:'all', page:1, pages:1, total:0, loading:false, exhausted:false };
+const state = { q:'', facet:'all', sort:'newest', page:1, pages:1, total:0, loading:false, exhausted:false };
+// Hydrate saved sort before the first grid render (wire-up IIFE runs in a later
+// <script> block, so without this the initial load ignores the persisted choice).
+try {
+  const savedSort = localStorage.getItem(location.hostname.replace(/\./g, '_') + '_sort');
+  if (savedSort) {
+    state.sort = savedSort;
+    const ss = document.getElementById('sortSelect');
+    if (ss) ss.value = savedSort;
+  }
+} catch (e) {}
 const LABELS = {"screen-print":"Screen print"};
 
 function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }
@@ -540,6 +548,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);
diff --git a/server.js b/server.js
index 26b7f3d..cae35df 100644
--- a/server.js
+++ b/server.js
@@ -60,8 +60,35 @@ require("./_universal-auth")(app, cfg.auth);
 app.locals.siteConfig = cfg;
 app.use(express.static(path.join(__dirname, 'public')));
 
+// Color buckets for the "Color" sort — group similar hues via tag-match.
+const COLOR_ORDER = ['white','cream','beige','grey','gray','black','brown','tan','gold','yellow','orange','red','pink','purple','blue','teal','green'];
+function colorRank(p) {
+  const tags = (p.tags || []).map(t => t.toLowerCase());
+  for (let i = 0; i < COLOR_ORDER.length; i++) {
+    if (tags.some(t => t.includes(COLOR_ORDER[i]))) return i;
+  }
+  return COLOR_ORDER.length;
+}
+
+function sortProducts(list, sort) {
+  if (!sort || sort === 'newest') return list; // server's natural order
+  const arr = list.slice();
+  switch (sort) {
+    case 'sku':
+      return arr.sort((a, b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
+    case 'title':
+      return arr.sort((a, b) => String(a.title || '').localeCompare(String(b.title || '')));
+    case 'color':
+      return arr.sort((a, b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
+    case 'style':
+      return arr.sort((a, b) => String(a.aesthetic || '').localeCompare(String(b.aesthetic || '')) || String(a.title || '').localeCompare(String(b.title || '')));
+    default:
+      return list;
+  }
+}
+
 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 +96,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));

← 4bcd9a5 hero-4grid: relocate json from data/ to public/ for Express  ·  back to Screenprintedwallpaper  ·  fix /api/facets total to report niche-filtered count, not pr 85e1479 →