[object Object]

← back to Commercialrealestate

CRCP sales.html: add compact List view (grid/list/table) + mobile ⚙ Filters modal

f634570dfb7218c5319fa783c1d77abd595d3d40 · 2026-08-18 20:44:52 -0700 · Steve Abrams

Files touched

Diff

commit f634570dfb7218c5319fa783c1d77abd595d3d40
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 18 20:44:52 2026 -0700

    CRCP sales.html: add compact List view (grid/list/table) + mobile ⚙ Filters modal
---
 public/sales.html | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 57 insertions(+), 4 deletions(-)

diff --git a/public/sales.html b/public/sales.html
index 575cbbd..c5257d5 100644
--- a/public/sales.html
+++ b/public/sales.html
@@ -89,6 +89,27 @@
   .rail .rsec.collapsed > h4::after { transform:rotate(-90deg); }
   .rail .rsec.collapsed > *:not(h4) { display:none !important; }
 </style>
+  <style>
+  /* compact List view (3rd view) */
+  .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>
@@ -98,7 +119,8 @@
   <div class="nav"><a href="/crcp.html">← CRCP</a><a href="/condos.html">Condos</a><a href="/graphics.html">Graphics</a><a href="/fha-loans.html">FHA Loans</a></div>
   <span class="spacer"></span>
   <input class="search" id="q" type="text" placeholder="search all fields — address, city, zip, price, beds, sqft… (space = AND)">
-  <div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
+  <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 sold comps"></select>
   <label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
   <button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
@@ -120,6 +142,7 @@
 <div class="wrap">
   <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 class="charts">
     <div class="panel"><h3>By city (avg price)</h3><canvas id="cityChart"></canvas></div>
     <div class="panel"><h3>Sales by year</h3><canvas id="yearChart"></canvas></div>
@@ -262,14 +285,18 @@ function render(){
   const rows=filtered(); const cols=visCols();
   $('#count').textContent=`${rows.length} of ${DATA.length} sold comps`;
   $('#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>';
+  const lv=$('#listView');
+  $('#tableView').classList.toggle('hide',view!=='table');
+  $('#gridView').classList.toggle('hide',view!=='grid');
+  if(lv) lv.classList.toggle('hide',view!=='list');
   if(view==='table'){
-    $('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
     $('#tbody').innerHTML=rows.map((r,i)=>`<tr class="srow" data-i="${i}" style="cursor:pointer">`+cols.map(c=>{const cls=(c.t==='n'||c.t==='money')?' class="n"':'';return `<td${cls}>${cell(r,c)}</td>`;}).join('')+'</tr>').join('');
     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('#tableView table.g')); } // resize handles survive sort/search re-render (TK-10089)
+  } else if(view==='list' && lv){
+    lv.innerHTML=rows.map((r,i)=>`<div class="lrow srow" data-i="${i}" style="cursor:pointer"><div class="lnm">${esc(r.address||'—')}</div><div class="lmeta">`+cols.filter(c=>c.k!=='address').map(c=>`<span class="lm"><span class="lk">${esc(c.l)}</span>${cell(r,c)}</span>`).join('')+`</div></div>`).join('');
   } else {
-    $('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
     const skipGrid=new Set(['address','city']);
     const bodyCols=cols.filter(c=>!skipGrid.has(c.k));
     $('#gridView').innerHTML=rows.map((r,i)=>`<div class="gc srow" data-i="${i}">
@@ -354,7 +381,7 @@ $('#view').addEventListener('click',e=>{const b=e.target.closest('button');if(!b
   const dv=parseInt(localStorage.getItem('salesDensity'),10),dval=(dv>=1&&dv<=10)?dv:5;
   dz.value=dval;applyDensity(dval);
   dz.addEventListener('input',()=>{const v=+dz.value;applyDensity(v);localStorage.setItem('salesDensity',v);});
-  const pv=localStorage.getItem('salesView'); if(pv==='grid'||pv==='table'){view=pv;document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));}
+  const pv=localStorage.getItem('salesView'); if(pv==='grid'||pv==='table'||pv==='list'){view=pv;document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));}
   $('#denswrap').style.display=(view==='grid')?'inline-flex':'none';
   window.__syncSortSel=()=>{if(Array.from(sel.options).some(o=>o.value===sortKey)&&sel.value!==sortKey)sel.value=sortKey;};
 })();
@@ -421,5 +448,31 @@ fetch('/api/closed-sales?limit=5000').then(r=>r.json()).then(d=>{
 <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.
+     The rail keeps its own .rsec collapse handler; this only moves the node + toggles the overlay. */
+  (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('sales-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>

← 324dab8 CRCP: browser-render template (scrape-stepp.js) via global P  ·  back to Commercialrealestate  ·  CRCP gov-agents.html: add grid/list/table view system (was t 4eac4b6 →