← 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=>({'&':'&','<':'<','>':'>','"':'"'}[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)