← 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 →