[object Object]

← back to Li Followhub

auto-save: 2026-07-31T14:58:42 (1 files) — public/index.html

7f199c89a31eda3e412c1ef83e2c58e07cf6cfb6 · 2026-07-31 14:58:43 -0700 · Steve Abrams

Files touched

Diff

commit 7f199c89a31eda3e412c1ef83e2c58e07cf6cfb6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 31 14:58:43 2026 -0700

    auto-save: 2026-07-31T14:58:42 (1 files) — public/index.html
---
 public/index.html | 119 ++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 107 insertions(+), 12 deletions(-)

diff --git a/public/index.html b/public/index.html
index f2c8bc9..cc3a7f6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -70,6 +70,17 @@
   .chips a{display:inline-block;background:var(--li);color:#fff;padding:7px 13px;border-radius:20px;
            margin:5px 8px 5px 0;font-weight:600;font-size:13px;text-decoration:none}
   .chips a:hover{background:var(--li-d)}
+  /* drill links — every displayed data point is an <a href> to its filtered view */
+  a.drill{color:inherit;text-decoration:none;cursor:pointer}
+  a.drill:hover{text-decoration:underline}
+  .card .nm a.drill:hover{color:var(--li)}
+  .tag a.drill,.when a.drill{color:inherit}
+  .tag a.drill:hover,.when a.drill:hover{text-decoration:underline}
+  .activefilters{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:2px 0 8px;font-size:12.5px}
+  .activefilters .fpill{background:#eef3f8;border:1px solid var(--line);color:var(--li-d);
+        border-radius:14px;padding:3px 10px;font-weight:600;display:inline-flex;gap:6px;align-items:center}
+  .activefilters .fpill a{color:var(--bad);text-decoration:none;font-weight:800}
+  .activefilters .fclear{color:var(--li);text-decoration:underline;cursor:pointer}
   .empty{color:var(--mut);padding:30px;text-align:center}
   .foot{color:var(--mut);font-size:12px;margin:20px 0}
   h2.sec{font-size:15px;color:var(--li-d);margin:26px 0 4px;border-top:1px solid var(--line);padding-top:18px}
@@ -146,6 +157,7 @@
   </div>
 
   <div class="listnote" id="listnote"></div>
+  <div class="activefilters" id="activefilters"></div>
   <div class="grid" id="grid"></div>
 
   <div id="searches"></div>
@@ -162,13 +174,51 @@ const prefs = {
   density: +(localStorage.getItem('lfh.density') || 4),
   q: '',
 };
+// --- href-drill: URL-addressable, shareable, deep-linkable filter state ---
+const esc0 = s => encodeURIComponent(String(s==null?'':s));
+// drill-only facets (each filters the grid down to matching rows)
+const FIL = { status:'', type:'' };
+const STATUS_LABEL = { done:'Followed', todo:'Not followed' };
+const TYPE_LABEL = { direct:'Direct profile', salesnav:'Sales Nav' };
+// build ?list=&status=&type=&q=&sort=&hideDone= from current controls+FIL, override with `over`
+function qstr(over){
+  const p = Object.assign({ list:activeTab, status:FIL.status, type:FIL.type,
+                            q:prefs.q, sort:prefs.sort, hideDone:prefs.hideDone?'1':'' }, over||{});
+  const qs = [];
+  if(p.list) qs.push('list='+esc0(p.list));
+  if(p.status) qs.push('status='+esc0(p.status));
+  if(p.type) qs.push('type='+esc0(p.type));
+  if(p.q) qs.push('q='+esc0(p.q));
+  if(p.sort && p.sort!=='undone') qs.push('sort='+esc0(p.sort));
+  if(p.hideDone) qs.push('hideDone=1');
+  return qs.length ? '?'+qs.join('&') : location.pathname;
+}
+// a drill atom = an <a href> to the deeper (filtered) view of a data point
+function drill(field, val, label, cls){
+  return `<a class="drill ${cls||''}" data-filter="${field}" data-v="${esc0(val)}" href="${qstr({[field]:val})}">${label}</a>`;
+}
+function readURL(){
+  const p = new URLSearchParams(location.search);
+  if(p.has('list')){ activeTab = p.get('list'); localStorage.setItem('lfh.tab',activeTab); }
+  FIL.status = p.get('status')||'';
+  FIL.type   = p.get('type')||'';
+  prefs.q    = p.get('q')||'';
+  if(p.has('sort')) prefs.sort = p.get('sort');
+  if(p.has('hideDone')) prefs.hideDone = p.get('hideDone')==='1';
+}
+function writeURL(push){
+  const url = qstr();
+  if(push) history.pushState(null,'',url); else history.replaceState(null,'',url);
+}
 
 async function load(){
-  const r = await fetch('/api/data'); STATE = await r.json();
+  const r = await fetch(location.origin + '/api/data'); STATE = await r.json();
   $('#target').value = STATE.dailyTarget;
+  readURL();
   applyPrefs(); renderTabs(); render(); renderSearches(); renderFoot();
 }
 function applyPrefs(){
+  $('#q').value = prefs.q;
   $('#sort').value = prefs.sort;
   $('#hideDone').checked = prefs.hideDone;
   $('#density').value = prefs.density;
@@ -192,7 +242,9 @@ function renderTabs(){
       <span class="c">${done}/${l.items.length}</span></div>`;
   }).join('');
   $('#tabs').querySelectorAll('.tab').forEach(t=>t.onclick=()=>{
-    activeTab=t.dataset.k; localStorage.setItem('lfh.tab',activeTab); renderTabs(); render();
+    activeTab=t.dataset.k; localStorage.setItem('lfh.tab',activeTab);
+    FIL.status=''; FIL.type=''; // reset drill facets when switching lists
+    writeURL(true); renderTabs(); render();
   });
 }
 function currentList(){ return STATE.lists.find(l=>l.key===activeTab) || STATE.lists[0]; }
@@ -202,6 +254,11 @@ function visibleItems(){
   const q = prefs.q.trim().toLowerCase();
   if(q) items = items.filter(i=>(i.name||'').toLowerCase().includes(q));
   if(prefs.hideDone) items = items.filter(i=>!STATE.followed[i.id]);
+  // drill facets
+  if(FIL.status==='done') items = items.filter(i=>!!STATE.followed[i.id]);
+  else if(FIL.status==='todo') items = items.filter(i=>!STATE.followed[i.id]);
+  if(FIL.type==='direct') items = items.filter(i=>i.resolved!==false);
+  else if(FIL.type==='salesnav') items = items.filter(i=>i.resolved===false);
   const done = i => STATE.followed[i.id]?1:0;
   if(prefs.sort==='name') items.sort((a,b)=>(a.name||'').localeCompare(b.name||''));
   else if(prefs.sort==='done') items.sort((a,b)=>done(b)-done(a)||(a.name||'').localeCompare(b.name||''));
@@ -215,27 +272,44 @@ function fmtWhen(ts){
 function render(){
   const l = currentList();
   $('#listnote').textContent = l ? l.note : '';
+  renderActiveFilters();
   const items = visibleItems();
   updateCadence();
-  if(!items.length){ $('#grid').innerHTML = `<div class="empty">Nothing here — try clearing search / “Hide followed”.</div>`; return; }
+  if(!items.length){ $('#grid').innerHTML = `<div class="empty">Nothing here — try clearing search / “Hide followed” / a filter above.</div>`; return; }
   $('#grid').innerHTML = items.map(i=>{
     const f = STATE.followed[i.id];
     const sn = i.resolved===false;
+    // every displayed data point drills to its filtered view
+    const typeTag = sn
+      ? drill('type','salesnav','Sales Nav','tag sn')
+      : (i.softDone&&!f ? '<span class="tag soft">likely already</span>' : drill('type','direct','Direct','tag soft'));
+    const nameDrill = drill('status', f?'done':'todo', escapeHtml(i.name||'(profile)'), '');
     return `<div class="card ${f?'done':''}" data-id="${encodeURIComponent(i.id)}">
-      ${sn?'<span class="tag sn">Sales Nav</span>':(i.softDone&&!f?'<span class="tag soft">likely already</span>':'')}
-      <div class="nm">${escapeHtml(i.name||'(profile)')}</div>
-      ${f?`<div class="when">🕓 followed ${fmtWhen(f.ts)}</div>`:''}
+      ${typeTag}
+      <div class="nm">${nameDrill}</div>
+      ${f?`<div class="when">🕓 ${drill('status','done','followed '+fmtWhen(f.ts))}</div>`:''}
       <div class="row">
         <a class="open ${sn?'salesnav':''}" target="_blank" rel="noopener noreferrer" href="${i.url}">${sn?'SalesNav ↗':'Open ↗'}</a>
         <label class="chk"><input type="checkbox" ${f?'checked':''}> done</label>
       </div>
     </div>`;
   }).join('');
-  $('#grid').querySelectorAll('.card').forEach(c=>{
-    const id = decodeURIComponent(c.dataset.id);
-    c.querySelector('input').onchange = e => toggle(id, e.target.checked);
+  $('#grid').querySelectorAll('.card input[type=checkbox]').forEach(inp=>{
+    const id = decodeURIComponent(inp.closest('.card').dataset.id);
+    inp.onchange = e => toggle(id, e.target.checked);
   });
 }
+function renderActiveFilters(){
+  const parts = [];
+  if(FIL.status) parts.push(`<span class="fpill">${STATUS_LABEL[FIL.status]||FIL.status}
+    <a data-clear="status" href="${qstr({status:''})}" title="clear">✕</a></span>`);
+  if(FIL.type) parts.push(`<span class="fpill">${TYPE_LABEL[FIL.type]||FIL.type}
+    <a data-clear="type" href="${qstr({type:''})}" title="clear">✕</a></span>`);
+  if(prefs.q) parts.push(`<span class="fpill">“${escapeHtml(prefs.q)}”
+    <a data-clear="q" href="${qstr({q:''})}" title="clear">✕</a></span>`);
+  if(parts.length) parts.push(`<a class="fclear" data-clear="all" href="${qstr({status:'',type:'',q:''})}">clear all</a>`);
+  $('#activefilters').innerHTML = parts.length ? '<span style="color:var(--mut)">Filtered:</span> '+parts.join(' ') : '';
+}
 function escapeHtml(s){return String(s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));}
 
 async function toggle(id, done){
@@ -275,9 +349,30 @@ function renderFoot(){
 }
 
 // controls
-$('#q').oninput = e => { prefs.q=e.target.value; render(); };
-$('#sort').onchange = e => { prefs.sort=e.target.value; localStorage.setItem('lfh.sort',prefs.sort); render(); };
-$('#hideDone').onchange = e => { prefs.hideDone=e.target.checked; localStorage.setItem('lfh.hideDone',prefs.hideDone?'1':'0'); render(); };
+$('#q').oninput = e => { prefs.q=e.target.value; writeURL(false); render(); };
+$('#sort').onchange = e => { prefs.sort=e.target.value; localStorage.setItem('lfh.sort',prefs.sort); writeURL(false); render(); };
+$('#hideDone').onchange = e => { prefs.hideDone=e.target.checked; localStorage.setItem('lfh.hideDone',prefs.hideDone?'1':'0'); writeURL(false); render(); };
+
+// delegated drill-click: modified-click opens in a new tab; plain click filters in place + pushes URL
+document.addEventListener('click', e => {
+  const a = e.target.closest('a.drill, .activefilters a[data-clear]');
+  if(!a) return;
+  if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button===1) return; // let href open a new tab
+  e.preventDefault();
+  const clear = a.getAttribute('data-clear');
+  if(clear){
+    if(clear==='all'){ FIL.status=''; FIL.type=''; prefs.q=''; $('#q').value=''; }
+    else if(clear==='q'){ prefs.q=''; $('#q').value=''; }
+    else FIL[clear]='';
+  } else {
+    const field = a.getAttribute('data-filter');
+    const v = decodeURIComponent(a.getAttribute('data-v')||'');
+    if(field in FIL) FIL[field] = (FIL[field]===v ? '' : v); // toggle
+  }
+  writeURL(true); render();
+});
+// back/forward restores the exact filtered set
+addEventListener('popstate', () => { readURL(); applyPrefs(); renderTabs(); render(); });
 $('#density').oninput = e => { prefs.density=+e.target.value; localStorage.setItem('lfh.density',prefs.density);
   document.documentElement.style.setProperty('--cols',prefs.density); };
 $('#target').onchange = async e => {

← 295e155 auto-save: 2026-07-12T08:47:12 (3 files) — public/index.html  ·  back to Li Followhub  ·  (newest)