[object Object]

← back to Nationalrealestate

USRE Map: URL-shareable county filters + on-map Copy link (parity with grid) (v0.11.0)

c5b668f170f84715babcd72bd231b86b70ea0463 · 2026-07-26 20:42:29 -0700 · Steve

Files touched

Diff

commit c5b668f170f84715babcd72bd231b86b70ea0463
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jul 26 20:42:29 2026 -0700

    USRE Map: URL-shareable county filters + on-map Copy link (parity with grid) (v0.11.0)
---
 package-lock.json |  4 ++--
 package.json      |  2 +-
 public/index.html | 34 +++++++++++++++++++++++++++++++---
 3 files changed, 34 insertions(+), 6 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index ab8b1c4..9d952d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nationalrealestate",
-  "version": "0.10.0",
+  "version": "0.11.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "nationalrealestate",
-      "version": "0.10.0",
+      "version": "0.11.0",
       "dependencies": {
         "better-sqlite3": "^13.0.1",
         "dotenv": "^16.4.5",
diff --git a/package.json b/package.json
index 80bac7c..df3697e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "nationalrealestate",
-  "version": "0.10.0",
+  "version": "0.11.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/index.html b/public/index.html
index f76060c..b0130c0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -64,6 +64,9 @@
     background:rgba(17,21,29,.94);border:1px solid var(--gold);border-radius:20px;padding:6px 8px 6px 16px;
     font-size:12px;color:var(--muted);white-space:nowrap;backdrop-filter:blur(6px);box-shadow:0 2px 12px rgba(0,0,0,.5);}
   #mapfbar b{color:var(--gold);font-variant-numeric:tabular-nums;}
+  #mapfbar #mapfbar-copy{background:none;color:var(--muted);border:1px solid var(--line);border-radius:14px;
+    font-size:11px;padding:4px 10px;cursor:pointer;}
+  #mapfbar #mapfbar-copy:hover{color:var(--gold);border-color:var(--gold);}
   #mapfbar #mapfbar-reset{background:var(--gold);color:var(--bg);border:0;border-radius:14px;
     font-size:11px;font-weight:700;padding:4px 12px;cursor:pointer;}
   #mapfbar #mapfbar-reset:hover{filter:brightness(1.08);}
@@ -85,7 +88,7 @@
   <div id="metrics"></div>
 </div>
 <div id="map"></div>
-<div id="mapfbar" style="display:none"><span id="mapfbar-txt"></span><button id="mapfbar-reset" type="button">reset</button></div>
+<div id="mapfbar" style="display:none"><span id="mapfbar-txt"></span><button id="mapfbar-copy" type="button">🔗 link</button><button id="mapfbar-reset" type="button">reset</button></div>
 <div id="legend"><div class="t">Loading…</div></div>
 <div id="status">loading county data…</div>
 <div id="natstrip" style="position:fixed;left:50%;transform:translateX(-50%);bottom:18px;z-index:1000;
@@ -184,6 +187,22 @@ function loadMapFilters(){
     if (s) METRICS.forEach(m => { if (s[m.key]) fRanges[m.key] = { min: s[m.key].min ?? null, max: s[m.key].max ?? null }; });
   } catch (e) {}
 }
+function readMapFiltersFromURL(){
+  const sp = new URLSearchParams(location.search);
+  let any = false;
+  METRICS.forEach(m => {
+    const mn = sp.get(m.key + '_min'), mx = sp.get(m.key + '_max');
+    if (mn != null && mn !== ''){ fRanges[m.key].min = +mn; any = true; }
+    if (mx != null && mx !== ''){ fRanges[m.key].max = +mx; any = true; }
+  });
+  return any;
+}
+function syncMapURL(){
+  const sp = new URLSearchParams();
+  METRICS.forEach(m => { const r = fRanges[m.key]; if (r.min != null) sp.set(m.key + '_min', r.min); if (r.max != null) sp.set(m.key + '_max', r.max); });
+  const qs = sp.toString();
+  try { history.replaceState(null, '', location.pathname + (qs ? '?' + qs : '') + location.hash); } catch (e) {}
+}
 function mapChipsHTML(){
   const md = k => METRICS.find(m => m.key === k);
   return METRICS.filter(m => { const r = fRanges[m.key]; return r.min != null || r.max != null; }).map(m => {
@@ -200,6 +219,7 @@ function resetMapFilters(){
 }
 function applyFilters(){
   if (geoLayer) geoLayer.setStyle(styleFeature);
+  syncMapURL(); // keep the query string in sync so filtered map views are shareable
   const chips = document.getElementById('mf-chips');
   if (chips) chips.innerHTML = mapChipsHTML();
   const el = document.getElementById('mf-count');
@@ -218,7 +238,7 @@ function applyFilters(){
 function buildMapFilters(){
   const host = document.getElementById('navDrawerBody');
   if (!host || document.getElementById('mapfilters')) return;
-  loadMapFilters(); // restore persisted ranges before rendering the inputs
+  loadMapFilters(); readMapFiltersFromURL(); // localStorage first, then URL query wins (shareable links)
   const wrap = document.createElement('div'); wrap.id = 'mapfilters';
   wrap.innerHTML = `<div class="mf-h">Filter counties <button id="mf-reset" type="button">reset</button></div>` +
     METRICS.map(m => { const r = fRanges[m.key];
@@ -235,7 +255,15 @@ function buildMapFilters(){
   });
   wrap.querySelector('#mf-reset').addEventListener('click', resetMapFilters);
   const bar = document.getElementById('mapfbar');
-  if (bar) bar.querySelector('#mapfbar-reset').addEventListener('click', resetMapFilters);
+  if (bar){
+    bar.querySelector('#mapfbar-reset').addEventListener('click', resetMapFilters);
+    const cpy = bar.querySelector('#mapfbar-copy');
+    if (cpy) cpy.addEventListener('click', () => {
+      const t = cpy.textContent;
+      if (navigator.clipboard) navigator.clipboard.writeText(location.href);
+      cpy.textContent = '✓ copied'; setTimeout(() => cpy.textContent = t, 1200);
+    });
+  }
   // active-filter chips: click ✕ to clear that metric's range
   wrap.querySelector('#mf-chips').addEventListener('click', e => {
     const chip = e.target.closest('.mf-chip'); if (!chip) return;

← c246a3b USRE grid: URL-shareable filters — sync filters/search/sort  ·  back to Nationalrealestate  ·  USRE: top location search (county/city/state) — /api/geo-sea 0a3f34d →