← back to Sdcc Journalists Viewer
sdcc-journalists-viewer: href-drill — outlet/medium/location/beat are filter-links
bba7aa37b445b7553e9a8a98a1ef9f0bf2747dfd · 2026-07-31 15:34:22 -0700 · Steve Abrams
Each of those data points is now an <a href="?outlet=…|medium=…|location=…|beat=…">
to its filtered view (href-drill HARD rule). Inline primitives (qstr/drill/readURL/
writeURL) added to the search+checkbox model; click filters in place + reflects into
the URL, cmd/ctrl-click opens the shareable href, back-button + deep-link restore via
popstate/readURL. Search stays URL-addressable too.
Verified (Playwright through the /login auth wall, live :9878): click outlet 'Tax Notes'
-> ?outlet=… + grid 38->1; deep-link restores; back-button->38; canary FLAT->DONE;
0 pageerrors. href-drill backlog grid 4/92. TK-10089.
Files touched
Diff
commit bba7aa37b445b7553e9a8a98a1ef9f0bf2747dfd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 31 15:34:22 2026 -0700
sdcc-journalists-viewer: href-drill — outlet/medium/location/beat are filter-links
Each of those data points is now an <a href="?outlet=…|medium=…|location=…|beat=…">
to its filtered view (href-drill HARD rule). Inline primitives (qstr/drill/readURL/
writeURL) added to the search+checkbox model; click filters in place + reflects into
the URL, cmd/ctrl-click opens the shareable href, back-button + deep-link restore via
popstate/readURL. Search stays URL-addressable too.
Verified (Playwright through the /login auth wall, live :9878): click outlet 'Tax Notes'
-> ?outlet=… + grid 38->1; deep-link restores; back-button->38; canary FLAT->DONE;
0 pageerrors. href-drill backlog grid 4/92. TK-10089.
---
public/index.html | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/public/index.html b/public/index.html
index f31330a..8a5f809 100644
--- a/public/index.html
+++ b/public/index.html
@@ -127,6 +127,19 @@ const FIELDS = [
];
const LS = (k,v)=> v===undefined ? localStorage.getItem("sdccjv_"+k) : localStorage.setItem("sdccjv_"+k,v);
let DATA = [], META = {};
+// ── href-to-deeper-data primitives — every data point is an <a href> to its filtered view (URL-addressable) ──
+const escU = s => encodeURIComponent(String(s==null?"":s));
+const FIL = { medium:"", location:"", beat:"", outlet:"" };
+function qstr(over){
+ const q=(document.getElementById("q").value||"").trim();
+ const f=Object.assign({medium:FIL.medium,location:FIL.location,beat:FIL.beat,outlet:FIL.outlet,q},over||{});
+ const u=[]; for(const k of ["medium","location","beat","outlet"]) if(f[k]) u.push(k+"="+escU(f[k])); if(f.q) u.push("q="+escU(f.q));
+ return u.length?("?"+u.join("&")):location.pathname;
+}
+function drill(field,val,label,cls){ if(val==null||val==="") return esc(label!=null?label:val);
+ return `<a class="drill ${cls||''}" data-filter="${field}" data-v="${esc(val)}" href="${esc(qstr({[field]:String(val)}))}" title="Filter to ${esc(val)}">${esc(label!=null?label:val)}</a>`; }
+function readURL(){ const u=new URLSearchParams(location.search); FIL.medium=u.get("medium")||""; FIL.location=u.get("location")||""; FIL.beat=u.get("beat")||""; FIL.outlet=u.get("outlet")||""; const qe=document.getElementById("q"); if(qe) qe.value=u.get("q")||""; }
+function writeURL(push){ history[push?"pushState":"replaceState"]({},"",qstr()); }
// List-view columns — [key, label, type]. Every one is a clickable sort header.
const COLUMNS = [
@@ -179,14 +192,14 @@ function card(r){
const parts = [];
parts.push(`<div class="name">${esc(r.name)}</div>`);
const oc = outletChanged(r);
- parts.push(`<div class="outlet">${esc(r.current_outlet_verified||r.outlet_on_file||"—")}${vis.outletchange&&oc?` <span class="badge stale">moved from ${esc(r.outlet_on_file)}</span>`:""}</div>`);
+ parts.push(`<div class="outlet">${(r.current_outlet_verified||r.outlet_on_file)?drill('outlet',r.current_outlet_verified||r.outlet_on_file):"—"}${vis.outletchange&&oc?` <span class="badge stale">moved from ${esc(r.outlet_on_file)}</span>`:""}</div>`);
const badges=[];
- if(vis.medium&&r.medium) badges.push(`<span class="badge">${esc(r.medium)}</span>`);
- if(vis.location&&r.location) badges.push(`<span class="badge">${esc(r.location)}</span>`);
+ if(vis.medium&&r.medium) badges.push(`<span class="badge">${drill('medium',r.medium)}</span>`);
+ if(vis.location&&r.location) badges.push(`<span class="badge">${drill('location',r.location)}</span>`);
if(vis.tracked&&r.articles_tracked_10yr&&+r.articles_tracked_10yr>0) badges.push(`<span class="badge">${esc(r.articles_tracked_10yr)} tracked</span>`);
if(vis.contact&&r.date_of_last_contact) badges.push(`<span class="badge contact">last contact ${esc(r.date_of_last_contact)}</span>`);
if(badges.length) parts.push(`<div>${badges.join("")}</div>`);
- if(vis.beat&&r.beat) parts.push(`<div class="kv"><b>Beat:</b> ${esc(r.beat)}</div>`);
+ if(vis.beat&&r.beat) parts.push(`<div class="kv"><b>Beat:</b> ${drill('beat',r.beat)}</div>`);
const links=[];
if(vis.email&&r.email) links.push(`<a href="mailto:${esc(r.email)}" title="${esc(r.email_source||"")}">✉️ ${esc(r.email)}</a>`);
@@ -217,6 +230,10 @@ function filteredRows(){
if(fE && !r.email) return false;
if(fC && !r.date_of_last_contact) return false;
if(fA && /stub|deprioritiz|not a (journalist|reporter)|inactive/i.test(r.notes||"")) return false;
+ if(FIL.medium && (r.medium||"")!==FIL.medium) return false;
+ if(FIL.location && (r.location||"")!==FIL.location) return false;
+ if(FIL.beat && (r.beat||"")!==FIL.beat) return false;
+ if(FIL.outlet && (r.current_outlet_verified||r.outlet_on_file||"")!==FIL.outlet) return false;
return true;
});
}
@@ -348,11 +365,16 @@ async function load(){
const t = j.generated_at ? new Date(j.generated_at) : null;
document.getElementById("syncinfo").textContent =
(j.drive_pull_ok?"synced from Drive ":"⚠ Drive unreachable, cached ") + (t?t.toLocaleString(undefined,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"");
+ readURL(); // land in the filter state carried by the URL (deep-link / shareable)
render();
}
-document.getElementById("q").oninput=render;
+document.getElementById("q").oninput=()=>{writeURL(false);render();};
["fEmail","fContact","fActive"].forEach(id=>document.getElementById(id).onchange=render);
+// click a data point → filter in place + reflect into the URL (cmd/ctrl/middle-click opens the shareable href)
+document.addEventListener("click",e=>{ const dr=e.target.closest("a.drill"); if(!dr)return; if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1)return; e.preventDefault();
+ const f=dr.dataset.filter, v=dr.dataset.v; FIL[f]=(FIL[f]===v?"":v); writeURL(true); render(); });
+addEventListener("popstate",()=>{ readURL(); render(); }); // back/forward restores filter state
document.getElementById("sort").onchange=e=>{ LS("sort",e.target.value); render(); };
document.getElementById("density").oninput=e=>{
const v=+e.target.value; LS("density",v);
← 190b76d auto-save: 2026-07-29T20:42:08 (1 files) — data/journalists.
·
back to Sdcc Journalists Viewer
·
auto-save: 2026-07-31T15:59:20 (1 files) — data/journalists. 0018c6f →