← back to Commercialrealestate
feat(index): ⬇ CSV export of the current filtered+sorted view (visible columns, proper escaping) + column-sort persisted in the shareable URL (?cs=key:a|d restores click-to-sort). Contrarian-gated, 0 errors.
c919ae1670c38a0aa0807f7ce073283116c8f46d · 2026-07-02 11:10:22 -0700 · Steve Abrams
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit c919ae1670c38a0aa0807f7ce073283116c8f46d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 11:10:22 2026 -0700
feat(index): ⬇ CSV export of the current filtered+sorted view (visible columns, proper escaping) + column-sort persisted in the shareable URL (?cs=key:a|d restores click-to-sort). Contrarian-gated, 0 errors.
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
public/index.html | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/public/index.html b/public/index.html
index e9108a5..49e4427 100644
--- a/public/index.html
+++ b/public/index.html
@@ -422,6 +422,7 @@
<button data-view="list">☰ List</button>
</span>
<div class="right">
+ <button id="csvbtn" class="ex" title="Download the current filtered + sorted view (visible columns) as CSV">⬇ CSV</button>
<button id="linkbtn" class="ex" title="Copy a shareable link to this exact filtered view">🔗 Copy link</button>
<label>Sort</label>
<select id="sortTop" style="width:auto;min-width:180px"></select>
@@ -933,6 +934,7 @@ function stateToURL(){
if(F.nonqm) p.set('nq','1');
if(scenario!=='25') p.set('sc',scenario);
const so=$('#sort')&&$('#sort').value; if(so && so!=='deal') p.set('sort',so);
+ if(colSort) p.set('cs',colSort.key+':'+(colSort.dir>0?'a':'d'));
if(view!=='grid') p.set('v',view);
const qs=p.toString();
// NOTE: the app declares a local `history` (chat log) that shadows window.history — must qualify.
@@ -955,8 +957,18 @@ function urlToState(){
if(p.get('sc')){ scenario=p.get('sc'); }
if(p.get('sort')&&$('#sort')){ $('#sort').value=p.get('sort'); if($('#sortTop'))$('#sortTop').value=p.get('sort'); }
if(p.get('v')){ view=p.get('v'); }
+ if(p.get('cs')){ const [k,d]=p.get('cs').split(':'); colSort={key:k,dir:d==='a'?1:-1}; }
return any;
}
+function csvEsc(v){ v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"'; }
+function exportCSV(){
+ const cols=listCols();
+ const rows=sortRows(applyFilters(DATA.ranked.slice()));
+ const head=['Address','City','ZIP'].concat(cols.map(c=>c.label));
+ const lines=[head.map(csvEsc).join(',')];
+ rows.forEach(p=>{ const row=[p.address,p.city,p.zip].concat(cols.map(c=>fieldVal(p,c.k))); lines.push(row.map(csvEsc).join(',')); });
+ const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-explorer-'+rows.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
+}
function copyShareLink(){ const t=$('#linkbtn'); try{ navigator.clipboard.writeText(location.href); if(t){ const o=t.textContent; t.textContent='✓ Copied'; setTimeout(()=>t.textContent=o,1400); } }catch(e){ if(t) t.textContent='⌘C to copy'; } }
function render(){
const g=$('#grid'); const fc=activeFilterCount();
@@ -1177,6 +1189,7 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
// active-filter summary bar: remove one filter or reset all
$('#activebar').addEventListener('click',e=>{ if(e.target.closest('#afreset')){ resetAll(); return; } const c=e.target.closest('.afchip'); if(c) clearFilter(c.dataset.clear); });
$('#linkbtn').onclick=copyShareLink;
+ $('#csvbtn').onclick=exportCSV;
// facet chip handlers (multi-select toggles)
$('#fType').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return;
if(c.dataset.warr==='nonqm'){ F.nonqm=!F.nonqm; syncChips(); render(); return; }
← acf3768 feat(mls+broker-grid): fleet consistency (DTD Option D) — co
·
back to Commercialrealestate
·
feat(index): shortlist — ⭐ star any listing (grid + list), d 8b66d89 →