[object Object]

← back to Commercialrealestate

CRCP gov-agents.html: add grid/list/table view system (was table-only) + all-sortable + mobile ⚙ Filters modal

4eac4b6928d9bb4884d9fe15f89f5c1efdeb0c9a · 2026-08-18 20:48:04 -0700 · Steve Abrams

Files touched

Diff

commit 4eac4b6928d9bb4884d9fe15f89f5c1efdeb0c9a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 18 20:48:04 2026 -0700

    CRCP gov-agents.html: add grid/list/table view system (was table-only) + all-sortable + mobile ⚙ Filters modal
---
 public/gov-agents.html | 78 +++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 71 insertions(+), 7 deletions(-)

diff --git a/public/gov-agents.html b/public/gov-agents.html
index 44779b0..e2098ee 100644
--- a/public/gov-agents.html
+++ b/public/gov-agents.html
@@ -67,6 +67,28 @@
   #modal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
   #modal .litem{display:flex;justify-content:space-between;gap:12px;padding:5px 0;border-bottom:1px solid #20262f;font-size:13px;} #modal .litem .m{color:var(--mut);}
 </style>
+  <style>
+  /* grid container + compact List view (added grid/list/table) */
+  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card-min,280px),1fr));gap:16px;padding:16px 20px 0;}
+  .lst{padding:14px 20px 0;display:flex;flex-direction:column;}
+  .lrow{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;padding:10px 4px;border-bottom:1px solid var(--line2,#20262f);cursor:pointer;}
+  .lrow:hover{background:rgba(200,162,75,.06);}
+  .lnm{font-weight:700;min-width:190px;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;}
+  .lk{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-right:5px;}
+  /* mobile: ⚙ Filters button + rail modal (rail is display:none <820px) */
+  .railbtn{display:none;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:8px 12px;font-size:13px;cursor:pointer;}
+  .railbtn:hover{border-color:var(--gold);}
+  .railov{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:70;display:none;align-items:flex-start;justify-content:center;padding:14px;}
+  .railov.on{display:flex;}
+  .railmd{width:100%;max-width:520px;max-height:90vh;display:flex;flex-direction:column;background:var(--card);border:1px solid var(--line);border-radius:15px;overflow:hidden;}
+  .railmd-h{display:flex;align-items:center;justify-content:space-between;padding:13px 16px;border-bottom:1px solid var(--line);font-size:15px;color:var(--ink);}
+  .railmd-x{background:none;border:0;color:var(--mut);font-size:20px;cursor:pointer;line-height:1;}
+  .railmd-b{overflow:auto;padding:0;}
+  .rail.in-modal{display:block!important;position:static!important;height:auto!important;width:auto!important;flex:none!important;top:auto!important;border:0!important;}
+  @media(max-width:820px){ .railbtn{display:inline-block;} }
+  </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>
@@ -76,6 +98,8 @@
   <div class="nav"><a href="/broker-grid.html">← Brokers</a><a href="/mls.html">📋 Listings</a><a href="/crcp.html">🛰️ CRCP</a></div>
   <span class="spacer"></span>
   <input class="search" id="q" type="text" placeholder="search all fields — name, firm, license #, city, county, zip, expiry… (space = AND)">
+  <button class="railbtn" id="railBtn" type="button">⚙ Filters</button>
+  <div class="toggle" id="view"><button data-v="grid">▦ Grid</button><button data-v="list">☰ List</button><button data-v="table" class="on">▤ Table</button></div>
   <select id="sortsel" title="Sort"></select>
   <label class="densl" title="Rows per fetch">⤢<input type="range" id="density" min="200" max="2000" step="100" value="500"></label>
   <span class="count" id="count">…</span>
@@ -91,7 +115,9 @@
   </div>
 </aside>
 <div class="rwrap">
-  <div class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+  <div id="tableView" class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+  <div id="gridView" class="grid hide"></div>
+  <div id="listView" class="lst hide"></div>
 </div>
 </div>
 <div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
@@ -112,7 +138,7 @@ const COLS=[
   {k:'zip',l:'Zip',g:'Location'},
   {k:'state',l:'State',g:'Location'},
 ];
-let METRO='', AGENTS=[], META={}, q='', sortKey='name', sortDir=1, limit=500, timer=null, lastColSig='';
+let METRO='', AGENTS=[], META={}, q='', sortKey='name', sortDir=1, limit=500, timer=null, lastColSig='', view=(localStorage.getItem('govView')||'table');
 // START TIGHT (Steve rule): only the essentials show on first load; the user toggles
 // the rest on from the rail. A saved pref in VISCOL always wins.
 const DEFAULT_ON=new Set(['name','firm','license_type','license_number','expiration','status']);
@@ -196,14 +222,24 @@ function render(){
   const cols=visCols();
   const rows=filtered();
   $('#thead').innerHTML='<tr>'+cols.map(c=>`<th data-k="${c.k}" draggable="true" title="Drag to reorder · click to sort" class="dragcol${sortKey===c.k?(sortDir>0?' asc':' desc'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
-  $('#tbody').innerHTML=rows.map((a,i)=>`<tr data-i="${i}">`+cols.map(c=>`<td>${cell(a,c)}</td>`).join('')+'</tr>').join('')||`<tr><td colspan="${cols.length||1}" class="miss" style="padding:20px">no matches</td></tr>`;
+  const tv=$('#tableView'),gv=$('#gridView'),lv=$('#listView');
+  if(tv) tv.classList.toggle('hide',view!=='table');
+  if(gv) gv.classList.toggle('hide',view!=='grid');
+  if(lv) lv.classList.toggle('hide',view!=='list');
+  if(view==='table'){
+    $('#tbody').innerHTML=rows.map((a,i)=>`<tr data-i="${i}">`+cols.map(c=>`<td>${cell(a,c)}</td>`).join('')+'</tr>').join('')||`<tr><td colspan="${cols.length||1}" class="miss" style="padding:20px">no matches</td></tr>`;
+    const csig=cols.map(c=>c.k).join(',');
+    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+    else if(window.ColResize && window.ColResize.reattach){ window.ColResize.reattach(document.querySelector('.tblwrap table.g')); } // resize handles survive sort/search re-render (TK-10089)
+  } else if(view==='list' && lv){
+    lv.innerHTML=rows.map((a,i)=>`<div class="lrow" data-i="${i}" style="cursor:pointer"><div class="lnm">${esc(a.name||'—')}</div><div class="lmeta">`+cols.filter(c=>c.k!=='name').map(c=>`<span class="lm"><span class="lk">${esc(c.l)}</span>${cell(a,c)}</span>`).join('')+`</div></div>`).join('')||'<div class="miss" style="padding:20px">no matches</div>';
+  } else if(gv){
+    gv.innerHTML=rows.map((a,i)=>`<div class="gc" data-i="${i}" style="cursor:pointer"><div class="nm">${esc(a.name||'—')}</div>${a.firm?`<div class="fm">${esc(a.firm)}</div>`:''}`+cols.filter(c=>c.k!=='name').map(c=>`<div class="row"><span class="k">${esc(c.l)}</span><span>${cell(a,c)}</span></div>`).join('')+`</div>`).join('')||'<div class="miss" style="padding:20px">no matches</div>';
+  }
   ROWS=rows;
   const capped=META.total>AGENTS.length;
   $('#count').textContent=`${rows.length}${(q||capped)?' of '+(capped?META.total.toLocaleString():AGENTS.length):''} agents`;
   $('#note').innerHTML=`Source: <b>${esc(META.source||'?')}</b> · ${esc(META.label||'')}`+(capped?` <span class="cap">— server returned ${AGENTS.length.toLocaleString()} of ${META.total.toLocaleString()} matches; refine your search to narrow.</span>`:'');
-  const csig=cols.map(c=>c.k).join(',');
-  if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
-  else if(window.ColResize && window.ColResize.reattach){ window.ColResize.reattach(document.querySelector('.tblwrap table.g')); } // resize handles survive sort/search re-render (TK-10089)
 }
 let ROWS=[];
 // ── expandable row: click a row for full detail (all fields) ──
@@ -218,7 +254,7 @@ function openDetail(a){
   mb.innerHTML=`<h3>${esc(a.name||'—')}</h3><div class="mfirm">${esc(a.firm||'—')}${a.license_type?' · '+esc(a.license_type):''}${a.license_number?' · '+esc(a.license_number):''}</div><div style="margin-top:12px">${rows}${extra}</div>`;
 }
 document.addEventListener('click',e=>{ if(e.target.id==='ov'){ $('#ov').classList.remove('on'); return; }
-  const tr=e.target.closest('#tbody tr'); if(tr && tr.dataset.i!=null && e.target.tagName!=='A'){ const a=ROWS[+tr.dataset.i]; if(a) openDetail(a); } });
+  const el=e.target.closest('#tbody tr,#gridView .gc,#listView .lrow'); if(el && el.dataset.i!=null && e.target.tagName!=='A'){ const a=ROWS[+el.dataset.i]; if(a) openDetail(a); } });
 document.addEventListener('keydown',e=>{ if(e.key==='Escape') $('#ov').classList.remove('on'); });
 async function loadMetro(){
   $('#count').textContent='loading…';
@@ -244,6 +280,9 @@ $('#metros').addEventListener('click',e=>{const c=e.target.closest('.mchip');if(
 // Search: keep the server-side query (narrows the fetch) AND filter client-side across ALL fields.
 $('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();clearTimeout(timer);timer=setTimeout(loadMetro,280);});
 $('#sortsel').addEventListener('change',()=>{sortKey=$('#sortsel').value;render();});
+// grid/list/table view toggle
+document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));
+$('#view')&&$('#view').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x===b));view=b.dataset.v;localStorage.setItem('govView',view);render();});
 $('#thead').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=1;}$('#sortsel').value=sortKey;render();});
 $('#density').addEventListener('change',()=>{limit=+$('#density').value;loadMetro();});
 // ── drag-to-reorder columns (Steve rule: move columns around) ──
@@ -310,5 +349,30 @@ loadIndex();
 <script src="/corner-nav.js" defer></script>
   <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
   <script src="/crcp-theme.js" defer></script><!-- CRCP day/night toggle (auto-injects into header) -->
+  <script>
+  /* mobile: relocate the filter rail into a ⚙ Filters modal (<820px), back to the sidebar on desktop */
+  (function(){
+    function boot(){
+      var rail=document.querySelector('.rail'), hdr=document.querySelector('header'), btn=document.getElementById('railBtn');
+      if(!rail||!hdr||!btn||document.getElementById('railOv')) return;
+      var home=document.createComment('gov-rail-home'); rail.parentNode.insertBefore(home,rail);
+      var ov=document.createElement('div'); ov.id='railOv'; ov.className='railov';
+      var md=document.createElement('div'); md.className='railmd';
+      var hd=document.createElement('div'); hd.className='railmd-h'; hd.innerHTML='<span>Filters</span><button type="button" class="railmd-x" aria-label="Close">✕</button>';
+      var bd=document.createElement('div'); bd.className='railmd-b';
+      md.appendChild(hd); md.appendChild(bd); ov.appendChild(md); document.body.appendChild(ov);
+      var open=function(){ov.classList.add('on');}, close=function(){ov.classList.remove('on');};
+      btn.addEventListener('click',open);
+      hd.querySelector('.railmd-x').addEventListener('click',close);
+      ov.addEventListener('click',function(e){ if(e.target===ov) close(); });
+      document.addEventListener('keydown',function(e){ if(e.key==='Escape') close(); });
+      var mq=window.matchMedia('(max-width: 820px)');
+      function place(){ if(mq.matches){ if(rail.parentNode!==bd) bd.appendChild(rail); rail.classList.add('in-modal'); } else { if(home.parentNode&&rail.parentNode!==home.parentNode) home.parentNode.insertBefore(rail,home); rail.classList.remove('in-modal'); close(); } }
+      if(mq.addEventListener) mq.addEventListener('change',place); else if(mq.addListener) mq.addListener(place);
+      place();
+    }
+    if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',boot); else boot();
+  })();
+  </script>
 </body>
 </html>

← f634570 CRCP sales.html: add compact List view (grid/list/table) + m  ·  back to Commercialrealestate  ·  CRCP: broker-direct SUBSTITUTES the aggregator row (sub out 6074d06 →