[object Object]

← 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 →