← 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
M public/index.htmlM server.js
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 => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[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 →