← back to Nationalrealestate
public/listings.html
195 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>USRealEstate — Listings by Brokerage</title>
<link rel="stylesheet" href="/crcp-grid.css">
<style>
:root{--bg:#0a0d13;--panel:#11151d;--line:#222936;--fg:#e8ecf2;--muted:#8a93a3;--gold:#c8a24b;
--cg-card:#11151d;--cg-line:#222936;--cg-ink:#e8ecf2;--cg-mut:#8a93a3;
--cg-acc:#c8a24b;--cg-accbg:rgba(200,162,75,.12);--cg-railbg:#0a0d13;--cg-topbg:rgba(17,21,29,.94);}
*{box-sizing:border-box;}
html,body{margin:0;background:var(--bg);color:var(--fg);
font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;}
#topbar{position:sticky;top:0;z-index:1000;display:flex;align-items:center;gap:18px;flex-wrap:wrap;
padding:10px 18px;background:rgba(17,21,29,.94);border-bottom:1px solid var(--line);backdrop-filter:blur(6px);}
#topbar .brand{font-size:16px;font-weight:700;white-space:nowrap;}
#topbar .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);}
#coverage{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-left:auto;font-size:12px;}
#coverage .cov-lbl{color:var(--muted);}
.st-chip{border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--muted);
padding:4px 10px;white-space:nowrap;font-size:12px;}
.st-chip b{color:var(--fg);} .st-chip .n{color:var(--gold);font-weight:700;margin-left:5px;}
#srvbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:10px 18px;border-bottom:1px solid var(--line);
background:var(--panel);font-size:13px;}
#srvbar select,#srvbar input{background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:8px;
padding:7px 10px;font-size:13px;}
#srvbar select:focus,#srvbar input:focus{outline:none;border-color:var(--gold);}
#srvbar .lbl{color:var(--muted);}
#loading{padding:60px;text-align:center;color:var(--muted);}
.rail-note{padding:10px 12px;font-size:11px;color:var(--muted);border-top:1px solid var(--line);line-height:1.45;}
/* the BROKERAGE FIRM is the key relationship — make it prominent on every row/card */
.firm-chip{display:inline-block;background:var(--cg-accbg);border:1px solid #3a3320;color:var(--gold);
border-radius:999px;padding:1px 9px;font-size:11px;font-weight:600;white-space:nowrap;}
.viewon{color:var(--gold);text-decoration:none;font-size:12px;white-space:nowrap;}
.viewon:hover{text-decoration:underline;}
.stat{color:var(--fg);} .stat b{color:var(--gold);}
.src-tag{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--muted);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
<div class="brand"><b>USRealEstate</b> — Listings by Brokerage</div>
<nav>
<a href="/">Map</a>
<a href="/markets.html">Markets</a><a href="/commercial.html">Commercial</a>
<a href="/brokers.html">Brokers</a>
<a href="/listings.html" class="active">Listings</a>
<a href="/sources.html">Sources</a>
<a href="/watchlist.html">Watchlist</a>
<a href="/admin.html">Admin</a>
</nav>
<span id="coverage"><span class="cov-lbl">coverage:</span><span style="color:var(--muted)">loading…</span></span>
</div>
<div id="srvbar">
<span class="lbl">Brokerage firm</span>
<select id="firmSel"><option value="">All firms</option></select>
<span class="lbl">County</span>
<select id="countySel"><option value="">All counties</option></select>
<span class="lbl">Source</span>
<select id="sourceSel"><option value="">All sources</option></select>
<input type="text" id="q" placeholder="search address / firm">
</div>
<div class="cg-shell">
<div class="cg-rail" id="cgRail"></div>
<div class="cg-main" id="cgMain"><div id="loading">loading listings…</div></div>
</div>
<script src="/crcp-grid.js"></script>
<script>
const esc = s => String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
const money = v => v == null ? '—' : '$' + Number(v).toLocaleString();
let grid = null;
let ALL = [];
function firmChip(r){ return r.firm_name ? `<span class="firm-chip">🏢 ${esc(r.firm_name)}</span>` : `<span class="src-tag">firm —</span>`; }
const FIELDS = [
{ k:'address', l:'Address', def:true },
{ k:'price', l:'Price', def:true, money:true, num:true },
{ k:'beds', l:'Beds', def:true, num:true },
{ k:'baths', l:'Baths', def:true, num:true },
{ k:'sqft', l:'Sq Ft', def:true, num:true },
{ k:'firm_name', l:'Brokerage', def:true, badge:true },
{ k:'county_name', l:'County', def:true },
{ k:'status', l:'Status', def:true },
{ k:'source', l:'Source', def:true },
];
function badgeHtml(r){
// brokerage firm chip + the outbound "View on <brokerage>" link — never present as ours
const host = (() => { try { return new URL(r.url).host.replace(/^www\./,''); } catch(e){ return r.source; } })();
const viewOn = r.url ? `<a class="viewon" href="${esc(r.url)}" target="_blank" rel="noopener noreferrer">View on ${esc(host)} ↗</a>` : '';
return `${firmChip(r)} ${viewOn}`;
}
async function loadCoverage(){
const d = await fetch(location.origin + '/api/listings/stats').then(r=>r.json());
const el = document.getElementById('coverage');
const bits = (d.by_source||[]).map(s => `<span class="st-chip"><b>${esc(s.source)}</b><span class="n">${s.listings}</span></span>`);
el.innerHTML = `<span class="cov-lbl">coverage:</span>` + bits.join(' ') +
` <span class="st-chip"><b>tied to firm</b><span class="n">${d.totals.with_firm}/${d.totals.total}</span></span>`;
}
async function loadFilters(){
const [firms, rows] = await Promise.all([
fetch(location.origin + '/api/listings/firms').then(r=>r.json()),
fetch(location.origin + '/api/listings?limit=2000').then(r=>r.json()),
]);
const fsel = document.getElementById('firmSel');
for (const f of firms.firms) {
const o = document.createElement('option');
o.value = f.firm_id; o.textContent = `${f.firm_name} (${f.listings})`;
fsel.appendChild(o);
}
// county + source options derived from the loaded rows
const counties = new Map(), sources = new Set();
for (const r of rows.rows) {
if (r.region_id && r.county_name) counties.set(String(r.region_id), `${r.county_name}, ${r.state_code}`);
if (r.source) sources.add(r.source);
}
const csel = document.getElementById('countySel');
[...counties.entries()].sort((a,b)=>a[1].localeCompare(b[1])).forEach(([id,name])=>{
const o=document.createElement('option'); o.value=id; o.textContent=name; csel.appendChild(o);
});
const ssel = document.getElementById('sourceSel');
[...sources].sort().forEach(s=>{ const o=document.createElement('option'); o.value=s; o.textContent=s; ssel.appendChild(o); });
}
async function load(){
const firm = document.getElementById('firmSel').value;
const county = document.getElementById('countySel').value;
const source = document.getElementById('sourceSel').value;
const p = new URLSearchParams({ limit: '2000' });
if (firm) p.set('firm', firm);
if (county) p.set('county', county);
if (source) p.set('source', source);
const d = await fetch(location.origin + '/api/listings?' + p).then(r=>r.json());
ALL = d.rows;
render();
}
function render(){
const q = document.getElementById('q').value.trim().toLowerCase();
const rows = q ? ALL.filter(r => `${r.address||''} ${r.firm_name||''}`.toLowerCase().includes(q)) : ALL;
if (grid) { grid.setData(rows); return; }
grid = CrcpGrid.mount({
data: rows,
fields: FIELDS,
mount: '#cgMain',
rail: '#cgRail',
storageKey: 'usreListings',
cols: 3,
view: 'grid',
// address links OUT to the broker's own page (grid wraps href in target=_blank rel=noopener noreferrer);
// the sub-line names the holding brokerage FIRM — so every row links out + names the firm in BOTH views.
title: r => ({ name: r.address || '(no address)', href: r.url || undefined, sub: (r.firm_name ? '🏢 ' + r.firm_name : r.source) + (r.county_name ? ' · ' + r.county_name + ', ' + r.state_code : '') }),
titleCol: 'Address',
titleSortK: 'address',
search: r => `${r.address||''} ${r.firm_name||''} ${r.county_name||''} ${r.source}`,
badgeHtml,
sort: { k: 'price', dir: -1 },
facets: [
{ k: 'firm_name', l: 'Brokerage Firm', type: 'chip', max: 20 },
{ k: 'source', l: 'Source Site', type: 'chip', max: 10 },
{ k: 'status', l: 'Status', type: 'chip', max: 8 },
{ k: 'state_code', l: 'State', type: 'chip', max: 10 },
],
});
// rail note — the legal framing, always visible
const rail = document.getElementById('cgRail');
const note = document.createElement('div');
note.className = 'rail-note';
note.innerHTML = 'Facts only (address · price · beds · baths · sqft · location). ' +
'Each listing links out to the <b>holding brokerage\'s own page</b> — we never present a listing as ours. ' +
'Pilot sample; not a full catalog.';
rail.appendChild(note);
}
['firmSel','countySel','sourceSel'].forEach(id => document.getElementById(id).addEventListener('change', load));
document.getElementById('q').addEventListener('input', render);
loadCoverage();
loadFilters();
load();
</script>
<script src="/nav-drawer.js"></script>
<script src="/geo-search.js"></script>
</body>
</html>