[object Object]

← back to Wallpaperdictionary

corner-nav: live as-you-type search with thumbnail previews (title+tags+description)

16dff3c20e50203f36cec845783d985f56b82234 · 2026-07-29 09:42:43 -0700 · Steve Abrams

Files touched

Diff

commit 16dff3c20e50203f36cec845783d985f56b82234
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 29 09:42:43 2026 -0700

    corner-nav: live as-you-type search with thumbnail previews (title+tags+description)
---
 public/corner-nav.js | 84 ++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 71 insertions(+), 13 deletions(-)

diff --git a/public/corner-nav.js b/public/corner-nav.js
index d390a17..5f6d493 100644
--- a/public/corner-nav.js
+++ b/public/corner-nav.js
@@ -66,6 +66,11 @@
 .cnav-search{padding:14px 16px;width:100%;font-size:18px;background:transparent;border:0;border-bottom:1px solid rgba(255,255,255,0.18);color:#f5f1e8;outline:none;box-sizing:border-box;margin-bottom:18px;font-family:inherit}\
 .cnav-search::placeholder{color:rgba(245,241,232,0.4)}\
 .cnav-sub{font-size:12px;color:rgba(245,241,232,0.55);margin:18px 0 8px;letter-spacing:0.08em;text-transform:uppercase}\
+.cnav-panel a.cnav-res{display:flex;align-items:center;gap:12px;padding:10px 4px;border-bottom:1px solid rgba(255,255,255,0.08);text-decoration:none;color:#f5f1e8}\
+.cnav-panel a.cnav-res:hover{opacity:1;background:rgba(255,255,255,0.04)}\
+.cnav-res img,.cnav-res .cnav-res-ph{width:48px;height:48px;object-fit:cover;border-radius:4px;background:rgba(255,255,255,0.06);flex:0 0 auto}\
+.cnav-res-t{font-size:14px;letter-spacing:0.02em;line-height:1.3;text-transform:none}\
+.cnav-searching,.cnav-nores{font-size:13px;color:rgba(245,241,232,0.55);padding:12px 4px;letter-spacing:0.02em}\
 @media (max-width:520px){.cnav-wrap{top:10px;right:10px;padding:8px 12px;gap:12px}.cnav-loginwrap{top:10px;left:10px;padding:8px 12px;gap:8px}.cnav-btn .cnav-lbl{display:none}.cnav-btn svg{width:20px;height:20px}}\
 ';
   var styleEl = document.createElement('style');
@@ -133,7 +138,7 @@
   var srch = panel('\
     <h3>Search</h3>\
     <input type="search" class="cnav-search" id="cnav-search-input" placeholder="Search ' + BRAND + '..." autocomplete="off">\
-    <div class="cnav-sub">Press enter to search · ESC to close</div>\
+    <div class="cnav-sub">Type to search · enter opens the first result · ESC to close</div>\
     <div id="cnav-search-results" style="margin-top:18px"></div>\
   ');
 
@@ -183,27 +188,80 @@
   document.querySelectorAll('.cnav-close').forEach(function (b) { b.addEventListener('click', closeAll); });
   document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(); });
 
-  // Search — live-filter on-page grid if one exists; else submit to /search
+  // Search — LIVE preview as you type. Queries the site product API (which
+  // searches title + tags + description) on a short debounce and renders
+  // clickable thumbnail results. Falls back to filtering the on-page grid
+  // only when no API is reachable, so pages without /api/products still work.
   var input = document.getElementById('cnav-search-input');
   var results = document.getElementById('cnav-search-results');
+  var SEARCH_API = CFG.searchApi || '/api/products';
+  var _st, _seq = 0;
+
+  function cnEsc(s) {
+    return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
+      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
+    });
+  }
+  function resHref(p) {
+    return p.product_url || p.url || (p.handle ? '/products/' + p.handle : '#');
+  }
+  function resImg(p)   { return p.image_url || p.image || p.featured_image || p.thumbnail || ''; }
+  function resTitle(p) { return p.title || p.name || p.sku || 'Untitled'; }
+
+  function renderResults(items, q) {
+    if (!items || !items.length) {
+      results.innerHTML = '<div class="cnav-nores">No matches for &ldquo;' + cnEsc(q) + '&rdquo;</div>';
+      return;
+    }
+    results.innerHTML = items.slice(0, 8).map(function (p) {
+      var img = resImg(p);
+      return '<a class="cnav-res" href="' + cnEsc(resHref(p)) + '">' +
+        (img ? '<img loading="lazy" src="' + cnEsc(img) + '" alt="">' : '<span class="cnav-res-ph"></span>') +
+        '<span class="cnav-res-t">' + cnEsc(resTitle(p)) + '</span></a>';
+    }).join('');
+  }
+
+  // Fallback: filter whatever cards are already rendered on the page.
+  function gridFilter(q) {
+    if (!hasGrid) { results.innerHTML = ''; return; }
+    var cards = hasGrid.querySelectorAll('[data-card], .product-card, .pcard, .grid-item, .card');
+    var shown = 0;
+    cards.forEach(function (c) {
+      var t = c.textContent.toLowerCase();
+      var hit = !q || t.indexOf(q) !== -1;
+      c.style.display = hit ? '' : 'none';
+      if (hit) shown++;
+    });
+    results.textContent = q ? (shown + ' result' + (shown === 1 ? '' : 's') + ' on this page') : '';
+  }
+
   if (input) {
     input.addEventListener('input', function () {
-      var q = input.value.trim().toLowerCase();
-      if (!hasGrid) return;
-      var cards = hasGrid.querySelectorAll('[data-card], .product-card, .pcard, .grid-item');
-      var shown = 0;
-      cards.forEach(function (c) {
-        var t = c.textContent.toLowerCase();
-        var hit = !q || t.indexOf(q) !== -1;
-        c.style.display = hit ? '' : 'none';
-        if (hit) shown++;
-      });
-      results.textContent = q ? (shown + ' result' + (shown === 1 ? '' : 's') + ' on this page') : '';
+      var q = input.value.trim();
+      clearTimeout(_st);
+      if (!q) { results.innerHTML = ''; gridFilter(''); return; }
+      results.innerHTML = '<div class="cnav-searching">Searching…</div>';
+      var myseq = ++_seq;
+      _st = setTimeout(function () {
+        fetch(SEARCH_API + '?q=' + encodeURIComponent(q) + '&limit=8')
+          .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
+          .then(function (d) {
+            if (myseq !== _seq) return; // a newer keystroke already fired
+            var items = d.items || d.products || (Array.isArray(d) ? d : []);
+            renderResults(items, q);
+          })
+          .catch(function () {
+            if (myseq !== _seq) return;
+            gridFilter(q.toLowerCase()); // no API → live-filter the page instead
+          });
+      }, 180);
     });
     input.addEventListener('keydown', function (e) {
       if (e.key === 'Enter') {
         var q = input.value.trim();
         if (!q) return;
+        var first = results.querySelector('a.cnav-res');
+        if (first) { location.href = first.getAttribute('href'); return; }
         if (hasGrid) { closeAll(); hasGrid.scrollIntoView({ behavior: 'smooth' }); }
         else { location.href = '/search?q=' + encodeURIComponent(q); }
       }

← 3faed96 corner-nav: move login to its own hamburger in upper-left (p  ·  back to Wallpaperdictionary  ·  corner-nav: block javascript:/data: URIs in search preview ( e41bd5c →