← back to Commercialrealestate
feat(index): Compare -> deliverable (DTD verdict A, 2/3 Codex+Claude; Qwen dissent C=similar-in-city) — ⬇ CSV export of the compared listings (one row per listing x 17 metric columns, entity-decoded) + 🖨 Print with a scoped @media print stylesheet (body.printing-compare) that prints only the side-by-side clean black-on-white for deal memos, without breaking normal Cmd+P. Contrarian-gated, 0 errors.
6c08b42b199c9886f7f08ae945ae518678c39b53 · 2026-07-02 15:16:39 -0700 · Steve Abrams
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit 6c08b42b199c9886f7f08ae945ae518678c39b53
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 15:16:39 2026 -0700
feat(index): Compare -> deliverable (DTD verdict A, 2/3 Codex+Claude; Qwen dissent C=similar-in-city) — ⬇ CSV export of the compared listings (one row per listing x 17 metric columns, entity-decoded) + 🖨 Print with a scoped @media print stylesheet (body.printing-compare) that prints only the side-by-side clean black-on-white for deal memos, without breaking normal Cmd+P. Contrarian-gated, 0 errors.
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
public/index.html | 27 ++++++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/public/index.html b/public/index.html
index 32b9cb3..a51450c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -214,6 +214,20 @@
.cmptbl .cmpk, .cmptbl thead th.cmpk { text-align:left; color:var(--mut); position:sticky; left:0; background:var(--card); }
.cmptbl thead th .cmpsub { color:var(--mut); font-size:11px; font-weight:400; }
.cmphint { color:var(--mut); padding:24px 4px; }
+ .cmptools { display:flex; gap:8px; justify-content:flex-end; margin-bottom:10px; }
+ .cmptools button { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:8px; padding:6px 12px; font-size:12px; cursor:pointer; }
+ .cmptools button:hover { border-color:var(--blue); }
+ @media print {
+ body.printing-compare * { visibility:hidden; }
+ body.printing-compare #cmpov, body.printing-compare #cmpov * { visibility:visible; }
+ body.printing-compare #cmpov { position:absolute; inset:0; background:#fff; display:block; padding:0; overflow:visible; }
+ body.printing-compare #cmpmodal { width:100%; box-shadow:none; border:0; background:#fff; color:#000; }
+ body.printing-compare #cmpmodal h3 { color:#000; }
+ body.printing-compare .cmpx, body.printing-compare .no-print { display:none !important; }
+ body.printing-compare .cmptbl th, body.printing-compare .cmptbl td { color:#000; border-color:#999; }
+ body.printing-compare .cmptbl thead th, body.printing-compare .cmptbl .cmpk { background:#fff; color:#000; }
+ body.printing-compare .cmpsub { color:#444; }
+ }
</style>
</head>
<body>
@@ -1090,9 +1104,18 @@ function openCompare(){
if(items.length<2){ body.innerHTML='<div class="cmphint">Star at least 2 listings (the ★ on any card) to compare them side by side.</div>'; ov.classList.add('on'); return; }
const head='<tr><th class="cmpk" scope="col"></th>'+items.map(p=>`<th scope="col">${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
const rows=CMP.map(m=>`<tr><td class="cmpk">${m.l}</td>`+items.map(p=>`<td>${m.f(p)}</td>`).join('')+'</tr>').join('');
- body.innerHTML=`<table class="cmptbl"><thead>${head}</thead><tbody>${rows}</tbody></table>`;
+ body.innerHTML=`<div class="cmptools no-print"><button id="cmpcsv">⬇ CSV</button><button id="cmpprint">🖨 Print</button></div><table class="cmptbl"><thead>${head}</thead><tbody>${rows}</tbody></table>`;
ov.classList.add('on');
}
+function decodeEnt(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,"'"); }
+function exportCompareCSV(){
+ const items=DATA.ranked.filter(p=>SHORT.has(p.id)); if(items.length<2) return;
+ const cols=['Address','City'].concat(CMP.map(m=>m.l));
+ const lines=[cols.map(csvEsc).join(',')];
+ items.forEach(p=>{ const row=[p.address,p.city].concat(CMP.map(m=>decodeEnt(m.f(p)))); 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-compare-'+items.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
+}
+function printCompare(){ document.body.classList.add('printing-compare'); window.print(); }
// ---- paginated DOM (perf): render in pages, "Show more" appends ----
let _rows=[], _shown=0;
const PAGE=()=> view==='list'?200:60;
@@ -1409,6 +1432,8 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
$('#cmpbtn').onclick=openCompare;
$('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };
$('#cmpov').addEventListener('click',e=>{ if(e.target.id==='cmpov') e.currentTarget.classList.remove('on'); });
+ $('#cmpmodal').addEventListener('click',e=>{ if(e.target.closest('#cmpcsv')) exportCompareCSV(); else if(e.target.closest('#cmpprint')) printCompare(); });
+ window.addEventListener('afterprint',()=>document.body.classList.remove('printing-compare'));
// a11y: Escape closes any open overlay (compare / broker / property-history modals)
document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ ['cmpov','bkov'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('on'); }); } });
← 6d8e182 feat(condos): surface parity (DTD verdict C, 2/3 Codex+Claud
·
back to Commercialrealestate
·
feat(index): saved searches (DTD verdict D, 2/3 Codex+Claude 45fe54b →