[object Object]

← back to Ca Donations

ca-donations: href-to-deeper-data — chips are real drill anchors + URL-synced tab/filter state (deep-link/back-button), clears href-drill regression

6af8822a0cb1d4f0a0cc0a96ddef17d82a85c7e7 · 2026-08-23 07:48:35 -0700 · steve

Files touched

Diff

commit 6af8822a0cb1d4f0a0cc0a96ddef17d82a85c7e7
Author: steve <steve@designerwallcoverings.com>
Date:   Sun Aug 23 07:48:35 2026 -0700

    ca-donations: href-to-deeper-data — chips are real drill anchors + URL-synced tab/filter state (deep-link/back-button), clears href-drill regression
---
 public/index.html | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/public/index.html b/public/index.html
index 913596c..81fde10 100644
--- a/public/index.html
+++ b/public/index.html
@@ -85,10 +85,18 @@ const FILTERS = {
 function chip(lbl, val, onClick) {
   if (val == null || val === '') return '';
   const safe = String(val).replace(/</g,'&lt;');
-  return `<span class="chip" data-click='${JSON.stringify(onClick)}'><span class="lbl">${lbl}</span>${safe}</span>`;
+  return `<a class="chip drill" data-filter="1" href="${hrefFor(onClick)}" data-click='${JSON.stringify(onClick)}'><span class="lbl">${lbl}</span>${safe}</a>`;
 }
 const money = (n) => n == null ? '' : '$' + Number(n).toLocaleString(undefined,{maximumFractionDigits:0});
 
+// ── href-to-deeper-data primitives (page-native over tab+state) — HARD rule 2026-07-31 ──
+// Every chip is a real, shareable href to its refined view (deep-link + back-button).
+function qstr(){ const st=state[tab]; const u=new URLSearchParams(); u.set('tab',tab); if(st.q)u.set('q',st.q); if(st.filter)u.set('filter',st.filter); if(st.sort)u.set('sort',st.sort); return '?'+u.toString(); }
+function hrefFor(o){ const t=(o&&o.t)||tab; const st={...state[t]}; if(o){ if(o.q!==undefined)st.q=o.q; if(o.filter!==undefined)st.filter=o.filter; if(o.filterNtee!==undefined)st.q=o.filterNtee; } const u=new URLSearchParams(); u.set('tab',t); if(st.q)u.set('q',st.q); if(st.filter)u.set('filter',st.filter); if(st.sort)u.set('sort',st.sort); return '?'+u.toString(); }
+function readURL(){ const u=new URLSearchParams(location.search); const t=u.get('tab'); if(t&&state[t])tab=t; const st=state[tab]; if(u.has('q'))st.q=u.get('q'); if(u.has('filter'))st.filter=u.get('filter'); if(u.has('sort'))st.sort=u.get('sort'); }
+function writeURL(push){ history[push?'pushState':'replaceState']({},'',qstr()); }
+window.qstr=qstr; window.readURL=readURL; window.writeURL=writeURL; window.hrefFor=hrefFor;
+
 async function loadStats() {
   const s = await (await fetch('/api/stats')).json();
   $('#stats').innerHTML =
@@ -106,6 +114,7 @@ function syncControls() {
 
 async function render() {
   const st = state[tab];
+  try { writeURL(false); } catch(_) {}
   let url, cards;
   if (tab === 'orgs') {
     url = `/api/orgs?q=${encodeURIComponent(st.q)}&status=${st.filter}&sort=${st.sort}&limit=200`;
@@ -114,8 +123,8 @@ async function render() {
       <div class="kv">
         ${chip('EIN', r.ein, {t:'orgs',q:r.ein})}
         ${chip('City', r.city, {t:'orgs',q:r.city})}
-        ${r.ntee_code ? `<span class="chip" data-click='${JSON.stringify({t:'orgs',filterNtee:r.ntee_code})}'><span class="lbl">NTEE</span>${r.ntee_code}</span>`:''}
-        ${r.ca_ag_status ? `<span class="chip status-${r.ca_ag_status}" data-click='${JSON.stringify({t:'orgs',filter:r.ca_ag_status})}'><span class="lbl">CA AG</span>${r.ca_ag_status}</span>`:''}
+        ${r.ntee_code ? `<a class="chip drill" data-filter="1" href="${hrefFor({t:'orgs',filterNtee:r.ntee_code})}" data-click='${JSON.stringify({t:'orgs',filterNtee:r.ntee_code})}'><span class="lbl">NTEE</span>${r.ntee_code}</a>`:''}
+        ${r.ca_ag_status ? `<a class="chip drill status-${r.ca_ag_status}" data-filter="1" href="${hrefFor({t:'orgs',filter:r.ca_ag_status})}" data-click='${JSON.stringify({t:'orgs',filter:r.ca_ag_status})}'><span class="lbl">CA AG</span>${r.ca_ag_status}</a>`:''}
         ${chip('Grants', 'view →', {t:'grants',q:r.name})}
       </div></div>`).join('');
     view.innerHTML = rows.length ? `<div class="grid">${cards}</div>` : `<div class="empty">No orgs match.</div>`;
@@ -150,6 +159,8 @@ async function render() {
 // href-drill: clicking any chip refines the query.
 view.addEventListener('click', (e) => {
   const el = e.target.closest('.chip'); if (!el || !el.dataset.click) return;
+  if (e.metaKey || e.ctrlKey) return; // let cmd/ctrl-click open the deep-link in a new tab
+  e.preventDefault();
   const o = JSON.parse(el.dataset.click);
   if (o.t) tab = o.t;
   document.querySelectorAll('.tabs button').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
@@ -169,6 +180,8 @@ document.querySelectorAll('.tabs button').forEach(b => b.onclick = () => {
   syncControls(); render();
 });
 
+try { readURL(); } catch(_) {}
+window.addEventListener('popstate', () => { try{readURL();}catch(_){} document.querySelectorAll('.tabs button').forEach(b => b.classList.toggle('active', b.dataset.tab === tab)); syncControls(); render(); });
 syncControls(); loadStats(); render();
 </script>
 </body>

← b63091b auto-data-snapshot: 2026-08-22T18:14:40 (1 data files) — dat  ·  back to Ca Donations  ·  auto-data-snapshot: 2026-08-23T09:39:07 (1 data files) — dat 30429c4 →