[object Object]

← back to Wallco Ai

wallco.ai · /murals search box parity with /designs — server-side ?q= filter (form-submit fallback), live autocomplete dropdown (same pattern: 180ms debounce, AbortController, Arrow/Enter/Esc keyboard nav, click-outside dismiss, did-you-mean chip row on 0-hits, See-all-N link when count>8), result-count header updates with matched count · verified: /murals 200, /murals?q=rose → 'matching rose (53 found)'

42951776581352a0d7d3ff15f67f99acc16712b9 · 2026-05-12 08:34:19 -0700 · SteveStudio2

Files touched

Diff

commit 42951776581352a0d7d3ff15f67f99acc16712b9
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 08:34:19 2026 -0700

    wallco.ai · /murals search box parity with /designs — server-side ?q= filter (form-submit fallback), live autocomplete dropdown (same pattern: 180ms debounce, AbortController, Arrow/Enter/Esc keyboard nav, click-outside dismiss, did-you-mean chip row on 0-hits, See-all-N link when count>8), result-count header updates with matched count · verified: /murals 200, /murals?q=rose → 'matching rose (53 found)'
---
 server.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 77 insertions(+), 3 deletions(-)

diff --git a/server.js b/server.js
index 07294bb..e34101d 100644
--- a/server.js
+++ b/server.js
@@ -4550,8 +4550,23 @@ ${HAMBURGER_JS}
 
 // ── MURALS
 app.get('/murals', (req, res) => {
+  const q = String(req.query.q || '').toLowerCase().trim();
   const murals = DESIGNS.filter(d => d.kind === 'mural' || d.kind === 'mural_panel');
-  const all    = murals.length === 0 ? DESIGNS : murals; // fallback: show all if no mural-kind yet
+  let all = murals.length === 0 ? DESIGNS : murals; // fallback: show all if no mural-kind yet
+  // Server-side ?q= filter — mirrors the search endpoint's logic so the
+  // form-submit fallback works without JS.
+  if (q && q.length >= 2) {
+    const terms = q.split(/\s+/).filter(Boolean);
+    all = all.filter(d => {
+      const hay = [
+        (d.title || '').toLowerCase(),
+        (d.category || '').toLowerCase(),
+        (d.handle || '').toLowerCase(),
+        ...(d.motifs || []).map(m => String(m).toLowerCase())
+      ].join(' ');
+      return terms.every(t => hay.includes(t));
+    });
+  }
 
   const cards = all.map(d => {
     const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
@@ -4586,14 +4601,73 @@ ${htmlHeader('/murals')}
   <div class="section-header" style="padding:40px 40px 0">
     <h1>Murals</h1>
     <p>Any design available as a custom-sized mural, panelized for wall installation.
-    ${murals.length === 0 ? 'All current designs can be ordered as murals — contact us.' : `${murals.length} mural designs available.`}</p>
+    ${murals.length === 0 ? 'All current designs can be ordered as murals — contact us.' : `${murals.length} mural designs available.`}${q ? ` · matching &ldquo;${q.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]))}&rdquo; (${all.length} found)` : ''}</p>
+  </div>
+  <div class="catalog-toolbar" style="padding:18px 40px 0">
+    <form method="get" action="/murals" class="filter-form">
+      <div class="toolbar-row">
+        <div class="search-wrap" style="position:relative;flex:1;min-width:200px">
+          <input type="search" name="q" value="${q.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]))}" placeholder="Search murals…" class="search-input" id="murals-search-input" autocomplete="off" aria-autocomplete="list" aria-controls="murals-search-suggest" aria-expanded="false" role="combobox" style="width:100%">
+          <div id="murals-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>
+      </div>
+    </form>
   </div>
   <div class="design-grid catalog-grid" style="padding:24px 40px">
-    ${cards}
+    ${cards || '<p class="empty-state">No murals match that search.</p>'}
   </div>
 </section>
 </main>
 ${FOOTER}
+<script>
+(function(){
+  var sIn = document.getElementById('murals-search-input');
+  var sBox = document.getElementById('murals-search-suggest');
+  if (!sIn || !sBox) return;
+  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, dym){
+    if (!results || !results.length) {
+      var dymRow = '';
+      if (dym && dym.length) {
+        dymRow = '<div style="padding:10px 14px 12px;border-top:1px solid var(--line,#f0eee9);background:#fafaf7"><div style="font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);margin-bottom:6px">Did you mean</div><div style="display:flex;flex-wrap:wrap;gap:5px">'
+          + dym.slice(0,5).map(function(s){ return '<a href="/murals?q='+encodeURIComponent(s.word)+'" style="display:inline-flex;align-items:center;padding:4px 10px;font:11px var(--sans,system-ui);background:var(--bg,#fff);color:var(--ink,#111);border:1px solid var(--line,#e6e2d8);border-radius:999px;text-decoration:none">'+esc(s.word)+'</a>'; }).join('')
+          + '</div></div>';
+      }
+      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>' + dymRow;
+      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>' : '';
+      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||'')+'</div></div></a>';
+    }).join('');
+    var more = results.length > 8 ? '<a href="/murals?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, j.did_you_mean||[]); })
+      .catch(function(){});
+  };
+  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(); });
+})();
+</script>
 ${HAMBURGER_JS}
 </body>
 </html>`);

← 4739bcb wallco.ai · /api/designs/search now emits did_you_mean[] on  ·  back to Wallco Ai  ·  wallco.ai · sample basket — multi-design trade samples (mirr 542e977 →