[object Object]

← back to Nationalrealestate

USRE geo-search: recent searches (localStorage) shown on empty focus + clear (v0.15.0)

9ab361e885a68cb2b345fc47c8f1f19d75d837b5 · 2026-07-26 21:58:35 -0700 · Steve

Files touched

Diff

commit 9ab361e885a68cb2b345fc47c8f1f19d75d837b5
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jul 26 21:58:35 2026 -0700

    USRE geo-search: recent searches (localStorage) shown on empty focus + clear (v0.15.0)
---
 package-lock.json     |  4 ++--
 package.json          |  2 +-
 public/geo-search.css |  5 +++++
 public/geo-search.js  | 41 +++++++++++++++++++++++++++++++++--------
 4 files changed, 41 insertions(+), 11 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 7b9f6a3..4dfdf6f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nationalrealestate",
-  "version": "0.14.0",
+  "version": "0.15.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "nationalrealestate",
-      "version": "0.14.0",
+      "version": "0.15.0",
       "dependencies": {
         "better-sqlite3": "^13.0.1",
         "dotenv": "^16.4.5",
diff --git a/package.json b/package.json
index 035c45f..c43e81d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "nationalrealestate",
-  "version": "0.14.0",
+  "version": "0.15.0",
   "private": true,
   "type": "module",
   "description": "USRealEstate — national U.S. residential market explorer. Free-data v1 (Redfin Data Center, Zillow Research, Census ACS, FHFA). Internal analyst tool behind basic auth.",
diff --git a/public/geo-search.css b/public/geo-search.css
index 3b5eac7..cc84a5f 100644
--- a/public/geo-search.css
+++ b/public/geo-search.css
@@ -28,6 +28,11 @@
 .geo-badge{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--gold,#c8a24b);
   border:1px solid var(--line,#222936);border-radius:10px;padding:2px 7px;white-space:nowrap;}
 .geo-empty{padding:12px 14px;color:var(--muted,#8a93a3);font-size:13px;line-height:1.5;}
+.geo-rechdr{display:flex;justify-content:space-between;align-items:center;padding:8px 14px;
+  font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted,#8a93a3);
+  border-bottom:1px solid var(--line,#222936);}
+.geo-recclr{cursor:pointer;text-transform:none;letter-spacing:0;text-decoration:underline;}
+.geo-recclr:hover{color:var(--gold,#c8a24b);}
 
 /* ── pill mode: fixed top-center (map + content pages) ── */
 .geo-pill{position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:1100;width:min(440px,86vw);}
diff --git a/public/geo-search.js b/public/geo-search.js
index 5038f81..f73b64f 100644
--- a/public/geo-search.js
+++ b/public/geo-search.js
@@ -24,6 +24,30 @@
     const close = () => { results.innerHTML = ''; results.classList.remove('open'); items = []; active = -1; };
     const go = u => { if (u) location.href = u; };
 
+    // recent searches (client-side, per browser) — shown when the box is focused + empty
+    const RKEY = 'usre.recentSearches';
+    const readRecent = () => { try { return JSON.parse(localStorage.getItem(RKEY) || '[]'); } catch (e) { return []; } };
+    const remember = it => {
+      try {
+        if (!it || !it.url) return;
+        const a = readRecent().filter(x => x.url !== it.url);
+        a.unshift({ name: it.name, url: it.url, type: it.type, state: it.state || null, sub: it.sub || null });
+        localStorage.setItem(RKEY, JSON.stringify(a.slice(0, 6)));
+      } catch (e) {}
+    };
+    const rowHtml = (r, i) =>
+      `<a class="geo-item" data-i="${i}" href="${esc(r.url)}">` +
+      `<span class="geo-name">${esc(r.name)}${r.sub ? `<span class="geo-subline">${esc(r.sub)}</span>` : ''}</span>` +
+      (r.state ? `<span class="geo-state">${esc(r.state)}</span>` : '') +
+      `<span class="geo-badge">${badge(r.type)}</span></a>`;
+    function showRecent() {
+      const rec = readRecent();
+      if (!rec.length) { close(); return; }
+      items = rec; active = -1;
+      results.innerHTML = '<div class="geo-rechdr">Recent<span class="geo-recclr">clear</span></div>' + rec.map(rowHtml).join('');
+      results.classList.add('open');
+    }
+
     function render(rows) {
       items = rows;
       if (!rows.length) {
@@ -34,11 +58,7 @@
             : 'No matches.') + '</div>';
         results.classList.add('open'); return;
       }
-      results.innerHTML = rows.map((r, i) =>
-        `<a class="geo-item" data-i="${i}" href="${esc(r.url)}">` +
-        `<span class="geo-name">${esc(r.name)}${r.sub ? `<span class="geo-subline">${esc(r.sub)}</span>` : ''}</span>` +
-        (r.state ? `<span class="geo-state">${esc(r.state)}</span>` : '') +
-        `<span class="geo-badge">${badge(r.type)}</span></a>`).join('');
+      results.innerHTML = rows.map(rowHtml).join('');
       results.classList.add('open'); active = -1;
     }
     function setActive(n) {
@@ -57,16 +77,21 @@
       const q = input.value.trim(); lastQ = q;
       clear.style.display = q ? 'block' : 'none';
       clearTimeout(t);
-      if (q.length < 2) { close(); return; }
+      if (q.length < 2) { showRecent(); return; } // empty box shows recent searches
       t = setTimeout(() => search(q), 180);
     });
+    input.addEventListener('focus', () => { if (input.value.trim().length < 2) showRecent(); });
     input.addEventListener('keydown', e => {
       if (e.key === 'ArrowDown') { e.preventDefault(); setActive(active + 1); }
       else if (e.key === 'ArrowUp') { e.preventDefault(); setActive(active - 1); }
-      else if (e.key === 'Enter') { if (active >= 0 && items[active]) go(items[active].url); else if (items[0]) go(items[0].url); }
+      else if (e.key === 'Enter') { const it = (active >= 0 && items[active]) ? items[active] : items[0]; if (it) { remember(it); go(it.url); } }
       else if (e.key === 'Escape') { close(); input.blur(); }
     });
-    clear.addEventListener('click', () => { input.value = ''; clear.style.display = 'none'; close(); input.focus(); });
+    clear.addEventListener('click', () => { input.value = ''; clear.style.display = 'none'; input.focus(); showRecent(); });
+    results.addEventListener('click', e => {
+      if (e.target.closest('.geo-recclr')) { e.preventDefault(); try { localStorage.removeItem(RKEY); } catch (x) {} close(); return; }
+      const a = e.target.closest('.geo-item'); if (a) { const it = items[+a.dataset.i]; if (it) remember(it); } // store before the browser follows href
+    });
     document.addEventListener('click', e => { if (!host.contains(e.target)) close(); });
   }
 

← 7c0710f USRE: searched address in a parcel-covered county deep-links  ·  back to Nationalrealestate  ·  USRE Map: export matching counties to CSV (parity w/ grid) + 1499f0f →