← 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
M package-lock.jsonM package.jsonM public/crcp-grid.cssM public/crcp-grid.js
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 →