← back to Costa Rica
costa-rica: adopt href-to-deeper-data primitives (TK-10093)
b3cbf1b496b6e798ed8ffa9a206f8730526faff7 · 2026-08-01 20:19:18 -0700 · Steve
Files touched
A public/drill.jsM public/index.html
Diff
commit b3cbf1b496b6e798ed8ffa9a206f8730526faff7
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Aug 1 20:19:18 2026 -0700
costa-rica: adopt href-to-deeper-data primitives (TK-10093)
---
public/drill.js | 36 +++++++++++++++++++++
public/index.html | 94 ++++++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 118 insertions(+), 12 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 c54e836..128dc5d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -64,6 +64,10 @@
footer{max-width:1320px;margin:0 auto;padding:30px 22px 60px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);margin-top:30px}
footer a{color:var(--accent)}
.empty{padding:60px 0;text-align:center;color:var(--muted)}
+ /* ── drill atom: every data point filters to its deeper view (card is an <a>, atoms are <span>) ── */
+ .drill{color:inherit;cursor:pointer;border-bottom:1px dotted transparent}
+ .drill:hover{border-bottom-color:currentColor;opacity:.85}
+ .cat .drill:hover,.where .drill:hover{color:var(--accent)}
</style>
</head>
<body>
@@ -126,6 +130,7 @@
<div>© Costa Rica Directory. Aggregated public-record data; we do not host owner-operated content without permission. Owner of a listing? <a href="mailto:info@agentabrams.com">info@agentabrams.com</a>.</div>
</footer>
+ <script src="/drill.js"></script>
<script>
const $ = (s,r=document)=>r.querySelector(s);
const $$ = (s,r=document)=>[...r.querySelectorAll(s)];
@@ -135,10 +140,67 @@
q: localStorage.getItem('cr-q') || '',
cat: '',
region: '',
+ vertical: '',
sort: localStorage.getItem('cr-sort') || 'newest',
cols: parseInt(localStorage.getItem('cr-cols') || '4', 10),
};
+ // ── href-to-deeper-data adapter (client state model) ──────────────────────
+ // Filters live in a plain `state` object (q/category/region/vertical), not a
+ // FIELDS/FIL set — so override the drill primitives to map each facet field to
+ // state, keeping the <span class="drill" data-filter data-v> atom contract.
+ // The card is an <a>, so in-card atoms are <span> (delegated + stopPropagation),
+ // never nested <a>. Only EXISTING on-card values are linkified.
+ function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
+ const DFIELDS=['q','category','region','vertical']; // drill field -> state key (cat maps to category)
+ function stateVal(k){ return k==='category' ? state.cat : state[k]; }
+ function drillState(over){
+ const s={q:state.q,category:state.cat,region:state.region,vertical:state.vertical};
+ if(over)Object.assign(s,over); return s;
+ }
+ function drillURL(s){
+ const u=new URLSearchParams();
+ for(const k of DFIELDS)if(s[k])u.set(k,s[k]);
+ if(state.sort&&state.sort!=='newest')u.set('sort',state.sort);
+ const q=u.toString(); return q?('?'+q):location.pathname;
+ }
+ // drill(field,value,label,cls) → in-card <span> whose data IS the filtered view.
+ // Not an <a> (card is already an anchor); delegated click applies it in place.
+ function drill(field,val,label,cls){
+ if(val==null||val==='')return esc(label!=null?label:val);
+ return `<span class="drill ${cls||''}" role="link" tabindex="0" data-filter="${field}" data-v="${esc(String(val))}" title="Filter to ${esc(String(label!=null?label:val))}">${esc(String(label!=null?label:val))}</span>`;
+ }
+ function setF(field,val){
+ if(field==='category'){ state.cat=String(val); syncCatNav(); }
+ else state[field]=String(val);
+ writeURL(true); load();
+ }
+ function syncCatNav(){
+ $$('.nav-links a').forEach(x=>x.classList.toggle('active',(x.dataset.cat||'')===state.cat));
+ }
+ function readURL(){
+ const u=new URLSearchParams(location.search);
+ state.q=u.get('q')||''; state.cat=u.get('category')||'';
+ state.region=u.get('region')||''; state.vertical=u.get('vertical')||'';
+ if(u.get('sort'))state.sort=u.get('sort');
+ }
+ function writeURL(push){ history[push?'pushState':'replaceState']({},'',drillURL(drillState())); }
+ // delegated: plain click / Enter on a drill atom filters in place; the card
+ // <a> navigation is suppressed via preventDefault + stopPropagation.
+ grid.addEventListener('click',e=>{
+ const d=e.target.closest('span.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);
+ });
+ grid.addEventListener('keydown',e=>{
+ if(e.key!=='Enter'&&e.key!==' ')return;
+ const d=e.target.closest('span.drill'); if(!d)return;
+ e.preventDefault(); e.stopPropagation();
+ setF(d.dataset.filter,d.dataset.v);
+ });
+ addEventListener('popstate',()=>{ readURL(); $('#q').value=state.q; $('#sort').value=state.sort; renderRegions(window.__regions__||[]); syncCatNav(); load(); });
+
function applyTheme(){
const cur = document.documentElement.getAttribute('data-theme') || 'light';
$('#i-sun').style.display = cur === 'dark' ? '' : 'none';
@@ -162,13 +224,13 @@
});
$('#sort').value = state.sort;
- $('#sort').addEventListener('change', e => { state.sort = e.target.value; localStorage.setItem('cr-sort', state.sort); load(); });
+ $('#sort').addEventListener('change', e => { state.sort = e.target.value; localStorage.setItem('cr-sort', state.sort); writeURL(true); load(); });
$('#q').value = state.q;
let qDebounce;
$('#q').addEventListener('input', e => {
clearTimeout(qDebounce);
- qDebounce = setTimeout(() => { state.q = e.target.value.trim(); localStorage.setItem('cr-q', state.q); load(); }, 220);
+ qDebounce = setTimeout(() => { state.q = e.target.value.trim(); localStorage.setItem('cr-q', state.q); writeURL(false); load(); }, 220);
});
$$('.nav-links a').forEach(a => a.addEventListener('click', e => {
@@ -176,14 +238,14 @@
$$('.nav-links a').forEach(x => x.classList.remove('active'));
a.classList.add('active');
state.cat = a.dataset.cat || '';
- load();
+ writeURL(true); load();
}));
function regionPill(slug, name, n, active){
const a = document.createElement('button');
a.className = 'pill' + (active ? ' active' : '');
a.innerHTML = `${name}<span class="n">${n||0}</span>`;
- a.addEventListener('click', () => { state.region = active ? '' : slug; renderRegions(window.__regions__); load(); });
+ a.addEventListener('click', () => { state.region = active ? '' : slug; renderRegions(window.__regions__); writeURL(true); load(); });
return a;
}
@@ -212,13 +274,18 @@
const img = heroSrc
? `<div class="img"><img loading="lazy" src="${heroSrc}" alt=""></div>`
: `<div class="img">⛰</div>`;
- const region = p.region_name ? `${p.region_name}${p.province?', '+p.province:''}` : (p.address || '');
+ // region drills on the region SLUG (server filters r.slug); province appended as plain text
+ const regionDrill = p.region_name && p.region_slug
+ ? drill('region', p.region_slug, p.region_name) + (p.province ? ', ' + esc(p.province) : '')
+ : esc(p.region_name ? `${p.region_name}${p.province?', '+p.province:''}` : (p.address || ''));
+ // vertical (the .cat label) drills on the raw p.vertical value; label is the spaced form
+ const catDrill = p.vertical ? drill('vertical', p.vertical, verticalLabel) : esc(verticalLabel);
wrap.innerHTML = `
${img}
<div class="body">
- <div class="cat">${verticalLabel}</div>
- <div class="name">${p.name}</div>
- <div class="where">${region}</div>
+ <div class="cat">${catDrill}</div>
+ <div class="name">${esc(p.name)}</div>
+ <div class="where">${regionDrill}</div>
<div class="desc">${p.description||''}</div>
<div class="row">
<span class="badge ${p.verified?'verified':''}">${p.verified?'verified':p.source||'pending'}</span>
@@ -232,10 +299,11 @@
async function load(){
const params = new URLSearchParams();
- if (state.q) params.set('q', state.q);
- if (state.cat) params.set('category', state.cat);
- if (state.region) params.set('region', state.region);
- if (state.sort) params.set('sort', state.sort);
+ if (state.q) params.set('q', state.q);
+ if (state.cat) params.set('category', state.cat);
+ if (state.region) params.set('region', state.region);
+ if (state.vertical) params.set('vertical', state.vertical);
+ if (state.sort) params.set('sort', state.sort);
params.set('limit','120');
meta.textContent = 'Loading…';
try {
@@ -253,6 +321,8 @@
}
}
+ // deep-link: hydrate state from the URL before first load (URL wins over localStorage)
+ readURL(); $('#q').value=state.q; $('#sort').value=state.sort; syncCatNav();
loadRegions().then(load);
</script>
</body>
← a52fd3f Card badges for website/phone presence (site vs social disti
·
back to Costa Rica
·
costa-rica: escape p.description in card innerHTML (Cody ARG 8c8ebde →