← back to Commercialrealestate
Add CSV export to MLS + broker grids (exports current filtered/sorted rows)
c469c560a70d01498a321888e7d68d84e2f62e03 · 2026-06-30 17:01:59 -0700 · Steve Abrams
⬇ CSV button on both spreadsheet views — client-side export of the currently filtered+sorted rows with
all columns (proper quote-escaping). Verified headless: cre-mls-1407.csv + cre-brokers-2537.csv download, 0 errors.
Files touched
M public/broker-grid.htmlM public/mls.html
Diff
commit c469c560a70d01498a321888e7d68d84e2f62e03
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jun 30 17:01:59 2026 -0700
Add CSV export to MLS + broker grids (exports current filtered/sorted rows)
⬇ CSV button on both spreadsheet views — client-side export of the currently filtered+sorted rows with
all columns (proper quote-escaping). Verified headless: cre-mls-1407.csv + cre-brokers-2537.csv download, 0 errors.
---
public/broker-grid.html | 6 ++++++
public/mls.html | 6 ++++++
2 files changed, 12 insertions(+)
diff --git a/public/broker-grid.html b/public/broker-grid.html
index 4259943..384c74b 100644
--- a/public/broker-grid.html
+++ b/public/broker-grid.html
@@ -16,6 +16,7 @@
.toggle{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;}
.toggle button{background:var(--card);border:0;color:var(--mut);padding:8px 13px;font-size:13px;cursor:pointer;}
.toggle button.on{background:var(--gold);color:#1a1205;font-weight:700;}
+ .csvbtn{background:var(--card);border:1px solid var(--line);color:var(--acc);border-radius:9px;padding:8px 12px;font-size:13px;cursor:pointer;} .csvbtn:hover{border-color:var(--acc);}
.count{color:var(--mut);font-size:12px;}
.wrap{padding:14px 18px 60px;}
.tblwrap{overflow:auto;max-height:calc(100vh - 130px);border:1px solid var(--line);border-radius:10px;}
@@ -55,6 +56,7 @@
<span class="spacer"></span>
<input class="search" id="q" type="text" placeholder="search name, firm, phone, email, specialty…">
<div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
+ <button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
<span class="count" id="count">…</span>
</header>
<div class="wrap">
@@ -164,6 +166,10 @@ document.addEventListener('click',e=>{ const row=e.target.closest('.brow'); if(r
$('#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=(k==='listings'||k==='total_assets'||k==='created_at')?-1:1;}render();});
$('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
+function exportCSV(){ const rows=filtered(); const cv=(r,c)=>{let v=r[c.k]; 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-brokers-'+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('/api/brokers/all').then(r=>r.json()).then(d=>{DATA=d.brokers||[];render();})
diff --git a/public/mls.html b/public/mls.html
index dd52b28..8401e1a 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -16,6 +16,7 @@
.toggle{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;}
.toggle button{background:var(--card);border:0;color:var(--mut);padding:8px 13px;font-size:13px;cursor:pointer;}
.toggle button.on{background:var(--blue);color:#0b0e14;font-weight:700;}
+ .csvbtn{background:var(--card);border:1px solid var(--line);color:var(--acc);border-radius:9px;padding:8px 12px;font-size:13px;cursor:pointer;} .csvbtn:hover{border-color:var(--acc);}
.count{color:var(--mut);font-size:12px;}
.wrap{padding:14px 18px 60px;}
/* TABLE (spreadsheet) */
@@ -55,6 +56,7 @@
<span class="spacer"></span>
<input class="search" id="q" type="text" placeholder="search anything — address, city, firm, type, year…">
<div class="toggle" id="view"><button data-v="table" class="on">▦ Table</button><button data-v="grid">▤ Grid</button></div>
+ <button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
<span class="count" id="count">…</span>
</header>
<div class="wrap">
@@ -157,6 +159,10 @@ 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');
+ 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(); })
← 5a26b03 Make the grids actionable: MLS row → 100-yr property history
·
back to Commercialrealestate
·
Add .deploy.conf for Kamatera prod deploy (CRCP + FHA loans 8d4f2eb →