[object Object]

← back to Nationalrealestate

USRE grid: URL-shareable filters — sync filters/search/sort to query string + Copy link button (bookmarkable views) (v0.10.0)

c246a3b173389a67097e0f6a1afd8b4263628e49 · 2026-07-26 20:37:24 -0700 · Steve

Files touched

Diff

commit c246a3b173389a67097e0f6a1afd8b4263628e49
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jul 26 20:37:24 2026 -0700

    USRE grid: URL-shareable filters — sync filters/search/sort to query string + Copy link button (bookmarkable views) (v0.10.0)
---
 package-lock.json    |  4 ++--
 package.json         |  2 +-
 public/crcp-grid.css |  3 +++
 public/crcp-grid.js  | 42 ++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 46 insertions(+), 5 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 575680b..ab8b1c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nationalrealestate",
-  "version": "0.9.1",
+  "version": "0.10.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "nationalrealestate",
-      "version": "0.9.1",
+      "version": "0.10.0",
       "dependencies": {
         "better-sqlite3": "^13.0.1",
         "dotenv": "^16.4.5",
diff --git a/package.json b/package.json
index 42d4cf1..80bac7c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "nationalrealestate",
-  "version": "0.9.1",
+  "version": "0.10.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/crcp-grid.css b/public/crcp-grid.css
index 80bf9f1..f3842c4 100644
--- a/public/crcp-grid.css
+++ b/public/crcp-grid.css
@@ -38,6 +38,9 @@
 .cg-af-clear{font-size:12px;background:none;border:0;cursor:pointer;padding:3px 6px;
   color:var(--cg-mut,#8b949e);text-decoration:underline;}
 .cg-af-clear:hover{color:var(--cg-acc,#58a6ff);}
+.cg-af-copy{font-size:12px;background:none;border:1px solid var(--cg-line,#2a313c);cursor:pointer;padding:3px 10px;
+  border-radius:8px;color:var(--cg-mut,#8b949e);}
+.cg-af-copy:hover{color:var(--cg-acc,#58a6ff);border-color:var(--cg-acc,#58a6ff);}
 .cg-q{min-width:240px;background:var(--cg-card,#161b22);color:var(--cg-ink,#e6edf3);border:1px solid var(--cg-line,#2a313c);border-radius:8px;padding:8px 11px;font-size:13px;}
 .cg-seg{display:inline-flex;border:1px solid var(--cg-line,#2a313c);border-radius:8px;overflow:hidden;}
 .cg-seg button{background:var(--cg-card,#161b22);color:var(--cg-mut,#8b949e);border:0;border-right:1px solid var(--cg-line,#2a313c);padding:7px 12px;font-size:12px;cursor:pointer;}
diff --git a/public/crcp-grid.js b/public/crcp-grid.js
index 0208f5c..71afff4 100644
--- a/public/crcp-grid.js
+++ b/public/crcp-grid.js
@@ -100,6 +100,35 @@
       return true;
     }
 
+    // ---- URL sync: filters/search/sort <-> query string (shareable views) ----
+    function readFromURL() {
+      if (O.urlSync === false) return;
+      const sp = new URLSearchParams(location.search);
+      if (![...sp.keys()].length) return;
+      facets.forEach(fc => {
+        if (fc.type === 'chip') { const v = sp.get(fc.k); if (v) v.split(',').forEach(x => F.chips[fc.k].add(x)); }
+        else {
+          const mn = sp.get(fc.k + '_min'), mx = sp.get(fc.k + '_max');
+          if (mn != null && mn !== '') F.ranges[fc.k].min = +mn;
+          if (mx != null && mx !== '') F.ranges[fc.k].max = +mx;
+        }
+      });
+      const q = sp.get('q'); if (q) F.q = q.toLowerCase();
+      const so = sp.get('sort'); if (so) { const [k, d] = so.split(':'); if (k) SORT = { k, dir: d === 'asc' ? 1 : -1 }; }
+    }
+    function syncURL() {
+      if (O.urlSync === false) return;
+      const sp = new URLSearchParams();
+      facets.forEach(fc => {
+        if (fc.type === 'chip') { const set = F.chips[fc.k]; if (set && set.size) sp.set(fc.k, [...set].join(',')); }
+        else { const r = F.ranges[fc.k]; if (r) { if (r.min != null) sp.set(fc.k + '_min', r.min); if (r.max != null) sp.set(fc.k + '_max', r.max); } }
+      });
+      if (F.q) sp.set('q', F.q);
+      if (SORT && SORT.k) sp.set('sort', SORT.k + ':' + (SORT.dir < 0 ? 'desc' : 'asc'));
+      const qs = sp.toString();
+      try { history.replaceState(null, '', location.pathname + (qs ? '?' + qs : '') + location.hash); } catch (e) {}
+    }
+
     // ---- rail ----
     function buildRail() {
       const rail = $(O.rail);
@@ -206,7 +235,8 @@
       if (F.q) parts.push(`<span class="cg-af" data-aft="q">“${esc(F.q)}” <b>✕</b></span>`);
       if (!parts.length) return '';
       return `<span class="cg-af-lbl">Filters</span>` + parts.join('') +
-        `<button class="cg-af-clear" data-clearall="1">Clear all</button>`;
+        `<button class="cg-af-clear" data-clearall="1">Clear all</button>` +
+        `<button class="cg-af-copy" data-copy="1" title="Copy a shareable link to this filtered view">🔗 Copy link</button>`;
     }
     function clearFilter(el) {
       if (el.dataset.clearall != null) {
@@ -245,6 +275,7 @@
       if (sel) sel.value = SORT && SORT.k ? SORT.k + ':' + (SORT.dir < 0 ? 'desc' : 'asc') : '';
       const ab = $(O.mount + ' .cg-active');
       if (ab) ab.innerHTML = activeFilterHTML();
+      syncURL();
     }
 
     // ---- topbar ----
@@ -262,6 +293,7 @@
         `<label class="cg-dens">Density <input type="range" class="cg-cols" min="1" max="12" step="1" value="${O.cols || 3}"></label>` +
         `<span class="cg-count"></span></div><div class="cg-active"></div><div class="cg-grid"></div>`);
       const q = m.querySelector('.cg-q');
+      if (F.q) q.value = F.q; // reflect a search restored from the URL
       q.addEventListener('input', () => { clearTimeout(q._t); q._t = setTimeout(() => { F.q = q.value.trim().toLowerCase(); render(); }, 160); });
       m.querySelector('.cg-sort').addEventListener('change', e => {
         const v = e.target.value;
@@ -288,6 +320,12 @@
       }
       // th-click sorting + whole-row click (delegated; grid re-renders under us)
       m.addEventListener('click', e => {
+        const cp = e.target.closest('.cg-af-copy');
+        if (cp) {
+          const done = () => { const t = cp.textContent; cp.textContent = '✓ Copied'; setTimeout(() => cp.textContent = t, 1200); };
+          if (navigator.clipboard) navigator.clipboard.writeText(location.href).then(done, done); else done();
+          return;
+        }
         const af = e.target.closest('.cg-af, .cg-af-clear');
         if (af) { clearFilter(af); return; }
         const th = e.target.closest('.cg-sortth');
@@ -327,7 +365,7 @@
       return rows.length;
     }
 
-    buildTop(); buildRail(); render();
+    readFromURL(); buildTop(); buildRail(); render();
     return {
       render,
       setData(d) { O.data.length = 0; d.forEach(x => O.data.push(x)); buildRail(); render(); },

← b9110d8 USRE Map: on-map active-filter indicator (count + reset) so  ·  back to Nationalrealestate  ·  USRE Map: URL-shareable county filters + on-map Copy link (p c5b668f →