[object Object]

← back to Dw Pairs Well

collection-grouped: real sort (hue/style/colorways/az) in feed + show toolbar on load + Hue/Style options

99f1752cbdf21b76f9f119a73fd826e0a7b5cbf2 · 2026-06-26 07:24:30 -0700 · Steve

Files touched

Diff

commit 99f1752cbdf21b76f9f119a73fd826e0a7b5cbf2
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jun 26 07:24:30 2026 -0700

    collection-grouped: real sort (hue/style/colorways/az) in feed + show toolbar on load + Hue/Style options
---
 server.js | 46 ++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 44 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index a668e51..2158c5e 100644
--- a/server.js
+++ b/server.js
@@ -601,6 +601,32 @@ app.get('/api/colorways', async (req, res) => {
   }
 });
 
+// Hue angle (0-360) from a hex; grayscale/low-sat or missing → 999 so it sorts last.
+function hexHue(hex) {
+  if (!hex) return 999;
+  const m = String(hex).replace('#','').match(/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
+  if (!m) return 999;
+  const r = parseInt(m[1],16)/255, g = parseInt(m[2],16)/255, b = parseInt(m[3],16)/255;
+  const max = Math.max(r,g,b), min = Math.min(r,g,b), d = max - min;
+  if (d < 0.06) return 999;                      // grayscale → bucket at the end
+  let h;
+  if (max === r) h = ((g - b) / d) % 6;
+  else if (max === g) h = (b - r) / d + 2;
+  else h = (r - g) / d + 4;
+  h *= 60; if (h < 0) h += 360;
+  return h;
+}
+// First recognized design-style token from tags → for Style sort; none → 'zzz' (sorts last).
+const STYLE_WORDS = ['abstract','animal','botanical','damask','floral','geometric','ikat','moroccan',
+  'paisley','plaid','scenic','stripe','striped','toile','traditional','trellis','tropical','modern',
+  'contemporary','art deco','mid-century','victorian','transitional','grasscloth','textured','solid'];
+function styleOf(tags) {
+  if (!tags) return 'zzz';
+  const low = String(tags).toLowerCase();
+  for (const w of STYLE_WORDS) if (low.indexOf(w) !== -1) return w;
+  return 'zzz';
+}
+
 // GET /api/collection-grouped?handle=<collection-handle>&page=N&per=48
 //   The WS-2 grouped-grid feed. Returns ONE tile per pattern_id for the named
 //   collection, with a colorway count per tile, correct across pagination
@@ -616,6 +642,7 @@ app.get('/api/collection-grouped', async (req, res) => {
     const all = req.query.all === '1';
     const page = Math.max(1, parseInt(req.query.page, 10) || 1);
     const per = Math.max(1, Math.min(96, parseInt(req.query.per, 10) || 48));
+    const sort = String(req.query.sort || 'newest').slice(0, 24);
     if (!vendor && !all) {
       return res.status(400).json({ ok: false, error: 'pass ?vendor=<name> or ?all=1 (collection proxy for local testing)' });
     }
@@ -625,7 +652,7 @@ app.get('/api/collection-grouped', async (req, res) => {
     if (vendor) { params.push(vendor); where += ` AND sp.vendor = $${params.length}`; }
 
     const r = await pool.query(`
-      SELECT sp.dw_sku, sp.handle, sp.title, sp.vendor, sp.mfr_sku, sp.pattern_name,
+      SELECT sp.dw_sku, sp.handle, sp.title, sp.vendor, sp.mfr_sku, sp.pattern_name, sp.tags,
              sp.image_url, sp.metafields, sp.synced_at, sce.color_family, sce.dominant_hex
       FROM shopify_products sp
       LEFT JOIN shopify_color_enrichment sce ON sce.shopify_id = sp.shopify_id
@@ -636,6 +663,7 @@ app.get('/api/collection-grouped', async (req, res) => {
 
     // group server-side over the FULL set → counts are correct regardless of paging
     const groups = new Map();
+    let seq = 0;
     for (const row of r.rows) {
       const key = groupKeyForRow(row);
       let g = groups.get(key);
@@ -647,7 +675,10 @@ app.get('/api/collection-grouped', async (req, res) => {
           vendor: row.vendor || null,
           // representative tile = first (newest) member
           rep_handle: row.handle, rep_dw_sku: row.dw_sku, rep_image_url: row.image_url,
-          count: 0, swatches: []
+          count: 0, swatches: [],
+          _seq: seq++,                              // newest-first insertion order
+          _hue: hexHue(row.dominant_hex),           // for Color (Hue) sort
+          _style: styleOf(row.tags)                 // for Style sort
         };
         groups.set(key, g);
       }
@@ -656,6 +687,17 @@ app.get('/api/collection-grouped', async (req, res) => {
     }
 
     const arr = [...groups.values()];
+    // SORT the grouped tiles (over the whole set) before paging, so order is stable across pages.
+    const byPattern = (a, b) => String(a.pattern).localeCompare(String(b.pattern));
+    const sorters = {
+      newest:      (a, b) => a._seq - b._seq,
+      colors_desc: (a, b) => b.count - a.count || byPattern(a, b),
+      pattern_az:  byPattern,
+      vendor_az:   (a, b) => String(a.vendor||'').localeCompare(String(b.vendor||'')) || byPattern(a, b),
+      hue:         (a, b) => (a._hue - b._hue) || byPattern(a, b),       // grayscale (hue=999) sorts last
+      style:       (a, b) => String(a._style).localeCompare(String(b._style)) || byPattern(a, b)
+    };
+    arr.sort(sorters[sort] || sorters.newest);
     const total_tiles = arr.length;
     const start = (page - 1) * per;
     const slice = arr.slice(start, start + per);

← 901bbdd WS-3: /api/colorways + /api/collection-grouped (interim T2 g  ·  back to Dw Pairs Well  ·  collection-grouped feed: vendor-FAMILY scope (catches all br af1d385 →