[object Object]

← back to Stringwallpaper

sort-skill canonical: add light-dark/dark-light/wheel + helpers

117fd0a1d1adcfbd34fb0b83e05feaf09e688db0 · 2026-05-10 10:42:39 -0700 · Steve

Files touched

Diff

commit 117fd0a1d1adcfbd34fb0b83e05feaf09e688db0
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun May 10 10:42:39 2026 -0700

    sort-skill canonical: add light-dark/dark-light/wheel + helpers
---
 server.js | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 81 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index 5edf0c8..794c22a 100644
--- a/server.js
+++ b/server.js
@@ -37,6 +37,83 @@ const DROPPED = DATA_RAW.length - PRODUCTS.length;
 console.log(`Loaded ${DATA_RAW.length}, kept ${PRODUCTS.length}, dropped ${DROPPED}`);
 
 const app = express();
+// DW STANDARD: sort by color/style/sku/title for any product-rendering site.
+const COLOR_RANK = {
+  black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,khaki:9,copper:10,
+  red:20,pink:21,coral:22,orange:23,peach:24,salmon:25,
+  yellow:30,gold:31,mustard:32,olive:33,
+  green:40,lime:41,mint:42,teal:50,aqua:51,turquoise:52,cyan:53,
+  blue:60,navy:61,indigo:62,periwinkle:63,
+  purple:70,violet:71,lavender:72,plum:73,magenta:74,fuchsia:75,
+};
+function colorRank(p) {
+  const tags = (p.tags || []).map(t => String(t).toLowerCase());
+  for (const t of tags) for (const k of Object.keys(COLOR_RANK)) if (t.includes(k)) return COLOR_RANK[k];
+  return 999;
+}
+const STYLE_TAGS = ['traditional','transitional','modern','contemporary','minimalist','art deco','victorian','mid-century','mid century','rustic','industrial','farmhouse','bohemian','boho','geometric','floral','botanical','damask','toile','grasscloth','silk','linen','vinyl','natural'];
+function styleKey(p) {
+  const tags = (p.tags || []).map(t => String(t).toLowerCase());
+  for (const s of STYLE_TAGS) if (tags.some(t => t.includes(s))) return s;
+  return 'zzz';
+}
+
+const COLOR_HEX = {
+  black:'#0a0a0a',charcoal:'#2a2a2a',gray:'#888',grey:'#888',silver:'#c0c0c0',
+  white:'#fff',ivory:'#fffff0',cream:'#f5e9c8',champagne:'#f0d8a8',beige:'#e7d6b6',sand:'#dccfa6',blonde:'#e8d8a0',
+  brown:'#6b4a2b',chestnut:'#5a3825',umber:'#5a3a1f',tan:'#c9a36a',khaki:'#bda464',honey:'#d6a23c',
+  copper:'#b87333',brass:'#b5a642',bronze:'#9c6b30',gold:'#d4af37',amber:'#c98a32',butterscotch:'#d6943c',saffron:'#d6a93c',ochre:'#b88c3a',mustard:'#caa53d',
+  red:'#c92a2a',coral:'#e87a6b',pink:'#e6a4b4',salmon:'#e08e7c',rose:'#d68a9a',magenta:'#c5358a',fuchsia:'#d63aaf',
+  orange:'#e07a32',peach:'#f0bb96',apricot:'#e0a373',
+  yellow:'#e8c84a',butter:'#f0e1a0',
+  olive:'#7a7a36','olive ':'#7a7a36',
+  green:'#3d8a4f',lime:'#92c84a',mint:'#aedcc1',
+  teal:'#2a8a8a',aqua:'#3ab4b4',turquoise:'#3ab8b0',cyan:'#3acac4',aquamarine:'#9ed5c8',
+  blue:'#3a5fb8',navy:'#1a2c52',indigo:'#37327a',periwinkle:'#8a93d4',
+  purple:'#6a3a8a',violet:'#7a4aa8',lavender:'#b0a3d6',plum:'#693a55',gilver:'#bfb9b3'
+};
+function rgbOfTag(tag) {
+  const k = String(tag||'').toLowerCase().trim();
+  for (const name of Object.keys(COLOR_HEX)) if (k.includes(name)) return COLOR_HEX[name];
+  return null;
+}
+function hexToRgb(h){const m=/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(h||'');return m?[parseInt(m[1],16),parseInt(m[2],16),parseInt(m[3],16)]:null;}
+function rgbToHsl(r,g,b){r/=255;g/=255;b/=255;const mx=Math.max(r,g,b),mn=Math.min(r,g,b);let h=0,s=0,l=(mx+mn)/2;if(mx!==mn){const d=mx-mn;s=l>.5?d/(2-mx-mn):d/(mx+mn);switch(mx){case r:h=(g-b)/d+(g<b?6:0);break;case g:h=(b-r)/d+2;break;case b:h=(r-g)/d+4;break;}h*=60;}return[h,s,l];}
+function dominantHex(p){
+  if (p.dominant_hex_real && /^#[\da-f]{6}$/i.test(p.dominant_hex_real)) return p.dominant_hex_real;
+  const tags=(p.tags||[]).map(t=>String(t).toLowerCase());
+  for (const t of tags){const hx=rgbOfTag(t);if(hx)return hx;}
+  return '#888';
+}
+// Up to 4 distinct color dots: real-pixel hex first, then tag-derived hexes.
+function colorDots(p){
+  const seen=new Set();const out=[];
+  if (p.dominant_hex_real && /^#[\da-f]{6}$/i.test(p.dominant_hex_real)) {
+    out.push(p.dominant_hex_real); seen.add(p.dominant_hex_real.toLowerCase());
+  }
+  for (const t of (p.tags||[])){
+    const hx=rgbOfTag(t);
+    if(hx && !seen.has(hx.toLowerCase())){seen.add(hx.toLowerCase());out.push(hx);if(out.length>=4)break;}
+  }
+  return out;
+}
+function luminance(p){const rgb=hexToRgb(dominantHex(p));return rgb?(0.2126*rgb[0]+0.7152*rgb[1]+0.0722*rgb[2]):128;}
+function hue(p){const rgb=hexToRgb(dominantHex(p));if(!rgb)return 999;const [h,s]=rgbToHsl(rgb[0],rgb[1],rgb[2]);return s<0.08?999:h;}
+
+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 === 'light-dark') return [...list].sort((a,b) => luminance(b) - luminance(a)); // bright first
+  if (mode === 'dark-light') return [...list].sort((a,b) => luminance(a) - luminance(b)); // dark first
+  if (mode === 'wheel') return [...list].sort((a,b) => hue(a) - hue(b));
+  if (mode === 'price-asc')  return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
+  if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
+  return list;
+}
+
+
 app.use(express.json({ limit: "256kb" }));
 const cfg = require('../_shared/site-config').load(__dirname);
 require("./_universal-contact")(app, cfg.contact);
@@ -44,7 +121,7 @@ require("./_universal-auth")(app, cfg.auth);
 app.use(express.static(path.join(__dirname, 'public')));
 
 app.get('/api/products', (req, res) => {
-  const { q, aesthetic, vendor, page = 1, limit = 24 } = req.query;
+  const { q, aesthetic, vendor, page = 1, limit = 24, sort = 'newest'} = req.query;
   let list = PRODUCTS;
   if (q) {
     const needle = q.toLowerCase();
@@ -52,11 +129,13 @@ 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) || 1);
   const lim = Math.min(60, parseInt(limit) || 24);
   const start = (pageNum - 1) * lim;
-  res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total / lim), items: list.slice(start, start + lim) });
+  res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total/lim), sort, items: list.slice(start, start + lim) });
+  // (replaced) old: pages: Math.ceil(total / lim), items: list.slice(start, start + lim) });
 });
 
 app.get('/api/sliders', (req, res) => {

← 06884b7 initial scaffold (gitify-all 2026-05-06)  ·  back to Stringwallpaper  ·  sort-skill UI: add Light→Dark / Dark→Light / Color Wheel opt 3ba14bf →