[object Object]

← back to Wallco Ai

wallco.ai · /designs search box → live autocomplete dropdown (uses /api/designs/search) — debounced 180ms, fetches /api/designs/search?q=&limit=24, renders top-8 with thumbnail+title+category+color-dot+room-badge in a floating dropdown beneath the input; ArrowUp/Down/Enter/Esc keyboard nav; click-outside dismiss; 'See all N results →' link to full /designs?q= page when count>8; AbortController cancels stale in-flight requests; form-submit fallback unchanged (Enter still works server-side without JS). aria-controls + role=combobox + aria-expanded for screen readers. Also adds data/generated/ + data/moodboards.json + data/reviews.json to .gitignore (was sweeping in 2 GB of generated PNGs).

3622bf562125fa957ceb3cf43a3dbd9cd1ed1c64 · 2026-05-12 08:00:35 -0700 · SteveStudio2

Files touched

Diff

commit 3622bf562125fa957ceb3cf43a3dbd9cd1ed1c64
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 08:00:35 2026 -0700

    wallco.ai · /designs search box → live autocomplete dropdown (uses /api/designs/search) — debounced 180ms, fetches /api/designs/search?q=&limit=24, renders top-8 with thumbnail+title+category+color-dot+room-badge in a floating dropdown beneath the input; ArrowUp/Down/Enter/Esc keyboard nav; click-outside dismiss; 'See all N results →' link to full /designs?q= page when count>8; AbortController cancels stale in-flight requests; form-submit fallback unchanged (Enter still works server-side without JS). aria-controls + role=combobox + aria-expanded for screen readers. Also adds data/generated/ + data/moodboards.json + data/reviews.json to .gitignore (was sweeping in 2 GB of generated PNGs).
---
 .gitignore |  3 +++
 server.js  | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 09fc806..3d823c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@ tmp/
 dist/
 build/
 data/images/
+data/generated/
+data/moodboards.json
+data/reviews.json
diff --git a/server.js b/server.js
index 34b2b51..af7fa13 100644
--- a/server.js
+++ b/server.js
@@ -1198,7 +1198,10 @@ ${htmlHeader('/designs')}
     <form method="get" action="/designs" class="filter-form">
       <input type="hidden" name="sort" id="sort-hidden" value="${sort}">
       <div class="toolbar-row">
-        <input type="search" name="q" value="${q}" placeholder="Search designs…" class="search-input">
+        <div class="search-wrap" style="position:relative;flex:1;min-width:200px">
+          <input type="search" name="q" value="${q}" placeholder="Search designs…" class="search-input" id="catalog-search-input" autocomplete="off" aria-autocomplete="list" aria-controls="catalog-search-suggest" aria-expanded="false" role="combobox" style="width:100%">
+          <div id="catalog-search-suggest" role="listbox" aria-label="Search suggestions" style="display:none;position:absolute;left:0;right:0;top:100%;margin-top:4px;background:var(--bg,#fff);border:1px solid var(--line,#ddd);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:50;max-height:60vh;overflow-y:auto"></div>
+        </div>
         <select name="cat" class="filter-select" onchange="this.form.submit()">
           <option value="">All Categories</option>
           ${cats.map(c => `<option value="${c}" ${c===cat?'selected':''}>${c.charAt(0).toUpperCase()+c.slice(1)}</option>`).join('')}
@@ -1406,6 +1409,80 @@ ${FOOTER}
       localStorage.setItem('wallco-sort', this.value);
     });
   }
+
+  // Live search dropdown — hits /api/designs/search, falls back to form submit on Enter / no-JS.
+  var sIn = document.getElementById('catalog-search-input');
+  var sBox = document.getElementById('catalog-search-suggest');
+  if (sIn && sBox) {
+    var sTimer = null, sCtl = null, sIdx = -1, sItems = [];
+    var esc = function(s){ return String(s||'').replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); };
+    var hide = function(){ sBox.style.display='none'; sIn.setAttribute('aria-expanded','false'); sIdx=-1; };
+    var render = function(results, qStr){
+      if (!results || !results.length) {
+        sBox.innerHTML = '<div style="padding:14px 16px;font:13px var(--sans,system-ui);color:var(--ink-faint,#888)">No matches for &ldquo;'+esc(qStr)+'&rdquo;</div>';
+        sBox.style.display='block'; sIn.setAttribute('aria-expanded','true');
+        sItems = []; sIdx = -1;
+        return;
+      }
+      var rows = results.slice(0, 8).map(function(r, i){
+        var img = r.image_url ? '<div style="width:44px;height:44px;flex-shrink:0;background:#f2efe8 url('+JSON.stringify(r.image_url).slice(1,-1)+') center/cover;border-radius:4px;border:1px solid var(--line,#eee)"></div>' : '<div style="width:44px;height:44px;flex-shrink:0;background:#f2efe8;border-radius:4px"></div>';
+        var dot = r.dominant_hex ? '<span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:'+esc(r.dominant_hex)+';border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>' : '';
+        var roomBadge = (r.room_mockups && r.room_mockups.length) ? '<span style="font:10px var(--sans,system-ui);color:var(--gold,#c9a14b);margin-left:6px">·room</span>' : '';
+        return '<a href="'+esc(r.url)+'" role="option" data-idx="'+i+'" style="display:flex;align-items:center;gap:10px;padding:8px 12px;text-decoration:none;color:var(--ink,#111);border-bottom:1px solid var(--line,#f0eee9);font:13px var(--sans,system-ui)">'
+          + img
+          + '<div style="min-width:0;flex:1"><div style="font-weight:500;color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(r.title||('Design #'+r.id))+'</div>'
+          + '<div style="font:11px var(--sans,system-ui);color:var(--ink-faint,#888);margin-top:2px">'+dot+esc(r.category||'')+roomBadge+'</div></div>'
+        + '</a>';
+      }).join('');
+      var more = results.length > 8
+        ? '<a href="/designs?q='+encodeURIComponent(qStr)+'" style="display:block;padding:8px 12px;font:12px var(--sans,system-ui);color:var(--ink-soft,#555);text-align:center;background:var(--card-bg,#fafafa);text-decoration:none">See all '+results.length+' results →</a>'
+        : '';
+      sBox.innerHTML = rows + more;
+      sBox.style.display='block'; sIn.setAttribute('aria-expanded','true');
+      sItems = Array.prototype.slice.call(sBox.querySelectorAll('a[role=option]'));
+      sIdx = -1;
+    };
+    var fetchSuggest = function(qStr){
+      if (sCtl) sCtl.abort();
+      sCtl = new AbortController();
+      fetch('/api/designs/search?q='+encodeURIComponent(qStr)+'&limit=24', { signal: sCtl.signal })
+        .then(function(r){ return r.json(); })
+        .then(function(j){ render(j.results||[], qStr); })
+        .catch(function(){ /* aborted or network — ignore */ });
+    };
+    sIn.addEventListener('input', function(){
+      var v = this.value.trim();
+      if (sTimer) clearTimeout(sTimer);
+      if (v.length < 2) { hide(); return; }
+      sTimer = setTimeout(function(){ fetchSuggest(v); }, 180);
+    });
+    sIn.addEventListener('keydown', function(e){
+      if (e.key === 'Escape') { hide(); return; }
+      if (sBox.style.display === 'none' || !sItems.length) return;
+      if (e.key === 'ArrowDown') {
+        e.preventDefault();
+        sIdx = (sIdx + 1) % sItems.length;
+        sItems.forEach(function(a,i){ a.style.background = i===sIdx ? 'var(--card-bg,#fafafa)' : ''; });
+        sItems[sIdx].scrollIntoView({ block:'nearest' });
+      } else if (e.key === 'ArrowUp') {
+        e.preventDefault();
+        sIdx = (sIdx - 1 + sItems.length) % sItems.length;
+        sItems.forEach(function(a,i){ a.style.background = i===sIdx ? 'var(--card-bg,#fafafa)' : ''; });
+        sItems[sIdx].scrollIntoView({ block:'nearest' });
+      } else if (e.key === 'Enter' && sIdx >= 0) {
+        e.preventDefault();
+        window.location = sItems[sIdx].href;
+      }
+    });
+    document.addEventListener('click', function(e){
+      if (!sIn.contains(e.target) && !sBox.contains(e.target)) hide();
+    });
+    sIn.addEventListener('focus', function(){
+      if (this.value.trim().length >= 2 && sBox.innerHTML) {
+        sBox.style.display='block'; sIn.setAttribute('aria-expanded','true');
+      }
+    });
+  }
 })();
 </script>
 

← 7d5cb78 wallco.ai · /api/designs/search?q=&limit= — lightweight in-m  ·  back to Wallco Ai  ·  wallco.ai · /api/designs/search now returns facets — motifs 4b1b3ed →