[object Object]

← back to Wallsandfabrics

fix(api): /api/facets drill-down — count each surface over the set narrowed by the OTHER filters

d0c745ebe57346c246d0797d28a79a8ccc05252d · 2026-06-01 07:57:45 -0700 · Steve

Add shared filterProducts(list,{q,surface},skip) so /api/products and /api/facets
narrow identically. Facets now count each surface facet over the q-narrowed set
(skip 'surface') so picking one surface no longer zeros sibling-surface counts;
total = fully-filtered length. /api/products refactored to reuse it (same behaviour).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit d0c745ebe57346c246d0797d28a79a8ccc05252d
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jun 1 07:57:45 2026 -0700

    fix(api): /api/facets drill-down — count each surface over the set narrowed by the OTHER filters
    
    Add shared filterProducts(list,{q,surface},skip) so /api/products and /api/facets
    narrow identically. Facets now count each surface facet over the q-narrowed set
    (skip 'surface') so picking one surface no longer zeros sibling-surface counts;
    total = fully-filtered length. /api/products refactored to reuse it (same behaviour).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 server.js | 55 +++++++++++++++++++++++++++++++++----------------------
 1 file changed, 33 insertions(+), 22 deletions(-)

diff --git a/server.js b/server.js
index ecfd820..c27fcc5 100644
--- a/server.js
+++ b/server.js
@@ -172,20 +172,32 @@ function sortProducts(list, mode) {
 }
 
 
+// Shared filter so /api/products and /api/facets narrow identically. `skip`
+// names a dimension to leave un-applied — used by /api/facets to count each
+// facet over the set narrowed by the OTHER active filters (drill-down), so
+// picking one surface doesn't zero the sibling-surface counts.
+function filterProducts(list, { q, surface } = {}, skip) {
+  let out = list;
+  if (skip !== 'q' && q) {
+    const needle = String(q).toLowerCase();
+    out = out.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => String(t).toLowerCase().includes(needle)));
+  }
+  if (skip !== 'surface' && surface && surface !== 'all') {
+    out = out.filter(p => p.__surface === surface);
+  }
+  return out;
+}
+
 app.get('/api/products', (req, res) => {
   // ?vendor= is stripped upstream by _shared/api-vendor-redact and search no
   // longer indexes the vendor field — DW vendor names stay off customer surfaces.
   const { q, surface, sort = 'newest', page = 1, limit = 24 } = req.query;
-  let list = PRODUCTS;
-  if (q) {
-    const needle = q.toLowerCase();
-    list = list.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => t.toLowerCase().includes(needle)));
-  }
-  // Compute surface counts from the q-filtered (but not surface-filtered) list so
-  // the surface tabs in the frontend reflect how many matches exist per surface for
-  // the active search query, not the whole unfiltered catalog.
-  const filteredSurfaces = list.reduce((acc, p) => { acc[p.__surface] = (acc[p.__surface] || 0) + 1; return acc; }, {});
-  if (surface && surface !== 'all') list = list.filter(p => p.__surface === surface);
+  // Surface tabs: count each surface over the q-narrowed set (skip 'surface')
+  // so the tab counts reflect matches per surface for the active query, not the
+  // whole catalog — and don't collapse to the one selected surface.
+  const filteredSurfaces = filterProducts(PRODUCTS, { q, surface }, 'surface')
+    .reduce((acc, p) => { acc[p.__surface] = (acc[p.__surface] || 0) + 1; return acc; }, {});
+  let list = filterProducts(PRODUCTS, { q, surface });
   list = sortProducts(list, sort);
   const total = list.length;
   const pageNum = Math.max(1, parseInt(page) || 1);
@@ -195,20 +207,19 @@ app.get('/api/products', (req, res) => {
 });
 
 app.get('/api/facets', (req, res) => {
-  // Honour the same filters /api/products honours so the surface counts +
-  // total reflect the FILTERED list (not the whole catalog). vendor filter
-  // is dropped upstream by api-vendor-redact middleware.
+  // Filter-aware drill-down: each facet is counted over the set narrowed by the
+  // OTHER active filters (per-dimension skip) so selecting one surface doesn't
+  // zero the sibling-surface counts. total = the fully-filtered length. vendor
+  // filter is dropped upstream by api-vendor-redact middleware.
   const { q, surface } = req.query;
-  let list = PRODUCTS;
-  if (q) {
-    const needle = String(q).toLowerCase();
-    list = list.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => String(t).toLowerCase().includes(needle)));
-  }
-  if (surface && surface !== 'all') list = list.filter(p => p.__surface === surface);
-  const surfaces = list.reduce((acc, p) => { acc[p.__surface] = (acc[p.__surface] || 0) + 1; return acc; }, {});
+  // surfaces: count over the q-narrowed set, skipping the surface filter itself.
+  const surfaces = filterProducts(PRODUCTS, { q, surface }, 'surface')
+    .reduce((acc, p) => { acc[p.__surface] = (acc[p.__surface] || 0) + 1; return acc; }, {});
+  // vendors: count over the fully-filtered set (kept for parity; redacted in UI).
+  const filtered = filterProducts(PRODUCTS, { q, surface });
   const vendors = {};
-  for (const p of list) vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
-  res.json({ surfaces, vendors, total: list.length });
+  for (const p of filtered) vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
+  res.json({ surfaces, vendors, total: filtered.length });
 });
 
 app.get('/api/sliders', (req, res) => {

← bac7b3c scrub residual 3rd-party vendor names from products.json sou  ·  back to Wallsandfabrics  ·  Remove Big Red widget — never UX-worked, collided with Help 2a7a881 →