[object Object]

← back to Commercialrealestate

auto-save: 2026-07-02T09:46:55 (1 files) — public/mls.html

d74ce0218f6ccdd9b10fbb4a42bcc897391cb5da · 2026-07-02 09:47:09 -0700 · Steve Abrams

Files touched

Diff

commit d74ce0218f6ccdd9b10fbb4a42bcc897391cb5da
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 09:47:09 2026 -0700

    auto-save: 2026-07-02T09:46:55 (1 files) — public/mls.html
---
 public/mls.html | 98 ++++++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 83 insertions(+), 15 deletions(-)

diff --git a/public/mls.html b/public/mls.html
index 77fe81a..d5301fd 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -47,6 +47,23 @@
   .ph-ci{display:flex;gap:14px;flex-wrap:wrap;margin:8px 0;font-size:13.5px;}
   .ph-sec{margin-top:14px;} .ph-sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);}
   .ph-item{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid #20262f;font-size:13px;} .ph-mut{color:var(--mut);font-size:12.5px;}
+  /* ---- Amazon-style left rail ---- */
+  .shell{display:flex;align-items:flex-start;}
+  .rail{flex:0 0 260px;background:#0b0e13;border-right:1px solid var(--line);align-self:stretch;position:sticky;top:53px;height:calc(100vh - 53px);overflow-y:auto;padding:0 0 40px;}
+  .rail .rsec{padding:13px 16px;border-bottom:1px solid var(--line);}
+  .rail h4{margin:0 0 9px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut);}
+  .chips{display:flex;flex-wrap:wrap;gap:6px;}
+  .chip{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;}
+  .chip:hover{color:var(--ink);} .chip.active{border-color:var(--blue);color:var(--blue);background:rgba(88,166,255,.08);}
+  .chip .ct{color:var(--mut);font-size:10px;margin-left:4px;}
+  .navchip{display:inline-flex;align-items:center;gap:5px;background:rgba(210,153,34,.10);border:1px solid rgba(210,153,34,.4);color:var(--warn);border-radius:16px;padding:5px 11px;font-size:12px;cursor:pointer;text-decoration:none;font-weight:600;}
+  .row2{display:flex;gap:8px;} .fld{flex:1;} .fld label{display:block;font-size:10px;color:var(--mut);margin-bottom:3px;}
+  .rail input[type=number]{width:100%;background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 9px;font-size:13px;}
+  .fieldtoggles{display:flex;flex-direction:column;gap:1px;max-height:320px;overflow-y:auto;}
+  .ftog{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink);cursor:pointer;padding:3px 4px;border-radius:6px;} .ftog:hover{background:rgba(88,166,255,.06);} .ftog input{accent-color:var(--blue);}
+  .minibtn{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;margin-right:5px;} .minibtn:hover{border-color:var(--blue);color:var(--ink);}
+  .wrap{flex:1 1 auto;min-width:0;}
+  @media(max-width:820px){ .rail{display:none;} }
 </style>
 </head>
 <body>
@@ -59,10 +76,23 @@
   <button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
   <span class="count" id="count">…</span>
 </header>
+<div class="shell">
+<aside class="rail">
+  <div class="rsec"><h4>Asset class</h4><div class="chips" id="fType"></div>
+    <div style="margin-top:9px"><a class="navchip" href="/condos">🏢 Condos — FHA warrantability →</a></div></div>
+  <div class="rsec"><h4>Status</h4><div class="chips" id="fStatus"></div></div>
+  <div class="rsec"><h4>Submarket / city</h4><div class="chips" id="fCity"></div></div>
+  <div class="rsec"><h4>Price</h4><div class="row2"><div class="fld"><label>Min $</label><input type="number" id="pMin" placeholder="any" step="50000"></div><div class="fld"><label>Max $</label><input type="number" id="pMax" placeholder="any" step="50000"></div></div></div>
+  <div class="rsec"><h4>Fields / columns — show / hide</h4>
+    <div class="fieldtoggles" id="fFields"></div>
+    <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button></div>
+  </div>
+</aside>
 <div class="wrap">
   <div id="tableView" class="tblwrap"><table class="mls"><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
   <div id="gridView" class="grid hide"></div>
 </div>
+</div>
 <div id="ov"><div id="modal"><button class="ph-x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
 <script>
 const $=s=>document.querySelector(s);
@@ -91,6 +121,39 @@ const COLS=[
   {k:'source',l:'Source',t:'link'},
 ];
 let DATA=[], view='table', sortKey='rank', sortDir=1, q='';
+const F={types:new Set(),cities:new Set(),status:new Set(),pmin:null,pmax:null};
+let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('mlsCols')||'{}');}catch(e){}return s;})();
+function colVis(k){ if(k in VISCOL) return !!VISCOL[k]; const c=COLS.find(x=>x.k===k); return c?c.def!==0:true; }
+function visCols(){ return COLS.filter(c=>colVis(c.k)); }
+function saveCols(){ try{localStorage.setItem('mlsCols',JSON.stringify(VISCOL));}catch(e){} }
+function tog(set,v){ set.has(v)?set.delete(v):set.add(v); }
+// Discover EVERY scalar data point present in the feed and expose each as a toggleable column (default hidden).
+function autoCols(){
+  const known=new Set(COLS.map(c=>c.k)); const skip=new Set(['id','finance','qwen','deal','warrant_signals','listings_json','tx_history','risks']);
+  const seen=new Set();
+  DATA.slice(0,60).forEach(r=>Object.keys(r||{}).forEach(k=>{
+    if(known.has(k)||skip.has(k)||seen.has(k)) return; const v=r[k]; if(v==null) return;
+    if(typeof v==='object') return; seen.add(k);
+    const isNum = typeof v==='number' || (v!==''&&!isNaN(+v));
+    COLS.push({k, l:k.replace(/_/g,' ').replace(/\b\w/g,ch=>ch.toUpperCase()).slice(0,20), t:isNum?'n':'s', def:0});
+  }));
+}
+function buildRail(){
+  const tc={},cc={},sc={};
+  DATA.forEach(r=>{ if(r.type)tc[r.type]=(tc[r.type]||0)+1; if(r.city)cc[r.city]=(cc[r.city]||0)+1; const s=String(r.status||'').startsWith('Active')?'Active':'Off-market'; sc[s]=(sc[s]||0)+1; });
+  const chip=(obj,attr,limit)=>Object.entries(obj).sort((a,b)=>b[1]-a[1]).slice(0,limit||99).map(([k,c])=>`<span class="chip" data-${attr}="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
+  $('#fType').innerHTML=chip(tc,'ty'); $('#fStatus').innerHTML=chip(sc,'st'); $('#fCity').innerHTML=chip(cc,'cy',16);
+  buildFieldToggles();
+}
+function buildFieldToggles(){ $('#fFields').innerHTML=COLS.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join(''); }
+function passFacets(r){
+  if(F.types.size && !F.types.has(r.type)) return false;
+  if(F.cities.size && !F.cities.has(r.city)) return false;
+  if(F.status.size){ const s=String(r.status||'').startsWith('Active')?'Active':'Off-market'; if(!F.status.has(s)) return false; }
+  if(F.pmin!=null && !(+r.price>=F.pmin)) return false;
+  if(F.pmax!=null && !(+r.price<=F.pmax)) return false;
+  return true;
+}
 const cval=(r,c)=>c.calc?c.calc(r):r[c.k];
 function cellHTML(r,c){
   const v=cval(r,c);
@@ -102,7 +165,7 @@ function cellHTML(r,c){
   return esc(v==null||v===''?'—':v);
 }
 function filtered(){
-  let rows=DATA;
+  let rows=DATA.filter(passFacets);
   if(q){ const t=q.toLowerCase(); rows=rows.filter(r=>COLS.some(c=>{const v=cval(r,c);return v!=null&&String(v).toLowerCase().includes(t);})); }
   const c=COLS.find(x=>x.k===sortKey)||COLS[0];
   rows=rows.slice().sort((a,b)=>{
@@ -114,27 +177,23 @@ function filtered(){
   return rows;
 }
 function render(){
-  const rows=filtered();
+  const rows=filtered(); const cols=visCols();
   $('#count').textContent=`${rows.length} of ${DATA.length} properties`;
   // table head
-  $('#thead').innerHTML='<tr>'+COLS.map(c=>`<th data-k="${c.k}" class="${sortKey===c.k?(sortDir>0?'asc':'desc'):''}">${c.l}</th>`).join('')+'</tr>';
+  $('#thead').innerHTML='<tr>'+cols.map(c=>`<th data-k="${c.k}" class="${sortKey===c.k?(sortDir>0?'asc':'desc'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
   if(view==='table'){
     $('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
-    $('#tbody').innerHTML=rows.map(r=>`<tr class="prow" data-addr="${esc(r.address||'')}" data-city="${esc(r.city||'')}" style="cursor:pointer">`+COLS.map(c=>{
+    $('#tbody').innerHTML=rows.map(r=>`<tr class="prow" data-addr="${esc(r.address||'')}" data-city="${esc(r.city||'')}" style="cursor:pointer">`+cols.map(c=>{
       const cls=(c.t==='money'||c.t==='n'||c.t==='pct')?' class="n"':'';
       return `<td${cls}>${cellHTML(r,c)}</td>`;
     }).join('')+'</tr>').join('');
   } else {
     $('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
+    const bodyCols=cols.filter(c=>!['address','city','zip','type'].includes(c.k));
     $('#gridView').innerHTML=rows.map(r=>`<div class="gc prow" data-addr="${esc(r.address||'')}" data-city="${esc(r.city||'')}" style="cursor:pointer">
-      <div class="addr">${esc(r.address||'—')}</div><div class="sub">${esc(r.city||'')} ${esc(r.zip||'')} · ${esc(r.type||'')}</div>
-      <div class="row"><span class="k">Price</span><span class="v">${money(r.price)}</span></div>
-      <div class="row"><span class="k">Units / SqFt</span><span class="v">${num(r.units)} / ${num(r.sqft)}</span></div>
-      <div class="row"><span class="k">Cap / Year</span><span class="v">${r.cap_rate!=null?r.cap_rate+'%':'—'} / ${num(r.year_built)}</span></div>
-      <div class="row"><span class="k">Status</span><span class="v"><span class="pill ${String(r.status||'').startsWith('Active')?'active':'other'}">${esc(r.status||'—')}</span></span></div>
-      <div class="row"><span class="k">Firm</span><span class="v mut">${esc(r.firm||'—')}</span></div>
-      ${r.source?`<div class="row"><span class="k">Listing</span><span class="v"><a href="${esc(r.source)}" target="_blank" rel="noopener noreferrer" style="color:var(--blue)">open↗</a></span></div>`:''}
-    </div>`).join('');
+      <div class="addr">${esc(r.address||'—')}</div><div class="sub">${esc(r.city||'')} ${esc(r.zip||'')} · ${esc(r.type||'')}</div>`
+      +bodyCols.map(c=>`<div class="row"><span class="k">${esc(c.l)}</span><span class="v">${cellHTML(r,c)}</span></div>`).join('')
+      +`</div>`).join('');
   }
 }
 $('#thead').addEventListener('click',e=>{ const th=e.target.closest('th'); if(!th) return; const k=th.dataset.k;
@@ -159,13 +218,22 @@ async function propHistory(address,city){
 document.addEventListener('click',e=>{ const row=e.target.closest('.prow'); if(row && e.target.tagName!=='A'){ propHistory(row.dataset.addr,row.dataset.city); }
   if(e.target.id==='ov'||e.target.classList.contains('ph-x')) $('#ov').classList.remove('on'); });
 $('#q').addEventListener('input',()=>{ q=$('#q').value.trim(); render(); });
-function exportCSV(){ const rows=filtered(); const cv=(r,c)=>{let v=cval(r,c); v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"';};
-  const csv=COLS.map(c=>'"'+c.l+'"').join(',')+'\n'+rows.map(r=>COLS.map(c=>cv(r,c)).join(',')).join('\n');
+function exportCSV(){ const rows=filtered(); const cols=visCols(); const cv=(r,c)=>{let v=cval(r,c); v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"';};
+  const csv=cols.map(c=>'"'+c.l+'"').join(',')+'\n'+rows.map(r=>cols.map(c=>cv(r,c)).join(',')).join('\n');
   const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download='cre-mls-'+rows.length+'.csv'; a.click(); URL.revokeObjectURL(a.href); }
 $('#csv').addEventListener('click',exportCSV);
 $('#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; render(); });
-fetch('/data/ranked.json').then(r=>r.json()).then(d=>{ DATA=(d.ranked||d)||[]; render(); })
+// left-rail facets + column toggles
+$('#fType').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.types,c.dataset.ty);c.classList.toggle('active');render();});
+$('#fStatus').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.status,c.dataset.st);c.classList.toggle('active');render();});
+$('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.cities,c.dataset.cy);c.classList.toggle('active');render();});
+$('#pMin').addEventListener('input',()=>{F.pmin=$('#pMin').value?+$('#pMin').value:null;render();});
+$('#pMax').addEventListener('input',()=>{F.pmax=$('#pMax').value?+$('#pMax').value:null;render();});
+$('#fFields').addEventListener('change',e=>{const cb=e.target.closest('input[data-ck]');if(!cb)return;VISCOL[cb.dataset.ck]=cb.checked;saveCols();render();});
+$('#fAll').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=true);saveCols();buildFieldToggles();render();});
+$('#fNone').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=false);saveCols();buildFieldToggles();render();});
+fetch('/data/ranked.json').then(r=>r.json()).then(d=>{ DATA=(d.ranked||d)||[]; autoCols(); buildRail(); render(); })
   .catch(()=>{ $('#count').textContent='failed to load /data/ranked.json'; });
 </script>
 </body>

← 5a3ee55 feat(condos): List/Grid toggle (both DB-listing & FHA-direct  ·  back to Commercialrealestate  ·  feat(broker-grid+index): Amazon left rail on broker-grid (ag f571111 →