← back to Commercialrealestate
public/sales.html
101 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230a0d13'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%23c8a24b'%3E%24%3C/text%3E%3C/svg%3E">
<title>Closed Sales — LA Condo Comps</title>
<link rel="stylesheet" href="/crcp-grid.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="/crcp-grid.js"></script>
<style>
:root{ --bg:#0c0f15; --panel:#141925; --fg:#E6E8EC; --muted:#8A93A2; --gold:#C8A24B; --active:#70AD47; --draft:#5B9BD5; --line:#232a38;
/* crcp-grid engine theme mapping (gold/panel) */
--cg-line:#232a38; --cg-mut:#8A93A2; --cg-ink:#E6E8EC; --cg-card:#141925;
--cg-acc:#C8A24B; --cg-accbg:rgba(200,162,75,.12); --cg-railbg:#0b0e13; --cg-topbg:rgba(12,15,21,.95); --cg-red:#f85149; }
*{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;}
header{display:flex;align-items:center;gap:14px;padding:14px 22px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:20;}
header h1{margin:0;font-size:17px;} header h1 b{color:var(--gold);} header a{color:var(--draft);text-decoration:none;font-size:13px;}
header .nav{margin-left:auto;display:flex;gap:14px;}
.stat{color:var(--muted);font-size:13px;padding:12px 22px 4px;} .stat b{color:var(--fg);}
#depthNote{font-size:12px;color:var(--muted);margin:2px 22px 8px;padding:6px 10px;border-left:2px solid var(--gold);background:rgba(200,162,75,.06);}
.charts{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:18px;} @media(max-width:880px){.charts{grid-template-columns:1fr;}}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:16px;}
.panel h3{margin:0 0 12px;font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);}
canvas{max-height:240px;}
.price{color:var(--active);font-weight:600;}
</style>
</head>
<body>
<header>
<h1><b>Closed Sales</b> · LA condo comps</h1>
<nav class="nav"><a href="/crcp.html">← CRCP</a><a href="/condos.html">Condos</a><a href="/graphics.html">Graphics</a><a href="/fha-loans.html">FHA Loans</a></nav>
</header>
<div class="stat" id="stat">loading…</div>
<div id="depthNote"></div>
<div class="cg-shell">
<aside class="cg-rail" id="cgRail"></aside>
<div class="cg-main" id="cgMain"><div class="cg-empty" style="padding:50px">loading sold comps…</div></div>
</div>
<div class="charts">
<div class="panel"><h3>By city (avg price)</h3><canvas id="cityChart"></canvas></div>
<div class="panel"><h3>Sales by year</h3><canvas id="yearChart"></canvas></div>
</div>
<script>
const $=s=>document.querySelector(s);
const money=n=>n?'$'+Number(n).toLocaleString():'—';
let cityChart, yearChart;
// Every data point — each toggleable (cards + list columns).
const FIELDS=[
{k:'sold_price',l:'Sold', money:1, col:1, def:1},
{k:'sold_date', l:'Sold date', col:1, def:1, calc:r=>String(r.sold_date||'').slice(0,10)},
{k:'beds', l:'Beds', num:1, col:1, def:1},
{k:'baths', l:'Baths', num:1, col:1, def:1},
{k:'sqft', l:'Sq Ft', num:1, col:1, def:1},
{k:'ppsf', l:'$/Sq Ft', money:1, col:1, def:1, calc:r=>r.sqft?Math.round(r.sold_price/r.sqft):null},
{k:'city', l:'City', col:1, def:1},
{k:'zip', l:'ZIP', col:1, def:0},
];
const FACETS=[
{k:'city', l:'City', type:'chip', max:20},
{k:'year', l:'Sold year', type:'chip', calc:r=>String(r.sold_date||'').slice(0,4), max:12},
{k:'beds', l:'Beds', type:'chip', calc:r=>String(r.beds||'—')},
{k:'sold_price',l:'Sold price $',type:'range', money:1},
{k:'sqft', l:'Sq Ft', type:'range'},
{k:'ppsf', l:'$/Sq Ft', type:'range', money:1, calc:r=>r.sqft?Math.round(r.sold_price/r.sqft):null},
];
async function load(){
let d;
try{ d=await (await fetch('/api/closed-sales?limit=5000')).json(); }
catch(e){ $('#cgMain').innerHTML='<div class="cg-empty" style="padding:50px">sold-comps load error.</div>'; return; }
const t=d.total||{};
$('#stat').innerHTML=`<b>${(t.n||0).toLocaleString()}</b> sold condos · avg <b>${money(t.avg_price)}</b> · ${(t.oldest||'').toString().slice(0,10)} → ${(t.newest||'').toString().slice(0,10)} · <span style="color:var(--muted)">${d.source||''}</span>`;
if(d.depthNote) $('#depthNote').textContent='ℹ '+d.depthNote;
CrcpGrid.mount({
data:d.rows||[], fields:FIELDS, facets:FACETS,
mount:'#cgMain', rail:'#cgRail', view:'list', cols:3,
storageKey:'sales', titleCol:'Address',
title:r=>({ name:r.address||'—', sub:[r.city,r.zip].filter(Boolean).join(' '), href:r.url||'' }),
search:r=>((r.address||'')+' '+(r.city||'')+' '+(r.zip||'')),
});
// charts (over the loaded window)
const bc=d.byCity||[];
if(cityChart) cityChart.destroy();
cityChart=new Chart($('#cityChart'),{type:'bar',data:{labels:bc.map(c=>c.city),datasets:[{label:'avg $',data:bc.map(c=>c.avg_price),backgroundColor:'#C8A24B'}]},options:{plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8A93A2',font:{size:9}}},y:{ticks:{color:'#8A93A2'}}}}});
const by=d.byYear||[];
if(yearChart) yearChart.destroy();
yearChart=new Chart($('#yearChart'),{type:'line',data:{labels:by.map(y=>y.yr),datasets:[{label:'sales',data:by.map(y=>y.n),borderColor:'#70AD47',backgroundColor:'rgba(112,173,71,.15)',fill:true,tension:.3}]},options:{plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8A93A2'}},y:{ticks:{color:'#8A93A2'}}}}});
}
load();
</script>
<script src="/col-resize.js" defer></script>
</body>
</html>