[object Object]

← back to Greenland Onboard

greenland-onboard: href-drill — city/width/backing are filter-links + URL-addressable

d9b850799cfd28dbd8c1303d8f75ab45d66ac90c · 2026-08-01 01:47:59 -0700 · Steve Abrams

Card data points (city / width / backing — exact-match FIELDS facets) are now
<a href="?city=…"> to their filtered view (href-drill HARD rule); the left-panel
facets + search + pattern are URL-addressable too. Click filters in place + reflects
into the URL, cmd/ctrl-click opens the shareable href, back-button + deep-link restore
via popstate/readURL.

Verified (Playwright + basic auth, live :9973): click city 'Hilton Head' -> ?city + grid
120->8 all-matching; deep-link restores; back-button->120; canary FLAT->DONE; 0 pageerrors.
href-drill backlog grid 6/92. TK-10089.

Files touched

Diff

commit d9b850799cfd28dbd8c1303d8f75ab45d66ac90c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 01:47:59 2026 -0700

    greenland-onboard: href-drill — city/width/backing are filter-links + URL-addressable
    
    Card data points (city / width / backing — exact-match FIELDS facets) are now
    <a href="?city=…"> to their filtered view (href-drill HARD rule); the left-panel
    facets + search + pattern are URL-addressable too. Click filters in place + reflects
    into the URL, cmd/ctrl-click opens the shareable href, back-button + deep-link restore
    via popstate/readURL.
    
    Verified (Playwright + basic auth, live :9973): click city 'Hilton Head' -> ?city + grid
    120->8 all-matching; deep-link restores; back-button->120; canary FLAT->DONE; 0 pageerrors.
    href-drill backlog grid 6/92. TK-10089.
---
 public/index.html | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/public/index.html b/public/index.html
index d551bc0..fc21e92 100644
--- a/public/index.html
+++ b/public/index.html
@@ -224,6 +224,12 @@ const FIELDS=[
 ];
 const FIL={}; FIELDS.forEach(([k])=>FIL[k]='');
 let FCOUNTS={};
+// ── href-drill: FIL/search state is URL-addressable (shareable + deep-link + back-button) ──
+const escU=s=>encodeURIComponent(String(s==null?'':s));
+function qstr(over){ const f=Object.assign({},FIL,over||{}); const u=[]; for(const [k] of FIELDS) if(f[k]) u.push(k+'='+escU(f[k])); const q=($('#q')&&$('#q').value.trim())||'', pat=($('#pattern')&&$('#pattern').value.trim())||''; if(q)u.push('q='+escU(q)); if(pat)u.push('pat='+escU(pat)); return u.length?('?'+u.join('&')):location.pathname; }
+function drill(field,val,label){ if(val==null||val==='')return esc(label!=null?label:val); return `<a class="drill" data-filter="${field}" data-v="${esc(val)}" href="${esc(qstr({[field]:String(val)}))}" title="Filter to ${esc(val)}">${esc(label!=null?label:val)}</a>`; }
+function readURL(){ const u=new URLSearchParams(location.search); for(const [k] of FIELDS) FIL[k]=u.get(k)||''; if($('#q'))$('#q').value=u.get('q')||''; if($('#pattern'))$('#pattern').value=u.get('pat')||''; }
+function writeURL(push){ history[push?'pushState':'replaceState']({},'',qstr()); }
 const FCAP=40;
 $('#sort').value=localStorage.getItem('gl.sort')||'newest';
 
@@ -254,7 +260,7 @@ document.querySelectorAll('#fieldsbox input[data-fv]').forEach(cb=>{ cb.onchange
 applyFieldVis();
 
 function card(p){
-  const cityColor=[p.city&&`<span class="city">${esc(p.city)}</span>`,esc(p.color||p.pattern||p.dw_sku)].filter(Boolean).join(' · ');
+  const cityColor=[p.city&&`<span class="city">${drill('city',p.city)}</span>`,esc(p.color||p.pattern||p.dw_sku)].filter(Boolean).join(' · ');
   const img=p.image
     ?`<div class="imgwrap"><img loading="lazy" src="${esc(p.image)}" alt="${esc(p.title||p.dw_sku)}" onerror="this.style.display='none';this.parentNode.classList.add('noimg')">`
     :`<div class="imgwrap noimg">`;
@@ -263,8 +269,8 @@ function card(p){
     :`<span class="stockbadge un">○ Unlisted</span>`;
   const price=`<span class="pricebadge">$${Number(p.retail_per_yard).toFixed(2)}/yd</span>`;
   const dot=FAM_HEX[p.color_family]||p.hex||'#666';
-  const spec=[p.width,p.backing,p.fire_rating&&('Fire '+p.fire_rating.split(',')[0])].filter(Boolean)
-    .map(s=>`<span>${esc(s)}</span>`).join('');
+  const spec=[p.width&&drill('width',p.width),p.backing&&drill('backing',p.backing),p.fire_rating&&('Fire '+esc(p.fire_rating.split(',')[0]))].filter(Boolean)
+    .map(s=>`<span>${s}</span>`).join('');
   return `<div class="card" data-sku="${esc(p.dw_sku)}" data-mfr="${esc(p.mfr_sku||'')}" data-title="${esc(p.color||p.pattern||p.dw_sku||'')}">
     ${img}${stock}${price}</div>
     <div class="b">
@@ -453,19 +459,23 @@ function renderSide(){
   }).join('');
   $('#clearf').style.display=(Object.values(FIL).some(Boolean)||$('#q').value||$('#pattern').value)?'block':'none';
 }
-function pickF(k,v){FIL[k]=(FIL[k]===v?'':v);applyFilters();}
+function pickF(k,v){FIL[k]=(FIL[k]===v?'':v);writeURL(true);applyFilters();}
 window.pickF=pickF;
-$('#clearf').onclick=()=>{FIELDS.forEach(([k])=>FIL[k]='');$('#q').value='';$('#pattern').value='';applyFilters();};
+$('#clearf').onclick=()=>{FIELDS.forEach(([k])=>FIL[k]='');$('#q').value='';$('#pattern').value='';writeURL(true);applyFilters();};
 $('#sort').onchange=()=>{localStorage.setItem('gl.sort',$('#sort').value);applyFilters();};
-let t; $('#q').oninput=()=>{clearTimeout(t);t=setTimeout(applyFilters,250);};
-let tp; $('#pattern').oninput=()=>{clearTimeout(tp);tp=setTimeout(applyFilters,250);};
+let t; $('#q').oninput=()=>{clearTimeout(t);t=setTimeout(()=>{writeURL(false);applyFilters();},250);};
+let tp; $('#pattern').oninput=()=>{clearTimeout(tp);tp=setTimeout(()=>{writeURL(false);applyFilters();},250);};
+// a card data point (city / width / backing) is an href-drill atom → filter in place + reflect into the URL
+document.addEventListener('click',e=>{ const d=e.target.closest('a.drill[data-filter]'); if(!d)return; if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1)return; e.preventDefault(); e.stopPropagation();
+  const k=d.dataset.filter,v=d.dataset.v; FIL[k]=(FIL[k]===v?'':v); writeURL(true); applyFilters(); });
+addEventListener('popstate',()=>{ readURL(); applyFilters(); });   // back/forward restores filter state
 $('#more').onclick=renderMore;
 addEventListener('scroll',()=>{if(shown<VIEW.length&&innerHeight+scrollY>=document.body.offsetHeight-600)renderMore();});
 
 async function loadAll(){
   const raw=await fetch(location.origin+'/api/products').then(r=>r.json());
   META=raw; ALL=raw.products||[];
-  buildFacetCache(); applyFilters();
+  buildFacetCache(); readURL(); applyFilters();   // land in the filter state carried by the URL (deep-link / shareable)
   // deep-link: open drawer if hash matches a SKU
   const h=decodeURIComponent(location.hash.replace('#','')); if(h&&ALL.some(p=>p.dw_sku===h))openDrawer(h);
   wireActivationTabs();

← d3f4af8 gitignore locally-served product images after history purge  ·  back to Greenland Onboard  ·  auto-data-snapshot: 2026-08-10T16:09:17 (4 data files) — bac 3bb9eb3 →