← back to Nationalrealestate
usre: contractors.html gets grid/list/table views + all-column server-side sort (clickable table headers + full Sort select, view+sort persisted)
cf1e3b288c604638deff4f21280797cdbfe39657 · 2026-08-13 08:04:15 -0700 · steve
Files touched
M public/contractors.html
Diff
commit cf1e3b288c604638deff4f21280797cdbfe39657
Author: steve <steve@designerwallcoverings.com>
Date: Thu Aug 13 08:04:15 2026 -0700
usre: contractors.html gets grid/list/table views + all-column server-side sort (clickable table headers + full Sort select, view+sort persisted)
---
public/contractors.html | 57 ++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 52 insertions(+), 5 deletions(-)
diff --git a/public/contractors.html b/public/contractors.html
index af8c2d8..5b77c30 100644
--- a/public/contractors.html
+++ b/public/contractors.html
@@ -284,12 +284,56 @@ function card(r){
return `<div class="cd">${parts.join('')}</div>`;
}
+// ── table view: dense all-columns spreadsheet, click-to-sort headers (server sort) ──
+const TBL_COLS = [
+ ['business_name','Business'],['license_no','License #'],['license_status','Status'],
+ ['city','City'],['county','County'],['zip','ZIP'],['primary_class','Class'],
+ ['issue_date','Issued'],['expire_date','Expires'],
+];
+function renderTable(){
+ const [sk,sd]=SORT.split(':');
+ const arrow=k=> k===sk ? (sd==='desc'?' ▾':' ▴') : '';
+ let h='<table class="ctrtbl"><thead><tr>'+
+ TBL_COLS.map(([k,l])=>`<th data-k="${k}" class="${k===sk?'sorted':''}">${esc(l)}${arrow(k)}</th>`).join('')+
+ '<th>Phone</th></tr></thead><tbody>';
+ for(const r of PAGE){
+ const cls=(r.classifications||[]).join(', ');
+ h+='<tr>'+
+ `<td><a href="/contractors.html?q=${encodeURIComponent(r.business_name)}">${esc(r.business_name)}</a></td>`+
+ `<td>${esc(r.license_no)}</td>`+
+ `<td>${esc(r.license_status||'—')}</td>`+
+ `<td>${r.city?`<a href="/contractors.html?city=${encodeURIComponent(r.city)}">${esc(r.city)}</a>`:'—'}</td>`+
+ `<td>${r.county?`<a href="/contractors.html?county=${encodeURIComponent(r.county)}">${esc(r.county)}</a>`:'—'}</td>`+
+ `<td>${esc(r.zip||'—')}</td>`+
+ `<td>${r.primary_class?`<a href="/contractors.html?class=${encodeURIComponent(r.primary_class)}">${esc(r.primary_class)}</a>`:esc(cls||'—')}</td>`+
+ `<td>${esc(r.issue_date||'—')}</td>`+
+ `<td>${esc(r.expire_date||'—')}</td>`+
+ `<td>${esc(r.phone||'—')}</td>`+
+ '</tr>';
+ }
+ h+='</tbody></table>';
+ const grid=$('#grid'); grid.className='table'; grid.innerHTML='';
+ // remove any prior table then insert
+ const prev=$('.ctrtbl'); if(prev) prev.remove();
+ grid.insertAdjacentHTML('afterend',h);
+ $('.ctrtbl thead').querySelectorAll('th[data-k]').forEach(th=>th.onclick=()=>{
+ const k=th.dataset.k; const [ck,cd]=SORT.split(':');
+ SORT = (k===ck) ? `${k}:${cd==='asc'?'desc':'asc'}` : `${k}:asc`;
+ localStorage.setItem('ctr:sort',SORT); $('#sort').value=SORT; loadFeed();
+ });
+}
+
function renderGrid(){
const grid=$('#grid');
- if(!PAGE.length){ grid.innerHTML='<div class="empty">No contractors match. (The CSLB registry may still be loading — this page is empty-safe.)</div>'; return; }
- const rows=[...PAGE].sort(SORTERS[SORT]||SORTERS.name);
- grid.innerHTML=rows.map(card).join('');
- // refresh in-page facets from what's loaded
+ // clear any stray table left by a previous table-view render
+ const prev=$('.ctrtbl'); if(prev) prev.remove();
+ if(!PAGE.length){ grid.className=''; grid.innerHTML='<div class="empty">No contractors match. (The CSLB registry may still be loading — this page is empty-safe.)</div>'; refreshFacets(); return; }
+ if(VIEW==='table'){ renderTable(); refreshFacets(); return; }
+ grid.className = (VIEW==='list'?'list':'');
+ grid.innerHTML=PAGE.map(card).join('');
+ refreshFacets();
+}
+function refreshFacets(){
facetList('#fCounty','county',agg(PAGE,'county'));
facetList('#fCity','city',agg(PAGE,'city').slice(0,25));
facetList('#fClass','class',aggClasses(PAGE));
@@ -298,6 +342,7 @@ function renderGrid(){
async function loadFeed(){
const p=new URLSearchParams();
for(const f of FILTERS){ if(state[f]) p.set(f,state[f]); }
+ const sp=sortParts(); p.set('sort',sp.sort); p.set('order',sp.order);
p.set('limit',limit); p.set('offset',state.offset||0);
$('#count').textContent='loading…';
let d; try{ d=await (await fetch('/api/contractors?'+p.toString())).json(); }
@@ -334,7 +379,9 @@ let qTimer;
$('#q').oninput=e=>{ clearTimeout(qTimer); const v=e.target.value.trim(); qTimer=setTimeout(()=>go({q:v||undefined}),300); };
$('#class').onchange=e=>go({class:e.target.value||undefined});
$('#status').onchange=e=>go({status:e.target.value});
-$('#sort').onchange=e=>{ SORT=e.target.value; localStorage.setItem('ctr:sort',SORT); renderGrid(); };
+$('#sort').onchange=e=>{ SORT=e.target.value; localStorage.setItem('ctr:sort',SORT); loadFeed(); };
+$('#viewseg').querySelectorAll('button').forEach(b=>b.onclick=()=>{ VIEW=b.dataset.view; localStorage.setItem('ctr:view',VIEW);
+ $('#viewseg').querySelectorAll('button').forEach(x=>x.classList.toggle('active',x.dataset.view===VIEW)); renderGrid(); });
$('#density').oninput=e=>{ document.documentElement.style.setProperty('--card',e.target.value+'px'); localStorage.setItem('ctr:card',e.target.value); };
$('#prev').onclick=()=>go({offset:Math.max(0,(state.offset||0)-limit)});
$('#next').onclick=()=>go({offset:(state.offset||0)+limit});
← 846689f nav-agent: add universal grid-controls drop-in (field-toggle
·
back to Nationalrealestate
·
usre: deals.html gets grid/list/table view toggle + every ta 1227b5c →