[object Object]

← back to Designersalesreps

designersalesreps: adopt href-to-deeper-data primitives (TK-10093)

3c253cc26e2f090c0a432933dbbdb1f1ad713cd8 · 2026-08-01 20:14:29 -0700 · Steve Abrams

Files touched

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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 →