← 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
M public/index.htmlM server.js
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 →