← back to Commercialrealestate
CRCP contractors.html: add Grid/List/Table to browse mode + sortable table headers (match mode unchanged)
0e9fa7bbd5725c1d178bf413b4a843234ea8b50d · 2026-08-18 21:09:47 -0700 · Steve Abrams
Files touched
M public/contractors.html
Diff
commit 0e9fa7bbd5725c1d178bf413b4a843234ea8b50d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 18 21:09:47 2026 -0700
CRCP contractors.html: add Grid/List/Table to browse mode + sortable table headers (match mode unchanged)
---
public/contractors.html | 40 +++++++++++++++++++++++++++++++++++++---
1 file changed, 37 insertions(+), 3 deletions(-)
diff --git a/public/contractors.html b/public/contractors.html
index 6173dfb..faa8636 100644
--- a/public/contractors.html
+++ b/public/contractors.html
@@ -73,6 +73,21 @@ input.dealf{background:var(--card);border:1px solid var(--line);color:var(--ink)
.finance{background:rgba(88,166,255,.08);border:1px solid rgba(88,166,255,.25);border-radius:8px;padding:2px 7px;font-size:10.5px;color:var(--blue);cursor:pointer;text-decoration:none}
.finance:hover{border-color:var(--blue)}
@media(max-width:760px){.rail{display:none}}
+.viewseg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
+.viewseg button{background:var(--card);color:var(--mut);border:0;border-right:1px solid var(--line);padding:7px 11px;font-size:12px;cursor:pointer}
+.viewseg button:last-child{border-right:0}.viewseg button.on{background:var(--blue,#58a6ff);color:#0e1116;font-weight:600}
+.cttable{overflow:auto}
+.cttable table{width:100%;border-collapse:collapse;font-size:12.5px}
+.cttable th{text-align:left;color:var(--mut);text-transform:uppercase;font-size:10px;letter-spacing:.4px;padding:7px 9px;border-bottom:1px solid var(--line);white-space:nowrap;position:sticky;top:0;background:var(--card)}
+.cttable td{padding:7px 9px;border-bottom:1px solid var(--line)}
+.cttable tr:hover td{background:rgba(120,160,255,.06)}
+.lst{display:flex;flex-direction:column}
+.lrow{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;padding:10px 6px;border-bottom:1px solid var(--line);cursor:pointer}
+.lrow:hover{background:rgba(120,160,255,.06)}
+.lnm{font-weight:700;min-width:210px;flex:0 0 auto;color:var(--ink)}
+.lmeta{display:flex;flex-wrap:wrap;gap:5px 16px;flex:1 1 320px;min-width:0}
+.lm{font-size:12.5px;white-space:nowrap;color:var(--ink)}
+.lk{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-right:5px}
</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
<link rel="stylesheet" href="/crcp-theme.css"><!-- CRCP day/night tokens: load LAST so html[data-theme] wins -->
</head><body>
@@ -124,6 +139,7 @@ input.dealf{background:var(--card);border:1px solid var(--line);color:var(--ink)
<!-- Browse-mode toolbar (search + sort + density) -->
<div class="toolbar" id="browseTools">
+ <div class="viewseg" id="viewseg"><button data-v="grid" class="on">▦ Grid</button><button data-v="list">☰ List</button><button data-v="table">▤ Table</button></div>
<input class="search" id="q" placeholder="search business name, city, license #…">
<select id="sort"></select>
<label style="font-size:11px;color:var(--mut)">density <input type="range" id="dens" min="2" max="7" step="1"></label>
@@ -138,7 +154,10 @@ const $=s=>document.querySelector(s);
const esc=s=>(s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
const api=(u)=>fetch(u,{headers:{'Content-Type':'application/json'}}).then(r=>r.json());
let MODE='browse';
-let DATA=[], q='', sortKey='business_name', sortDir=1;
+let DATA=[], q='', sortKey='business_name', sortDir=1, view=(localStorage.getItem('conView')||'grid');
+// columns for the Table + List views (browse mode); the Grid keeps its rich cards
+const CCOLS=[{k:'business_name',l:'Contractor'},{k:'primary_class',l:'Class'},{k:'license_status',l:'Status'},{k:'city',l:'City'},{k:'county',l:'County'},{k:'license_no',l:'License #'},{k:'phone',l:'Phone'}];
+const cfmt=(c,col)=>{let v=c[col.k];if(col.k==='primary_class')v=c.primary_class||(Array.isArray(c.classifications)?c.classifications.join(', '):'');return v==null||v===''?'—':esc(String(v));};
let dens=(function(){const v=parseInt(localStorage.getItem('cxDens'),10);return(v>=2&&v<=7)?v:4;})();
const SORTS=[['business_name','Name A→Z'],['city','City A→Z'],['county','County A→Z'],['primary_class','Class'],['license_status','Status'],['license_no','License #'],['issue_date','Issued (oldest)'],['expire_date','Expires (soonest)']];
const DATE_KEYS=new Set(['issue_date','expire_date']);
@@ -263,8 +282,15 @@ function renderBrowse(down,reason){
if(down){$('#body').innerHTML=emptyState(reason);$('#count').textContent='upstream not reachable';return;}
const rows=filteredSorted();
$('#count').textContent=`${rows.length.toLocaleString()} of ${DATA.length.toLocaleString()} shown`;
- $('#body').innerHTML=rows.length?`<div class="grid">${rows.slice(0,600).map(card).join('')}</div>`
- :emptyState('No licensed contractors match these filters.','Loosen the county / city / class filters, or clear the search.');
+ if(!rows.length){$('#body').innerHTML=emptyState('No licensed contractors match these filters.','Loosen the county / city / class filters, or clear the search.');return;}
+ const slice=rows.slice(0,600);
+ if(view==='list'){
+ $('#body').innerHTML=`<div class="lst">`+slice.map(c=>`<div class="lrow crow" data-lic="${esc(String(c.license_no||'').replace(/\D/g,''))}" style="cursor:pointer"><div class="lnm">${esc(c.business_name||'—')}</div><div class="lmeta">`+CCOLS.slice(1).map(col=>`<span class="lm"><span class="lk">${esc(col.l)}</span>${cfmt(c,col)}</span>`).join('')+`</div></div>`).join('')+`</div>`;
+ } else if(view==='table'){
+ $('#body').innerHTML=`<div class="cttable"><table><thead><tr>`+CCOLS.map(col=>`<th data-k="${col.k}" style="cursor:pointer" title="Click to sort">${esc(col.l)}${sortKey===col.k?(sortDir<0?' ↓':' ↑'):''}</th>`).join('')+`</tr></thead><tbody>`+slice.map(c=>`<tr class="crow" data-lic="${esc(String(c.license_no||'').replace(/\D/g,''))}" style="cursor:pointer">`+CCOLS.map(col=>`<td>${cfmt(c,col)}</td>`).join('')+`</tr>`).join('')+`</tbody></table></div>`;
+ } else {
+ $('#body').innerHTML=`<div class="grid">${slice.map(card).join('')}</div>`;
+ }
}
// Pull the registry through our own proxy (server injects CONTRACTORS_API_BASE). Server-side county/city
// let the upstream do the heavy filter; we sort/search/paginate client-side.
@@ -324,6 +350,14 @@ $('#dGo').onclick=loadMatch;
// ── browse toolbar wiring ──
$('#q').addEventListener('input',()=>{q=$('#q').value.trim();renderBrowse(false);});
$('#sort').addEventListener('change',()=>{sortKey=$('#sort').value;sortDir=1;renderBrowse(false);});
+// grid/list/table view toggle (browse mode)
+document.querySelectorAll('#viewseg button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));
+$('#viewseg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;document.querySelectorAll('#viewseg button').forEach(x=>x.classList.toggle('on',x===b));view=b.dataset.v;localStorage.setItem('conView',view);renderBrowse(false);});
+// table header → sort any column; a table/list row → open the CSLB license record
+document.addEventListener('click',e=>{
+ const th=e.target.closest('.cttable th'); if(th){const k=th.dataset.k;if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='license_no')?-1:1;}const so=$('#sort');if(so&&Array.from(so.options).some(o=>o.value===k))so.value=k;renderBrowse(false);return;}
+ const row=e.target.closest('.crow'); if(row && !e.target.closest('a,button')){const lic=row.dataset.lic;if(lic)window.open('https://www.cslb.ca.gov/OnlineServices/CheckLicenseII/LicenseDetail.aspx?LicNum='+encodeURIComponent(lic),'_blank','noopener');}
+});
$('#dens').addEventListener('input',()=>{dens=+$('#dens').value;localStorage.setItem('cxDens',dens);renderBrowse(false);});
// rail chips: multi-select. County/city/class with exactly one selection re-query the upstream
← 9e31cc7 CRCP deals-flow.html: add Grid/List/Table views + sortable t
·
back to Commercialrealestate
·
auto-data-snapshot: 2026-08-18T21:26:29 (2 data files) — dat c95e260 →