← back to Designersalesreps
designersalesreps: adopt href-to-deeper-data primitives (TK-10093)
3c253cc26e2f090c0a432933dbbdb1f1ad713cd8 · 2026-08-01 20:14:29 -0700 · Steve Abrams
Files touched
A public/drill.jsM public/index.html
Diff
commit 3c253cc26e2f090c0a432933dbbdb1f1ad713cd8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 20:14:29 2026 -0700
designersalesreps: adopt href-to-deeper-data primitives (TK-10093)
---
public/drill.js | 36 +++++++++++++++++++++++++++++
public/index.html | 68 +++++++++++++++++++++++++++++++++++++++++++++++--------
2 files changed, 95 insertions(+), 9 deletions(-)
diff --git a/public/drill.js b/public/drill.js
new file mode 100644
index 0000000..e63cdd3
--- /dev/null
+++ b/public/drill.js
@@ -0,0 +1,36 @@
+// ── href-to-deeper-data primitives — SINGLE SOURCE OF TRUTH (HARD rule, 2026-07-31) ──
+// Every displayed data point must be an href to its deeper (filtered) data. These are the
+// reusable atoms; a viewer's index.html loads this file, then defines FIELDS, FIL, $, esc,
+// and applyFilters (which these reference at call time via the shared classic-script global
+// scope). Canonical copy: ~/Projects/_shared/web/drill.js → each viewer's public/drill.js.
+// Memory: all-data-points-href-to-deeper-data. Reference viewers: astek-landing, schumacher-internal.
+
+// Build the URL for a given filter set — any count/atom becomes a real, shareable link.
+function qstr(fil, q, pat) {
+ const u = new URLSearchParams();
+ for (const [k] of FIELDS) { if (fil[k]) u.set(k, fil[k]); }
+ if (q) u.set('q', q); if (pat) u.set('pat', pat);
+ const s = u.toString(); return s ? ('?' + s) : location.pathname;
+}
+// Read the URL's filter state back into FIL + the search inputs (deep-link / back-button).
+function readURL() {
+ const u = new URLSearchParams(location.search);
+ for (const [k] of FIELDS) FIL[k] = u.get(k) || '';
+ $('#q').value = u.get('q') || ''; $('#pattern').value = u.get('pat') || '';
+}
+// Reflect the current filter state into the URL (push = new history entry, else replace).
+function writeURL(push) {
+ const url = qstr(FIL, $('#q').value.trim(), $('#pattern').value.trim());
+ history[push ? 'pushState' : 'replaceState']({}, '', url);
+}
+// drill(field,value,label,cls) → an <a> whose href IS the filtered view for that value.
+// cmd/ctrl/middle-click opens it in a new tab; plain click filters in place (delegated listener).
+function drill(field, val, label, cls) {
+ if (val == null || val === '') return '';
+ const href = qstr({ ...FIL, [field]: String(val) }, $('#q').value.trim(), $('#pattern').value.trim());
+ return `<a class="drill ${cls || ''}" href="${href}" data-filter="${field}" data-v="${esc(val)}" title="Filter to ${esc(val)}">${label != null ? esc(label) : esc(val)}</a>`;
+}
+// Atom drill target: SET the filter to this value (drill in), vs pickF which TOGGLES.
+function setF(k, v) { if (FIL[k] === String(v)) return; FIL[k] = String(v); applyFilters('push'); }
+
+window.qstr = qstr; window.readURL = readURL; window.writeURL = writeURL; window.drill = drill; window.setF = setF;
diff --git a/public/index.html b/public/index.html
index b9dcb50..e2c47ac 100644
--- a/public/index.html
+++ b/public/index.html
@@ -88,6 +88,10 @@ select.filter:focus{border-color:var(--accent)}
.modal{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:2rem;max-width:480px;width:90%;position:relative}
.modal h3{font-family:'Cormorant Garamond',Georgia,serif;font-size:1.5rem;margin-bottom:1rem}
.modal-close{position:absolute;top:1rem;right:1rem;background:none;border:none;color:var(--ink-soft);font-size:1.5rem;cursor:pointer;line-height:1}
+/* ── drill atom: every data point hrefs to its filtered view (deeper data) ── */
+.drill{color:inherit;text-decoration:none;cursor:pointer;border-bottom:1px dotted transparent}
+.drill:hover{color:var(--accent);border-bottom-color:var(--accent)}
+.tag.drill:hover{color:var(--accent)}
</style>
</head>
<body>
@@ -171,6 +175,7 @@ select.filter:focus{border-color:var(--accent)}
</div>
</div>
+<script src="/drill.js"></script>
<script>
const grid = document.getElementById('grid');
const countEl = document.getElementById('count');
@@ -180,6 +185,47 @@ const vertEl = document.getElementById('vertical');
const sortEl = document.getElementById('sort');
const densityEl = document.getElementById('density');
+// ── href-to-deeper-data adapter (server-paged select model) ──────────────────
+// This site's filters live in <select id=territory|vertical> + <input id=q>, not
+// a client-side FIELDS/FIL set — so override the drill primitives to map each
+// facet field to its control, keeping the <a class="drill" data-filter data-v href>
+// atom contract from /drill.js. Only EXISTING on-card values are linkified.
+function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
+const CTRL={q:qEl,territory:terrEl,vertical:vertEl}; // drill field -> control element
+function curState(over){
+ const s={q:qEl.value.trim(),territory:terrEl.value,vertical:vertEl.value};
+ if(over)Object.assign(s,over); return s;
+}
+function stateURL(s){
+ const u=new URLSearchParams();
+ for(const k of ['q','territory','vertical'])if(s[k])u.set(k,s[k]);
+ const q=u.toString(); return q?('?'+q):location.pathname;
+}
+// drill(field,value,label,cls) → <a> whose href IS the filtered view for that value
+function drill(field,val,label,cls){
+ if(val==null||val==='')return '';
+ const href=stateURL(curState({[field]:String(val)}));
+ return `<a class="drill ${cls||''}" href="${href}" data-filter="${field}" data-v="${esc(val)}" title="Filter to ${esc(val)}">${label!=null?esc(label):esc(val)}</a>`;
+}
+// setF — set a facet control to a value and reload (atom drill target)
+function setF(field,val){
+ const el=CTRL[field]; if(!el)return;
+ if(field==='q'){ el.value=val; } else { el.value=val; }
+ writeURL(true); load();
+}
+function readURL(){
+ const u=new URLSearchParams(location.search);
+ qEl.value=u.get('q')||''; terrEl.value=u.get('territory')||''; vertEl.value=u.get('vertical')||'';
+}
+function writeURL(push){ history[push?'pushState':'replaceState']({},'',stateURL(curState())); }
+// delegated: plain click on a drill atom filters in place; modified-click keeps the real href
+grid.addEventListener('click',e=>{
+ const d=e.target.closest('a.drill'); if(!d)return;
+ if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button===1)return;
+ e.preventDefault(); e.stopPropagation();
+ setF(d.dataset.filter,d.dataset.v);
+});
+
let debounceT;
function debounce(fn, ms=300){ clearTimeout(debounceT); debounceT=setTimeout(fn,ms); }
@@ -190,15 +236,15 @@ function load(){
grid.innerHTML = data.results.map(r=>`
<div class="card" onclick="showModal(${r.id})" data-id="${r.id}">
<div class="card-accent"></div>
- <div class="card-name">${r.name}</div>
- <div class="card-agency">${r.agency}</div>
+ <div class="card-name">${esc(r.name)}</div>
+ <div class="card-agency">${drill('q', r.agency, r.agency, 'card-agency-drill')}</div>
<div class="card-meta">
- <div class="meta-row"><span class="meta-label">City</span>${r.city}</div>
- <div class="meta-row"><span class="meta-label">Lines</span>${r.lines.slice(0,2).join(', ')}${r.lines.length>2?' +'+( r.lines.length-2)+' more':''}</div>
+ <div class="meta-row"><span class="meta-label">City</span>${drill('q', r.city, r.city)}</div>
+ <div class="meta-row"><span class="meta-label">Lines</span>${r.lines.slice(0,2).map(l=>drill('q',l,l)).join(', ')}${r.lines.length>2?' +'+( r.lines.length-2)+' more':''}</div>
</div>
<div class="tags">
- ${r.territories.map(t=>`<span class="tag territory">${t}</span>`).join('')}
- ${r.vertical.map(v=>`<span class="tag">${v}</span>`).join('')}
+ ${r.territories.map(t=>drill('territory',t,t,'tag territory')).join('')}
+ ${r.vertical.map(v=>drill('vertical',v,v,'tag')).join('')}
</div>
<button class="claim-btn" onclick="event.stopPropagation();document.getElementById('claim').scrollIntoView({behavior:'smooth'})">Claim / Update Listing</button>
</div>
@@ -206,16 +252,20 @@ function load(){
});
}
-qEl.addEventListener('input',()=>debounce(load));
-terrEl.addEventListener('change',load);
-vertEl.addEventListener('change',load);
+qEl.addEventListener('input',()=>debounce(()=>{writeURL(false);load();}));
+terrEl.addEventListener('change',()=>{writeURL(true);load();});
+vertEl.addEventListener('change',()=>{writeURL(true);load();});
sortEl.addEventListener('change',()=>{localStorage.setItem('dsr-sort',sortEl.value);load();});
+addEventListener('popstate',()=>{readURL();load();});
densityEl.addEventListener('input',()=>{document.querySelector('.grid').style.setProperty('--card-min',densityEl.value+'px');localStorage.setItem('dsr-density',densityEl.value);});
// restore prefs
const sv=localStorage.getItem('dsr-sort'); if(sv) sortEl.value=sv;
const dv=localStorage.getItem('dsr-density'); if(dv){densityEl.value=dv;grid.style.setProperty('--card-min',dv+'px');}
+// deep-link: hydrate filter controls from the URL before first load
+readURL();
+
let allReps=[];
fetch('/api/search?').then(r=>r.json()).then(d=>{allReps=d.results;});
← 37ed884 chore: v1.0.1 — go-live (CF-proxied, live on prod)
·
back to Designersalesreps
·
viewer standard: add Grid/List toggle + per-field on/off men 7accefe →