← back to Rentv 2026
compare: Deal Comparison (/compare) — side-by-side price/$per-unit/$per-SF/occupancy/vintage for up to 4 tracked deals, best-in-row highlight, shareable ?ids= link, compact scrollable picker
7c7b84125f6c87115c5d04a3b3148c86ed09987b · 2026-07-23 16:30:31 -0700 · Steve
Files touched
A public/compare.htmlM public/index.htmlA tools/specs/compare.json
Diff
commit 7c7b84125f6c87115c5d04a3b3148c86ed09987b
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 23 16:30:31 2026 -0700
compare: Deal Comparison (/compare) — side-by-side price/$per-unit/$per-SF/occupancy/vintage for up to 4 tracked deals, best-in-row highlight, shareable ?ids= link, compact scrollable picker
---
public/compare.html | 167 +++++++++++++++++++++++++++++++++++++++++++++++
public/index.html | 2 +-
tools/specs/compare.json | 23 +++++++
3 files changed, 191 insertions(+), 1 deletion(-)
diff --git a/public/compare.html b/public/compare.html
new file mode 100644
index 00000000..6ef53b2c
--- /dev/null
+++ b/public/compare.html
@@ -0,0 +1,167 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Deal Comparison — RENTV.com</title>
+<meta name="description" content="Compare RENTV-tracked commercial real estate deals side by side — price, price per unit and per square foot, occupancy and vintage.">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,800;1,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--wash:#f5f6f8;--up:#0f8a4d}
+ *{box-sizing:border-box;margin:0}
+ body{font-family:'Inter',system-ui,sans-serif;color:var(--ink);background:#fbfbfc;line-height:1.45}
+ a{color:inherit;text-decoration:none}
+ .top{position:sticky;top:0;z-index:900;display:flex;align-items:center;justify-content:space-between;height:54px;padding:0 16px;background:#fff;border-bottom:1px solid var(--line)}
+ .burger{display:inline-flex;align-items:center;gap:10px;border:0;background:none;cursor:pointer;font:inherit;color:inherit;padding:7px 9px;border-radius:8px}
+ .burger:hover{background:rgba(0,0,0,.06)}
+ .burger .lbl{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sub)}
+ .brand{position:absolute;left:50%;transform:translateX(-50%);font-family:'Playfair Display',Georgia,serif;font-weight:800;font-size:25px;letter-spacing:.02em}.brand span{color:var(--red)}
+ .user{width:37px;height:37px;border-radius:50%;border:1.5px solid var(--line);background:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ink)}
+ .user:hover{border-color:var(--red);color:var(--red)}
+ .scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:opacity .22s;z-index:950}
+ .scrim.open{opacity:1;pointer-events:auto}
+ .drawer{position:fixed;top:0;left:0;height:100%;width:360px;max-width:88vw;background:#0b0e12;color:#e7ecf1;transform:translateX(-102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:951;display:flex;flex-direction:column;overflow-y:auto;box-shadow:2px 0 40px rgba(0,0,0,.4)}
+ .drawer.open{transform:none}
+ .drawer .dh{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid #1c232b}
+ .drawer .dh .w{font-family:'Playfair Display',serif;font-size:23px;font-weight:800}.drawer .dh .w span{color:var(--red)}
+ .drawer .dh button{background:none;border:0;color:#8b949e;font-size:22px;cursor:pointer;line-height:1}
+ .grp{padding:14px 18px 4px}.grp .gt{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#6b747d;margin-bottom:6px}
+ .drawer nav a{display:block;padding:9px 18px;color:#dbe1e7;font-size:14.5px;border-left:3px solid transparent}
+ .drawer nav a:hover{background:#141a21;border-left-color:var(--red);color:#fff}
+ .drawer nav a.active{background:#141a21;border-left-color:var(--red);color:#fff;font-weight:700}
+ .drawer .df{margin-top:auto;padding:16px 18px;border-top:1px solid #1c232b;font-size:11px;color:#6b747d}
+
+ .wrap{max-width:1120px;margin:0 auto;padding:0 22px;width:100%}
+ .chead{padding:34px 0 6px}
+ .chead .k{color:var(--red);font-weight:800;font-size:12px;letter-spacing:.16em;text-transform:uppercase}
+ .chead h1{font-family:'Playfair Display',serif;font-size:clamp(32px,3.6vw,46px);font-weight:700;letter-spacing:-.01em;margin-top:4px}
+ .chead p{color:var(--sub);font-size:15px;margin-top:4px}
+
+ .picker{background:#fff;border:1px solid var(--line);border-radius:14px;padding:14px 16px;margin:18px 0 8px}
+ .picker .h{font-size:11px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--sub);margin-bottom:10px}
+ .opts{display:flex;flex-wrap:wrap;gap:8px;max-height:132px;overflow-y:auto;padding-right:4px}
+ .opt{border:1.5px solid var(--line);background:#fff;border-radius:9px;padding:8px 12px;font:600 12.5px 'Inter';color:var(--ink);cursor:pointer;max-width:280px;text-align:left;transition:.14s}
+ .opt:hover{border-color:var(--red)}
+ .opt.on{background:var(--red);border-color:var(--red);color:#fff}
+ .opt .t{display:block;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:250px}
+ .opt .s{display:block;font-size:11px;opacity:.75;margin-top:1px}
+ .hint{color:var(--sub);font-size:12.5px;margin:6px 2px 0}
+
+ .tablewrap{overflow-x:auto;padding:16px 0 60px}
+ table.cmp{width:100%;border-collapse:collapse;min-width:640px}
+ table.cmp th,table.cmp td{text-align:left;padding:13px 14px;border-bottom:1px solid var(--line);vertical-align:top}
+ table.cmp thead th{position:sticky;top:54px;background:#fbfbfc}
+ table.cmp .metric{font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--sub);width:150px;white-space:nowrap}
+ table.cmp .deal-h{font-family:'Playfair Display',serif;font-size:17px;font-weight:800;line-height:1.25}
+ table.cmp .deal-h .tag{display:inline-block;font-family:'Inter';font-size:9px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;background:var(--red);color:#fff;border-radius:20px;padding:2px 8px;margin-bottom:6px;vertical-align:middle}
+ table.cmp td .v{font-weight:700;font-size:15px}
+ table.cmp td .v.big{font-family:'Playfair Display',serif;font-size:20px;color:var(--red)}
+ table.cmp td .v.best{color:var(--up)}
+ table.cmp td .dash{color:#c3c9cf}
+ .empty{padding:44px 0;text-align:center;color:var(--sub)}
+ .empty h3{font-family:'Playfair Display',serif;font-size:23px;color:var(--ink);margin-bottom:6px}
+</style></head><body>
+
+<header class="top">
+ <button class="burger" id="burger" aria-label="Menu">
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
+ <span class="lbl">Menu</span>
+ </button>
+ <a class="brand" href="/">REN<span>TV</span></a>
+ <a class="user" href="/search" aria-label="Search" title="Search">
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
+ </a>
+</header>
+
+<div class="scrim" id="scrim"></div>
+<aside class="drawer" id="drawer">
+ <div class="dh"><div class="w">REN<span>TV</span></div><button id="dx" aria-label="Close">×</button></div>
+ <div class="grp"><div class="gt">Discover</div></div>
+ <nav><a href="/search">Search</a><a href="/pulse">Market Pulse</a><a href="/tools">CRE Toolkit</a><a href="/watchlist">Deal Watchlist</a><a href="/directory">CRE Directory</a><a href="/compare" class="active">Deal Comparison</a></nav>
+ <div class="grp"><div class="gt">Sections</div></div>
+ <nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
+ <div class="grp"><div class="gt">Intelligence</div></div>
+ <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker & Owner Desk</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
+ <div class="grp"><div class="gt">Company</div></div>
+ <nav><a href="/about">About</a><a href="/advertise">Advertise</a><a href="/subscribe">Subscribe / Newsletters</a></nav>
+ <div class="df">RENTV.com — Commercial real estate news, Western U.S.</div>
+</aside>
+
+<main class="wrap">
+ <div class="chead">
+ <div class="k">Deal Comparison</div>
+ <h1>Put deals side by side</h1>
+ <p>Pick up to four tracked transactions and compare the numbers that matter — price, per-unit, per-SF, occupancy and vintage.</p>
+ </div>
+
+ <div class="picker">
+ <div class="h">Choose deals to compare <span id="pcount" style="color:var(--red)"></span></div>
+ <div class="opts" id="opts"></div>
+ <div class="hint">Tap to add or remove. Priced deals compare best.</div>
+ </div>
+
+ <div class="tablewrap" id="tablewrap"></div>
+</main>
+
+<script>
+const $=id=>document.getElementById(id);
+const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const drawer=$('drawer'),scrim=$('scrim');
+$('burger').onclick=()=>{drawer.classList.add('open');scrim.classList.add('open')};
+const close=()=>{drawer.classList.remove('open');scrim.classList.remove('open')};
+$('dx').onclick=close;scrim.onclick=close;document.addEventListener('keydown',e=>{if(e.key==='Escape')close()});
+
+const MAX=4;
+let ALL=[], pick=[];
+const usd=n=>!isFinite(n)||!n?'—':n>=1e6?('$'+(n/1e6).toFixed(1)+'M'):n>=1e3?('$'+Math.round(n).toLocaleString()):('$'+Math.round(n));
+const num=n=>(isFinite(n)&&n)?Math.round(n).toLocaleString():null;
+
+function sync(){ const u=new URLSearchParams(); if(pick.length)u.set('ids',pick.join(',')); history.replaceState(null,'','/compare'+(pick.length?'?'+u.toString():'')); }
+function toggle(id){ const i=pick.indexOf(id); if(i>=0)pick.splice(i,1); else { if(pick.length>=MAX)return; pick.push(id);} sync(); render(); }
+
+function renderOpts(){
+ $('pcount').textContent=pick.length?`(${pick.length}/${MAX})`:'';
+ $('opts').innerHTML=ALL.map(d=>`<button class="opt${pick.includes(String(d.id))?' on':''}" data-id="${esc(d.id)}"><span class="t">${esc(d.title)}</span><span class="s">${[d.property_type,d.amount_label].filter(Boolean).map(esc).join(' · ')}</span></button>`).join('');
+ $('opts').querySelectorAll('.opt').forEach(b=>b.onclick=()=>toggle(b.dataset.id));
+}
+
+const ROWS=[
+ ['Transaction', d=>d.txn_type||'—'],
+ ['Location', d=>[d.city,d.state].filter(Boolean).join(', ')||'—'],
+ ['Price', d=>usd(d.amount), 'big', d=>d.amount],
+ ['Units', d=>num(d.units)||'—'],
+ ['Rentable SF', d=>num(d.sqft)||'—'],
+ ['Price / unit', d=>(d.amount&&d.units)?usd(d.amount/d.units):'—', 0, d=>(d.amount&&d.units)?d.amount/d.units:null, 'low'],
+ ['Price / SF', d=>(d.amount&&d.sqft)?'$'+Math.round(d.amount/d.sqft).toLocaleString():'—', 0, d=>(d.amount&&d.sqft)?d.amount/d.sqft:null, 'low'],
+ ['Occupancy', d=>d.occupancy_pct?d.occupancy_pct+'%':'—', 0, d=>d.occupancy_pct||null, 'high'],
+ ['Year built', d=>d.year_built||'—', 0, d=>d.year_built||null, 'high'],
+];
+
+function render(){
+ renderOpts();
+ const sel=pick.map(id=>ALL.find(d=>String(d.id)===String(id))).filter(Boolean);
+ if(!sel.length){ $('tablewrap').innerHTML='<div class="empty"><h3>Pick deals to compare</h3><div>Select two or more above to see them side by side.</div></div>'; return; }
+ let html='<table class="cmp"><thead><tr><th class="metric"></th>'+sel.map(d=>`<th><div class="deal-h">${d.property_type?`<span class="tag">${esc(d.property_type)}</span><br>`:''}${esc(d.title)}</div></th>`).join('')+'</tr></thead><tbody>';
+ ROWS.forEach(([label,fn,cls,valFn,dir])=>{
+ let bestIdx=-1;
+ if(valFn&&dir&&sel.length>1){
+ const vals=sel.map(valFn);
+ let best=null; vals.forEach((v,i)=>{ if(v==null)return; if(best==null||(dir==='low'?v<best:v>best)){best=v;bestIdx=i;} });
+ }
+ html+=`<tr><td class="metric">${esc(label)}</td>`+sel.map((d,i)=>{
+ const val=fn(d);
+ const c=(cls==='big'?' big':'')+(i===bestIdx?' best':'');
+ return `<td><span class="v${c}">${val==='—'?'<span class="dash">—</span>':esc(val)}</span></td>`;
+ }).join('')+'</tr>';
+ });
+ html+='</tbody></table>';
+ $('tablewrap').innerHTML=html;
+}
+
+(async function(){
+ try{ ALL=((await (await fetch('/api/deals?limit=200')).json()).deals||[]); }catch(e){ $('opts').innerHTML='<div class="empty"><h3>Deals unavailable</h3></div>'; return; }
+ const ids=(new URLSearchParams(location.search).get('ids')||'').split(',').map(s=>s.trim()).filter(Boolean);
+ const valid=new Set(ALL.map(d=>String(d.id)));
+ pick=ids.filter(id=>valid.has(id)).slice(0,MAX);
+ render();
+})();
+</script>
+</body></html>
diff --git a/public/index.html b/public/index.html
index f8391444..e1398561 100644
--- a/public/index.html
+++ b/public/index.html
@@ -104,7 +104,7 @@
<div class="dh"><span class="w">REN<span>TV</span></span><button id="dclose" aria-label="Close">✕</button></div>
<div class="snap" id="snap"></div>
<div class="grp"><div class="gt">Discover</div></div>
- <nav><a href="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</a><a href="/tools">🧮 CRE Toolkit</a><a href="/watchlist">⭐ Deal Watchlist</a><a href="/directory">🏢 CRE Directory</a></nav>
+ <nav><a href="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</a><a href="/tools">🧮 CRE Toolkit</a><a href="/watchlist">⭐ Deal Watchlist</a><a href="/directory">🏢 CRE Directory</a><a href="/compare">⚖️ Deal Comparison</a></nav>
<div class="grp"><div class="gt">Sections</div></div>
<nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
<div class="grp"><div class="gt">Intelligence</div></div>
diff --git a/tools/specs/compare.json b/tools/specs/compare.json
new file mode 100644
index 00000000..8804561d
--- /dev/null
+++ b/tools/specs/compare.json
@@ -0,0 +1,23 @@
+{
+ "slug": "compare",
+ "title": "Deal Comparison",
+ "subtitle": "Put deals side by side",
+ "voice": "Samantha",
+ "scenes": [
+ {
+ "url": "/compare",
+ "say": "Deal Comparison lets you put RENTV's tracked transactions head to head. Just pick the ones you want to weigh up.",
+ "caption": "Pick tracked deals to compare head-to-head"
+ },
+ {
+ "url": "/compare?ids=34208,34218,34177",
+ "say": "Choose a few — here, three Western multifamily sales — and RENTV lines up price, price per unit and price per square foot in one clean table.",
+ "caption": "Price, price-per-unit, price-per-SF — lined up"
+ },
+ {
+ "url": "/compare?ids=34208,34218,34177",
+ "say": "It even highlights the sharpest number in every row, so you can see at a glance which deal priced best. And the whole comparison lives in the link, so it's easy to share.",
+ "caption": "Best number in each row is highlighted · shareable link"
+ }
+ ]
+}
← 5a556954 directory: searchable CRE Directory (/directory) over live f
·
back to Rentv 2026
·
auto-save: 2026-07-23T16:52:38 (3 files) — data/markets.json f73bbee3 →