← back to Nationalrealestate
Deepen LA: commercial property-detail page unifying facts + brief + comps + firms
948bf607104548b2b132f5646cf028538b57668e · 2026-07-22 09:47:18 -0700 · Steve
- public/commercial-property.html: click any commercial asset -> full detail page with
the generated news brief as lede, assessed value breakdown (total/land/improvements),
building details, last-recorded date, comparable same-type assets in the same city,
and local LA real-estate firms (from the CA DRE registry). Ties together every layer
built this session (parcels + briefs + comps + broker intel) into one 'property page'.
- Wired card/marquee/feed/story clicks across the explorer + feed to open it.
- Verified headless: Beverly Center detail renders lede + 6 stats + 12 comps/firms, 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/commercial-feed.htmlA public/commercial-property.htmlM public/commercial.html
Diff
commit 948bf607104548b2b132f5646cf028538b57668e
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 22 09:47:18 2026 -0700
Deepen LA: commercial property-detail page unifying facts + brief + comps + firms
- public/commercial-property.html: click any commercial asset -> full detail page with
the generated news brief as lede, assessed value breakdown (total/land/improvements),
building details, last-recorded date, comparable same-type assets in the same city,
and local LA real-estate firms (from the CA DRE registry). Ties together every layer
built this session (parcels + briefs + comps + broker intel) into one 'property page'.
- Wired card/marquee/feed/story clicks across the explorer + feed to open it.
- Verified headless: Beverly Center detail renders lede + 6 stats + 12 comps/firms, 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/commercial-feed.html | 4 +-
public/commercial-property.html | 88 +++++++++++++++++++++++++++++++++++++++++
public/commercial.html | 4 +-
3 files changed, 92 insertions(+), 4 deletions(-)
diff --git a/public/commercial-feed.html b/public/commercial-feed.html
index ea079d3..f1f09ba 100644
--- a/public/commercial-feed.html
+++ b/public/commercial-feed.html
@@ -84,7 +84,7 @@ async function loadFeatured(){
const p=new URLSearchParams({limit:9});
if(state.type)p.set('type',state.type); if(state.city)p.set('city',state.city);
const d=await j('/api/commercial/marquee?'+p);
- $('#featured').innerHTML=d.results.map(r=>`<div class="story">
+ $('#featured').innerHTML=d.results.map(r=>`<div class="story" style="cursor:pointer" onclick="location.href='/commercial-property.html?ain='+encodeURIComponent('${r.ain}')">
<div class="h"><b>${usd(r.assessed_total)}</b> · ${r.type_label}</div>
<div class="b">${r.brief ? r.brief : `${r.address||''}${r.city?', '+r.city:''} — ${r.use_desc||''}.`}</div>
<div class="f">${r.address||''}${r.city?', '+r.city:''}</div>
@@ -96,7 +96,7 @@ async function load(){
const p=new URLSearchParams({limit:80});
if(state.type)p.set('type',state.type); if(state.city)p.set('city',state.city);
const d=await j('/api/commercial/feed?'+p);
- $('#feed').innerHTML=d.results.map(r=>`<div class="item">
+ $('#feed').innerHTML=d.results.map(r=>`<div class="item" style="cursor:pointer" onclick="location.href='/commercial-property.html?ain='+encodeURIComponent('${r.ain}')">
<div class="date">${dstr(r.recording_date)}</div>
<div class="body">
<div class="hl">${r.address||'—'}${r.city?`, ${r.city}`:''}</div>
diff --git a/public/commercial-property.html b/public/commercial-property.html
new file mode 100644
index 0000000..e384298
--- /dev/null
+++ b/public/commercial-property.html
@@ -0,0 +1,88 @@
+<!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='10' text-anchor='middle' fill='%23c8a24b'%3ECR%3C/text%3E%3C/svg%3E">
+<title>USRealEstate — Commercial Property</title>
+<style>
+ :root{--bg:#0a0d13;--panel:#11151d;--line:#222936;--fg:#e8ecf2;--muted:#8a93a3;--gold:#c8a24b;}
+ *{box-sizing:border-box;}
+ html,body{margin:0;background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;}
+ #topbar{display:flex;align-items:center;gap:14px;padding:10px 18px;background:rgba(17,21,29,.94);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20;backdrop-filter:blur(6px);}
+ .brand{font-size:16px;font-weight:700;white-space:nowrap;} .brand b{color:var(--gold);}
+ nav{display:flex;gap:14px;font-size:13px;} nav a{color:var(--muted);text-decoration:none;} nav a:hover,nav a.active{color:var(--gold);}
+ main{padding:20px 18px 60px;max-width:1000px;margin:0 auto;}
+ .tag{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--gold);}
+ h1{font-size:26px;margin:6px 0 2px;line-height:1.15;}
+ .sub{color:var(--muted);font-size:14px;}
+ .lede{font-size:16px;line-height:1.5;color:#d6dde6;font-style:italic;border-left:2px solid var(--gold);padding:4px 0 4px 14px;margin:18px 0;}
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:16px 0;}
+ .stat{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px;}
+ .stat .k{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;}
+ .stat .v{font-size:19px;font-weight:700;margin-top:3px;} .stat .v.g{color:var(--gold);}
+ h2{font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:26px 0 10px;}
+ .note{font-size:12px;color:var(--muted);line-height:1.5;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;}
+ .row{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px;}
+ .mini{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:11px;cursor:pointer;transition:border-color .12s;text-decoration:none;color:var(--fg);display:block;}
+ .mini:hover{border-color:var(--gold);} .mini .a{font-size:13px;font-weight:600;} .mini .m{font-size:11px;color:var(--muted);margin-top:4px;}
+ .mini .g{color:var(--gold);font-weight:700;}
+ #loading{padding:50px;text-align:center;color:var(--muted);}
+ .back{font-size:13px;color:var(--muted);text-decoration:none;} .back:hover{color:var(--gold);}
+</style>
+</head>
+<body>
+<div id="topbar">
+ <div class="brand"><b>USRealEstate</b> — Commercial</div>
+ <nav>
+ <a href="/commercial.html">Explorer</a><a href="/commercial-feed.html">Feed</a>
+ <a href="/commercial-brokers.html">Brokers</a>
+ </nav>
+</div>
+<main>
+ <a class="back" href="/commercial.html">← Back to explorer</a>
+ <div id="body"><div id="loading">Loading…</div></div>
+</main>
+<script>
+const $=s=>document.querySelector(s), usd=n=>n==null?'—':'$'+Number(n).toLocaleString();
+const ain=new URLSearchParams(location.search).get('ain');
+const county=new URLSearchParams(location.search).get('county')||'06037';
+async function j(u){const r=await fetch(u);return r.ok?r.json():null;}
+function dstr(d){return d?new Date(d).toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric'}):'—';}
+
+(async()=>{
+ if(!ain){$('#body').innerHTML='<div id="loading">No property specified.</div>';return;}
+ const p=await j(`/api/commercial/property/${encodeURIComponent(ain)}?county=${county}`);
+ if(!p){$('#body').innerHTML='<div id="loading">Property not found.</div>';return;}
+ // comps: same type + city, and local firms
+ const [comps, firms] = await Promise.all([
+ j(`/api/commercial/search?type=${p.type}&city=${encodeURIComponent(p.city||'')}&sort=value_desc&limit=7`),
+ p.city ? j(`/api/firms?state=CA&city=${encodeURIComponent(p.city)}&limit=6`) : Promise.resolve(null),
+ ]);
+ const compRows=(comps?.results||[]).filter(c=>c.ain!==p.ain).slice(0,6);
+ $('#body').innerHTML=`
+ <div class="tag">${p.type_label} · ${p.use_desc||''}</div>
+ <h1>${p.address||'—'}</h1>
+ <div class="sub">${p.city||''} ${p.zip||''} · APN ${p.ain}</div>
+ ${p.brief?`<div class="lede">${p.brief}</div>`:''}
+ <div class="stats">
+ <div class="stat"><div class="k">Assessed Total</div><div class="v g">${usd(p.assessed_total)}</div></div>
+ <div class="stat"><div class="k">Land</div><div class="v">${usd(p.assessed_land)}</div></div>
+ <div class="stat"><div class="k">Improvements</div><div class="v">${usd(p.assessed_improvement)}</div></div>
+ <div class="stat"><div class="k">Building Size</div><div class="v">${p.sqft?Number(p.sqft).toLocaleString()+' sqft':'—'}</div></div>
+ <div class="stat"><div class="k">Year Built</div><div class="v">${p.year_built||'—'}</div></div>
+ <div class="stat"><div class="k">Last Recorded</div><div class="v">${dstr(p.recording_date)}</div></div>
+ </div>
+ <div class="note">${p.pricing_note||''}</div>
+ <h2>Comparable ${p.type_label} Assets${p.city?` in ${p.city}`:''}</h2>
+ <div class="row">${compRows.map(c=>`<a class="mini" href="/commercial-property.html?ain=${c.ain}">
+ <div class="a">${c.address||'—'}</div>
+ <div class="m"><span class="g">${usd(c.assessed_total)}</span> · ${c.use_desc||''}</div></a>`).join('')||'<div class="note">No comparable assets found.</div>'}</div>
+ ${firms?.rows?.length?`<h2>Real Estate Firms in ${p.city}</h2>
+ <div class="row">${firms.rows.slice(0,6).map(f=>`<a class="mini" href="/commercial-brokers.html">
+ <div class="a">${f.name}</div><div class="m"><span class="g">${f.agent_count||0} agents</span>${f.license_no?` · DRE ${f.license_no}`:''}</div></a>`).join('')}</div>`:''}
+ `;
+})();
+</script>
+</body>
+</html>
diff --git a/public/commercial.html b/public/commercial.html
index 22a8431..78eea53 100644
--- a/public/commercial.html
+++ b/public/commercial.html
@@ -102,7 +102,7 @@ async function loadCities(){
}
async function loadMarquee(){
const d=await j('/api/commercial/marquee?limit=6'+(state.type?`&type=${state.type}`:''));
- $('#marquee').innerHTML=d.results.map(r=>`<div class="mq">
+ $('#marquee').innerHTML=d.results.map(r=>`<div class="mq" style="cursor:pointer" onclick="location.href='/commercial-property.html?ain='+encodeURIComponent('${r.ain}')">
<div class="val">${usd(r.assessed_total)}</div>
<div class="addr">${r.address||'—'}</div>
<div class="sub">${r.type_label} · ${r.city||''} · ${r.use_desc||''}</div>
@@ -114,7 +114,7 @@ async function load(){
if(state.type)p.set('type',state.type); if(state.city)p.set('city',state.city); if(state.q)p.set('q',state.q);
const d=await j('/api/commercial/search?'+p);
$('#count').textContent=d.total.toLocaleString()+' properties';
- $('#grid').innerHTML=d.results.map(r=>`<div class="card">
+ $('#grid').innerHTML=d.results.map(r=>`<div class="card" onclick="location.href='/commercial-property.html?ain='+encodeURIComponent('${r.ain}')">
<div class="t">${r.type_label}</div>
<div class="a">${r.address||'—'}</div>
<div class="c">${r.city||''} ${r.zip||''}</div>
← 45be23a Phase 5: CA DRE broker registry (FREE) + LA broker/firm inte
·
back to Nationalrealestate
·
Add refresh-commercial.sh — daily refresh of CA DRE registry 2bfd4ff →