← back to Commercialrealestate
user-badge: Saved Searches menu under the user-name icon — per-user server-backed (/api/saved-searches), snapshots the checked-field layout (cre_fields+filters+navagent) and restores it, name/list/restore/delete; appears on ?v=list + every page via the injected badge (no page-file edits); v0.26.0
3f9c93dd566ae1f85f52cb3a9cf4d58a1c8368ca · 2026-08-26 11:42:01 -0700 · Steve
Files touched
M package-lock.jsonM package.jsonM public/user-badge.js
Diff
commit 3f9c93dd566ae1f85f52cb3a9cf4d58a1c8368ca
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 26 11:42:01 2026 -0700
user-badge: Saved Searches menu under the user-name icon — per-user server-backed (/api/saved-searches), snapshots the checked-field layout (cre_fields+filters+navagent) and restores it, name/list/restore/delete; appears on ?v=list + every page via the injected badge (no page-file edits); v0.26.0
---
package-lock.json | 4 +-
package.json | 2 +-
public/user-badge.js | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 141 insertions(+), 4 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index ba20102..fee7b5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "commercialrealestate",
- "version": "0.25.1",
+ "version": "0.26.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "commercialrealestate",
- "version": "0.25.1",
+ "version": "0.26.0",
"dependencies": {
"better-sqlite3": "^12.11.1",
"express": "^4.22.2",
diff --git a/package.json b/package.json
index 9fd9a67..cad439f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "commercialrealestate",
- "version": "0.25.1",
+ "version": "0.26.0",
"private": true,
"description": "LA County CRE investment explorer — multi-firm sourced, Census + assessor enriched, Qwen-analyzed",
"scripts": {
diff --git a/public/user-badge.js b/public/user-badge.js
index 8366c84..aa1d7d9 100644
--- a/public/user-badge.js
+++ b/public/user-badge.js
@@ -20,6 +20,53 @@
});
};
+ // localStorage keys (plus every `navagent:` key) that together define a page's layout+filters,
+ // so a saved search restores the exact view — the field-visibility set (cre_fields), column order,
+ // view/group/density, and the filter/scenario/heat state. Filters also live in the URL (qs).
+ var LAYOUT_KEYS = ['cre_fields', 'idxColOrder', 'cre_view', 'cre_group', 'cre_density',
+ 'cre_filters', 'cre_scenario', 'cre_heat', 'cre_shortlist'];
+
+ // Snapshot the current page's restorable state: path + querystring (filters sync to the URL) plus
+ // the layout localStorage keys (field visibility = cre_fields, column order, view/group/density…)
+ // and every `navagent:` key. Stored verbatim inside a saved search's `filters` object.
+ function captureSnapshot() {
+ var layout = {};
+ try {
+ LAYOUT_KEYS.forEach(function (k) { var v = localStorage.getItem(k); if (v != null) layout[k] = v; });
+ for (var i = 0; i < localStorage.length; i++) {
+ var key = localStorage.key(i);
+ if (key && key.indexOf('navagent:') === 0) { var val = localStorage.getItem(key); if (val != null) layout[key] = val; }
+ }
+ } catch (e) {}
+ return { v: 1, path: location.pathname, qs: location.search || '', layout: layout };
+ }
+
+ // Restore a saved search's `filters`. New records = {v,path,qs,layout}; legacy records (e.g. the
+ // seeded {use,uMin} deals-flow default) carry no navigable target → do nothing destructive and
+ // return false so the caller can tell the user. Writes layout keys back, then navigates/reloads.
+ function restoreSnapshot(f) {
+ if (!f || typeof f !== 'object') return false;
+ if (f.layout && typeof f.layout === 'object') {
+ try { Object.keys(f.layout).forEach(function (k) { try { localStorage.setItem(k, String(f.layout[k])); } catch (e) {} }); } catch (e) {}
+ }
+ var hasPath = typeof f.path === 'string', hasQs = typeof f.qs === 'string';
+ if (!hasPath && !hasQs) return false;
+ var target = (hasPath ? f.path : location.pathname) + (hasQs ? f.qs : '');
+ var cur = location.pathname + (location.search || '');
+ if (target === cur) location.reload(); else location.assign(target);
+ return true;
+ }
+
+ function savedRowsHtml(arr) {
+ return arr.map(function (s) {
+ var star = s.is_default ? '<span class="ssdef" title="Default">★</span>' : '';
+ return '<div class="ssrow" data-id="' + esc(s.id) + '">' +
+ '<button class="ssname" type="button" title="Restore this view">' + star + '📁 ' + esc(s.name) + '</button>' +
+ '<button class="ssdel" type="button" title="Delete saved search" aria-label="Delete saved search">✕</button>' +
+ '</div>';
+ }).join('');
+ }
+
var PERSON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" ' +
'stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>';
@@ -48,7 +95,23 @@
'#crcp-ub .divider{height:1px;background:#232c3b;margin:10px -14px}' +
'#crcp-ub .out{display:block;width:100%;text-align:left;background:none;border:0;color:#3b7dff;' +
'font-size:13px;cursor:pointer;padding:2px 0}' +
- '#crcp-ub .out:hover{text-decoration:underline}';
+ '#crcp-ub .out:hover{text-decoration:underline}' +
+ '#crcp-ub .ss-h{color:#d9b25f;font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;margin:2px 0 6px}' +
+ '#crcp-ub .sslist{max-height:190px;overflow-y:auto;margin:0 -4px 8px;padding:0 4px}' +
+ '#crcp-ub .ssrow{display:flex;align-items:center;gap:6px;margin-bottom:4px}' +
+ '#crcp-ub .ssname{flex:1;min-width:0;text-align:left;background:#0e131c;border:1px solid #232c3b;color:#eef2f8;' +
+ 'border-radius:8px;padding:6px 9px;font-size:12px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
+ '#crcp-ub .ssname:hover{border-color:#d9b25f;color:#d9b25f}' +
+ '#crcp-ub .ssdef{color:#d9b25f;margin-right:4px}' +
+ '#crcp-ub .ssdel{background:none;border:0;color:#8b97a8;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px;flex:0 0 auto}' +
+ '#crcp-ub .ssdel:hover{color:#ff6b6b}' +
+ '#crcp-ub .ssdel:disabled{opacity:.4;cursor:default}' +
+ '#crcp-ub .ssempty{color:#8b97a8;font-size:11px;padding:2px 0}' +
+ '#crcp-ub .ssmsg{color:#ffb27a;font-size:11px;padding:2px 0}' +
+ '#crcp-ub .sssave{display:block;width:100%;text-align:left;background:none;border:0;color:#3b7dff;' +
+ 'font-size:13px;cursor:pointer;padding:2px 0}' +
+ '#crcp-ub .sssave:hover{text-decoration:underline}' +
+ '#crcp-ub .sssave:disabled{opacity:.5;cursor:default;text-decoration:none}';
var s = document.createElement('style'); s.id = 'crcp-ub-style'; s.textContent = css;
document.head.appendChild(s);
}
@@ -73,14 +136,88 @@
(role ? '<div class="mline">' + esc(role) + '</div>' : '') +
'<div class="mline">' + esc(me.email || me.username) + '</div>' +
'<div class="divider"></div>' +
+ '<div class="ss-h">Saved Searches</div>' +
+ '<div class="sslist" id="crcp-ss-list"><div class="ssempty">Loading…</div></div>' +
+ '<button class="sssave" type="button">💾 Save current view</button>' +
+ '<div class="ssmsg" id="crcp-ss-msg" style="display:none"></div>' +
+ '<div class="divider"></div>' +
'<button class="out" type="button">Sign out</button>' +
'</div>';
document.body.appendChild(wrap);
var pill = wrap.querySelector('.pill');
+
+ // ── Saved Searches wiring (per-user, server-backed via /api/saved-searches) ──
+ var ssList = wrap.querySelector('#crcp-ss-list');
+ var ssSaveBtn = wrap.querySelector('.sssave');
+ var ssMsg = wrap.querySelector('#crcp-ss-msg');
+ var savedCache = [];
+ var ssLoaded = false, ssMsgTimer = null;
+ function ssFlash(text) {
+ if (!ssMsg) return;
+ ssMsg.textContent = text; ssMsg.style.display = 'block';
+ if (ssMsgTimer) clearTimeout(ssMsgTimer);
+ ssMsgTimer = setTimeout(function () { ssMsg.style.display = 'none'; }, 2600);
+ }
+ function ssPaint() {
+ ssList.innerHTML = savedCache.length ? savedRowsHtml(savedCache)
+ : '<div class="ssempty">None yet — save your current view.</div>';
+ }
+ function ssRefresh() {
+ ssList.innerHTML = '<div class="ssempty">Loading…</div>';
+ fetch('/api/saved-searches', { headers: { Accept: 'application/json' } })
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (j) {
+ if (!j || !Array.isArray(j.saved)) { ssList.innerHTML = '<div class="ssmsg">Couldn’t load saved searches.</div>'; savedCache = []; return; }
+ savedCache = j.saved; ssPaint();
+ })
+ .catch(function () { ssList.innerHTML = '<div class="ssmsg">Couldn’t load saved searches.</div>'; });
+ }
+ if (ssList) {
+ ssList.addEventListener('click', function (e) {
+ e.stopPropagation();
+ var del = e.target.closest('.ssdel');
+ if (del) {
+ var drow = del.closest('.ssrow'); var did = drow && drow.getAttribute('data-id'); if (!did) return;
+ del.disabled = true;
+ fetch('/api/saved-searches/' + encodeURIComponent(did), { method: 'DELETE', headers: { Accept: 'application/json' } })
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function () { savedCache = savedCache.filter(function (s) { return s.id !== did; }); ssPaint(); })
+ .catch(function () { del.disabled = false; });
+ return;
+ }
+ var nm = e.target.closest('.ssname');
+ if (nm) {
+ var nrow = nm.closest('.ssrow'); var nid = nrow && nrow.getAttribute('data-id');
+ var rec = savedCache.filter(function (s) { return s.id === nid; })[0];
+ if (rec && !restoreSnapshot(rec.filters)) ssFlash('This saved item has no restorable view.');
+ }
+ });
+ }
+ if (ssSaveBtn) {
+ ssSaveBtn.addEventListener('click', function (e) {
+ e.stopPropagation();
+ var name = (window.prompt('Name this saved search:', 'Saved search') || '').trim();
+ if (!name) return;
+ ssSaveBtn.disabled = true;
+ fetch('/api/saved-searches', {
+ method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ body: JSON.stringify({ name: name.slice(0, 80), filters: captureSnapshot() })
+ })
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (j) {
+ ssSaveBtn.disabled = false;
+ if (j && j.search) { savedCache.unshift(j.search); ssLoaded = true; ssPaint(); ssFlash('Saved.'); }
+ else ssFlash('Save failed.');
+ })
+ .catch(function () { ssSaveBtn.disabled = false; ssFlash('Save failed.'); });
+ });
+ }
+
var toggle = function () {
var open = wrap.classList.toggle('open');
pill.setAttribute('aria-expanded', open ? 'true' : 'false');
+ if (open && !ssLoaded) { ssLoaded = true; ssRefresh(); } // lazy-load on first open
};
pill.addEventListener('click', function (e) { e.stopPropagation(); toggle(); });
pill.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
← 12e1254 corner-nav: ghost/outline menu pill (transparent bg, slate 1
·
back to Commercialrealestate
·
auto-data-snapshot: 2026-08-26T12:08:58 (1 data files) — dat 5688317 →