[object Object]

← back to Apartmentwallpaper

fix(facets): drive /api/facets from live filter state so chip counts actually update

66f45ffff0570764e4e32f82e058ca9e5db4d7bd · 2026-06-01 12:40:13 -0700 · SteveStudio2

claude-codex debate finding #1/#1a: loadFacets() fetched /api/facets with no
params and ran once at init, so commit 7f2f2b6's drill-down counting always saw
an empty query — chip counts never moved as filters applied (feature dead on the
frontend). Now loadFacets() sends q + selected aesthetic, is re-called from
resetGrid() on every filter change, clears #facets/#footerFacets before
re-rendering (no double-stacked chips), wires the click listener once (container
survives innerHTML clears), preserves the active chip across refreshes, and uses
closest('button') so clicking the count span registers. Verified live: facets
honor aesthetic+q params; limit=-5 clamps to 1.

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

Files touched

Diff

commit 66f45ffff0570764e4e32f82e058ca9e5db4d7bd
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon Jun 1 12:40:13 2026 -0700

    fix(facets): drive /api/facets from live filter state so chip counts actually update
    
    claude-codex debate finding #1/#1a: loadFacets() fetched /api/facets with no
    params and ran once at init, so commit 7f2f2b6's drill-down counting always saw
    an empty query — chip counts never moved as filters applied (feature dead on the
    frontend). Now loadFacets() sends q + selected aesthetic, is re-called from
    resetGrid() on every filter change, clears #facets/#footerFacets before
    re-rendering (no double-stacked chips), wires the click listener once (container
    survives innerHTML clears), preserves the active chip across refreshes, and uses
    closest('button') so clicking the count span registers. Verified live: facets
    honor aesthetic+q params; limit=-5 clamps to 1.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/index.html | 36 ++++++++++++++++++++++++++----------
 1 file changed, 26 insertions(+), 10 deletions(-)

diff --git a/public/index.html b/public/index.html
index b582137..7ece6f7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -662,35 +662,50 @@ function openDetails(p) {
 }
 
 
+// Facet chip counts are drill-down: each is recomputed against the live filter
+// state (q + selected aesthetic) so the numbers track the result set, not the
+// whole catalog. Called on init AND from resetGrid() on every filter change.
+let _facetsWired = false;
 async function loadFacets() {
+  const fp = new URLSearchParams();
+  if (state.q) fp.set('q', state.q);
+  if (state.facet && state.facet !== 'all') fp.set('aesthetic', state.facet);
   let f;
   try {
-    const r = await fetch('/api/facets');
+    const r = await fetch('/api/facets?' + fp);
     if (!r.ok) throw new Error('HTTP ' + r.status);
     f = await r.json();
   } catch (e) { console.error('loadFacets failed:', e); return; }
   const el = document.getElementById('facets');
+  el.innerHTML = '';   // clear before re-render so refreshed counts don't double-stack chips
   for (const [k, v] of Object.entries(f.aesthetics).sort((a,b) => b[1] - a[1])) {
     const b = document.createElement('button');
-    b.className = 'chip'; b.dataset.facet = k;
+    b.className = 'chip' + (state.facet === k ? ' active' : '');   // keep the active chip lit across refreshes
+    b.dataset.facet = k;
     b.innerHTML = (LABELS[k] || k) + ' <span style="opacity:.55;font-weight:500;margin-left:4px">' + v + '</span>';
     el.appendChild(b);
   }
-  el.addEventListener('click', e => {
-    if (e.target.tagName !== 'BUTTON') return;
-    document.querySelectorAll('#facets button').forEach(b => b.classList.remove('active'));
-    e.target.classList.add('active');
-    state.facet = e.target.dataset.facet;
-    resetGrid();
-  });
+  // Wire the delegated click handler ONCE — the #facets container survives the
+  // innerHTML clears above, so re-adding it every call would stack listeners.
+  // closest('button') so clicking the count <span> still registers the chip.
+  if (!_facetsWired) {
+    el.addEventListener('click', e => {
+      const btn = e.target.closest('button');
+      if (!btn || !el.contains(btn)) return;
+      state.facet = btn.dataset.facet;
+      resetGrid();
+    });
+    _facetsWired = true;
+  }
   document.getElementById('totalCount').textContent = f.total;
   document.getElementById('footerStat').textContent = f.total + ' patterns · live archive';
   // Footer aesthetic links
   const footerEl = document.getElementById('footerFacets');
+  footerEl.innerHTML = '';   // clear before re-render (same double-stack guard)
   for (const [k] of Object.entries(f.aesthetics)) {
     const b = document.createElement('button');
     b.textContent = LABELS[k] || k;
-    b.onclick = () => { state.facet = k; resetGrid(); document.querySelectorAll('#facets button').forEach(x => x.classList.toggle('active', x.dataset.facet === k)); document.getElementById('shop').scrollIntoView({behavior:'smooth'}); };
+    b.onclick = () => { state.facet = k; resetGrid(); document.getElementById('shop').scrollIntoView({behavior:'smooth'}); };
     footerEl.appendChild(b);
   }
 }
@@ -699,6 +714,7 @@ function resetGrid() {
   state.page = 1; state.exhausted = false;
   document.getElementById('grid').innerHTML = '';
   document.getElementById('loading').textContent = 'Loading…';
+  loadFacets();      // refresh drill-down chip counts for the new filter state
   loadGridPage();
 }
 

← 0ac44de fix(theme2): sticky data-theme-preview flag so Theme 1 isn't  ·  back to Apartmentwallpaper  ·  refactor(theme): extract page-theme to shared css/js; replac 8f5a5fa →