← back to Sublease Agentabrams
public/index.html
221 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sublease Marketplace · agentabrams</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css">
<style>
:root{--ink:#14181d;--mut:#6b7480;--line:#e7e9ee;--bg:#fbfbfc;--accent:#1a5f3c;--gold:#b08a3e;--sale:#2b6cb0;--cols:4}
*{box-sizing:border-box}
body{margin:0;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Helvetica,Arial,sans-serif;color:var(--ink);background:var(--bg)}
a{color:inherit;text-decoration:none}
.wrap{max-width:1340px;margin:0 auto;padding:0 22px}
header.top{border-bottom:1px solid var(--line);background:#fff;position:sticky;top:0;z-index:500}
.top .wrap{display:flex;align-items:center;justify-content:space-between;height:62px}
.brand{font-weight:700;letter-spacing:.02em;font-size:19px}.brand small{color:var(--gold);font-weight:600}
nav a{color:var(--mut);margin-left:20px;font-size:14px}
.hero{background:linear-gradient(135deg,#14181d,#233041);color:#fff;padding:48px 0 40px}
.hero h1{font-size:33px;margin:0 0 8px;font-weight:700;letter-spacing:-.01em}
.hero p{color:#c6cdd6;margin:0;max-width:660px}
.stats{display:flex;gap:32px;margin-top:24px;flex-wrap:wrap}
.stats b{display:block;font-size:25px;color:#fff}.stats span{color:#9aa6b2;font-size:12px;text-transform:uppercase;letter-spacing:.08em}
section{padding:32px 0}
h2{font-size:15px;text-transform:uppercase;letter-spacing:.1em;color:var(--mut);margin:0 0 16px;font-weight:700}
h2 .sub{color:var(--gold);text-transform:none;letter-spacing:0;font-weight:600;font-size:13px;margin-left:8px}
.sponsors{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}
.sp{border:1px solid var(--line);border-radius:12px;background:#fff;padding:16px;display:flex;flex-direction:column;gap:9px;transition:.15s;min-height:120px}
.sp:hover{border-color:var(--accent);box-shadow:0 6px 22px rgba(20,24,29,.07);transform:translateY(-2px)}
.sp img{height:34px;max-width:140px;object-fit:contain;object-position:left}
.sp .cnt{font-weight:700;color:var(--accent);font-size:13px}.sp .fin{color:var(--gold);font-size:12px;text-transform:uppercase;letter-spacing:.05em}
.badge{display:inline-block;font-size:10px;padding:2px 7px;border-radius:20px;background:#f0f2f5;color:var(--mut);text-transform:uppercase;letter-spacing:.05em}
.badge.high{background:#fbe9e7;color:#b3261e}
#map{height:440px;border-radius:14px;border:1px solid var(--line);z-index:1}
.controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px}
.controls select,.controls input[type=search]{padding:8px 10px;border:1px solid var(--line);border-radius:8px;font-size:14px;background:#fff}
.subtog{display:flex;align-items:center;gap:7px;padding:7px 12px;border:1px solid var(--accent);border-radius:8px;color:var(--accent);font-weight:600;font-size:13px;cursor:pointer;user-select:none}
.subtog.on{background:var(--accent);color:#fff}
.dens{display:flex;align-items:center;gap:8px;color:var(--mut);font-size:13px;margin-left:auto}
.count{color:var(--mut);font-size:13px;margin-bottom:12px}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px}
.card{border:1px solid var(--line);border-radius:12px;background:#fff;overflow:hidden;display:flex;flex-direction:column}
.card .top{padding:11px 13px 0;display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
.kind{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:5px}
.kind.sublease{background:#e7f5ec;color:var(--accent)}.kind.sale{background:#e8f0fb;color:var(--sale)}.kind.lease{background:#fff4e5;color:var(--gold)}
.card .b{padding:6px 13px 12px}.card .t{font-weight:600;font-size:14px;line-height:1.35}.card .a{color:var(--mut);font-size:12.5px;margin-top:3px}
.meta{display:flex;gap:7px;flex-wrap:wrap;margin-top:8px;font-size:12px}.chip{background:#f4f6f8;border-radius:6px;padding:2px 7px;color:#41505f}
.card .foot{margin-top:auto;padding:9px 13px;border-top:1px solid var(--line);display:flex;align-items:center;gap:8px;font-size:12px;color:var(--mut)}
.card .foot img{height:15px;max-width:64px;object-fit:contain}
.empty{color:var(--mut);border:1px dashed var(--line);border-radius:12px;padding:26px;text-align:center;grid-column:1/-1}
/* brokers */
.bgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
.bk{border:1px solid var(--line);border-radius:11px;background:#fff;padding:14px}
.bk .n{font-weight:700}.bk .f{color:var(--mut);font-size:12.5px;margin:2px 0 6px}
.bk .row{font-size:12.5px;color:#41505f}.bk a{color:var(--accent)}
footer{border-top:1px solid var(--line);color:var(--mut);font-size:12px;padding:24px 0;background:#fff}
</style>
</head>
<body>
<header class="top"><div class="wrap">
<div class="brand">Sublease<small>Marketplace</small></div>
<nav><a href="#map-sec">Map</a><a href="#listings">Listings</a><a href="#brokers">Brokers</a><a href="#sponsors">Sponsors</a><a href="/account" id="acctLink">Account</a><a href="/admin.html" id="adminLink" style="display:none">Admin</a></nav>
</div></header>
<div class="hero"><div class="wrap">
<h1>Commercial real estate, every broker in one place.</h1>
<p>A live marketplace of commercial listings & subleases aggregated from leading brokers, sponsors and marketplaces — powered by CRUnifiedDB.</p>
<div class="stats" id="stats"></div>
</div></div>
<section id="sponsors"><div class="wrap">
<h2>Sponsor brokers & financing partners</h2>
<div class="sponsors" id="sponsorWall"></div>
</div></section>
<section id="map-sec"><div class="wrap">
<h2>Listings map <span class="sub" id="mapNote"></span></h2>
<div id="map"></div>
</div></section>
<section id="listings"><div class="wrap">
<h2>Listings</h2>
<div class="controls">
<input type="search" id="q" placeholder="Search address, city, title…">
<div class="subtog" id="subtog">◱ Subleases only</div>
<select id="fType"><option value="">All space types</option></select>
<select id="fSponsor"><option value="">All sponsors</option></select>
<select id="sort">
<option value="featured">Featured (subleases first)</option>
<option value="size_desc">Size ↓</option><option value="size_asc">Size ↑</option>
<option value="price_desc">Price ↓</option><option value="city">City A→Z</option>
</select>
<label class="dens">Density <input type="range" id="dens" min="2" max="6" value="4"></label>
</div>
<div class="count" id="count"></div>
<div class="grid" id="grid"></div>
</div></section>
<section id="brokers"><div class="wrap">
<h2>Broker directory <span class="sub" id="bkNote"></span></h2>
<div class="controls"><input type="search" id="bq" placeholder="Search brokers by name, firm, area…" style="min-width:280px">
<select id="bstate"><option value="">All states</option></select></div>
<div class="bgrid" id="bgrid"></div>
</div></section>
<footer><div class="wrap">CRUnifiedDB · gated access · listings aggregated from public broker/marketplace pages + partner data. Prices, cap rates & terms are broker-stated — verify in diligence.</div></footer>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script>
const $=s=>document.querySelector(s), fmtSF=n=>n?Number(n).toLocaleString()+' SF':'—';
const RENDER_CAP=300; let ALL=[], SPON=[], subOnly=false;
async function boot(){
fetch('/api/me').then(r=>r.json()).then(m=>{if(m.role==='admin')$('#adminLink').style.display='';}).catch(()=>{});
const [st,sp,ls,mp]=await Promise.all([
fetch('/api/stats').then(r=>r.json()),
fetch('/api/sponsors').then(r=>r.json()),
fetch('/api/listings?limit=4000').then(r=>r.json()),
fetch('/api/map-points').then(r=>r.json())
]);
renderStats(st); SPON=sp.sponsors; renderSponsors(SPON); ALL=ls.listings;
fillFilters(); renderGrid(); renderMap(mp.points); loadBrokers('');
}
function renderStats(s){
$('#stats').innerHTML=`
<div><b>${(s.listings||0).toLocaleString()}</b><span>Listings</span></div>
<div><b>${(s.subleases||0).toLocaleString()}</b><span>Subleases</span></div>
<div><b>${(s.brokers||0).toLocaleString()}</b><span>Brokers</span></div>
<div><b>${s.sponsors||0}</b><span>Sponsors</span></div>`;
}
function renderSponsors(list){
$('#sponsorWall').innerHTML=list.map(s=>{
const fin=s.role==='financing_partner';
const risk=s.tos_risk==='high'?`<span class="badge high">ToS: high</span>`:'';
const line=fin?`<span class="fin">Financing partner</span>`:`<span class="cnt">${s.listing_count} listing${s.listing_count==1?'':'s'}</span>`;
return `<a class="sp" href="/broker/${s.slug}"><img src="${s.logo_path}" alt="${s.name}" onerror="this.style.display='none'">
<div>${line}</div><div class="a" style="color:var(--mut);font-size:12px">${s.tagline||''}</div>${risk}</a>`;
}).join('');
}
function fillFilters(){
const types=[...new Set(ALL.map(l=>l.space_type).filter(Boolean))].sort();
$('#fType').innerHTML='<option value="">All space types</option>'+types.map(t=>`<option>${t}</option>`).join('');
const brk=SPON.filter(s=>s.role==='broker');
$('#fSponsor').innerHTML='<option value="">All sponsors</option>'+brk.map(s=>`<option value="${s.slug}">${s.name}</option>`).join('');
['q','fType','fSponsor','sort'].forEach(id=>$('#'+id).addEventListener('input',renderGrid));
$('#subtog').addEventListener('click',()=>{subOnly=!subOnly;$('#subtog').classList.toggle('on',subOnly);renderGrid();});
$('#dens').addEventListener('input',e=>document.documentElement.style.setProperty('--cols',e.target.value));
const sd=localStorage.getItem('sub_dens'); if(sd){$('#dens').value=sd;document.documentElement.style.setProperty('--cols',sd);}
$('#dens').addEventListener('change',e=>localStorage.setItem('sub_dens',e.target.value));
$('#bq').addEventListener('input',()=>loadBrokers());
$('#bstate').addEventListener('change',()=>loadBrokers());
}
function renderGrid(){
const qv=$('#q').value.toLowerCase(),ty=$('#fType').value,sp=$('#fSponsor').value,so=$('#sort').value;
let rows=ALL.filter(l=>{
if(subOnly&&!l.is_sublease)return false;
if(ty&&l.space_type!==ty)return false;
if(sp&&l.sponsor_slug!==sp)return false;
if(qv&&!(`${l.title} ${l.address} ${l.city}`.toLowerCase().includes(qv)))return false;
return true;
});
const S={size_desc:(a,b)=>(b.size_sf||0)-(a.size_sf||0),size_asc:(a,b)=>(a.size_sf||1e9)-(b.size_sf||1e9),
price_desc:(a,b)=>(b.price_amount||0)-(a.price_amount||0),city:(a,b)=>(a.city||'').localeCompare(b.city||''),
featured:(a,b)=>(b.is_sublease-a.is_sublease)||(a.id-b.id)}[so];
rows.sort(S);
const shown=rows.slice(0,RENDER_CAP);
$('#count').textContent=`${rows.length.toLocaleString()} listing${rows.length==1?'':'s'}`+(rows.length>RENDER_CAP?` · showing first ${RENDER_CAP} (refine filters to narrow)`:'');
$('#grid').innerHTML=shown.length?shown.map(cardHTML).join(''):`<div class="empty">No listings match.</div>`;
}
function cardHTML(l){
const price=l.price_amount?`$${Number(l.price_amount).toLocaleString()}${l.listing_kind==='sale'?'':' '+(l.price_unit||'')}`:(l.price_unit||'Call');
const extra=l.listing_kind==='sale'?[l.units?l.units+' units':null,l.cap_rate?l.cap_rate+'% cap':null].filter(Boolean):[];
return `<div class="card">
<div class="top"><span class="kind ${l.listing_kind}">${l.listing_kind}</span>
${l.asset_class?`<span class="chip">${l.asset_class}</span>`:''}</div>
<div class="b"><div class="t">${l.title||'Listing'}</div>
<div class="a">${[l.address,l.city].filter(Boolean).join(', ')||l.city||''}</div>
<div class="meta"><span class="chip">${fmtSF(l.size_sf)}</span><span class="chip">${price}</span>
${extra.map(x=>`<span class="chip">${x}</span>`).join('')}</div></div>
<a class="foot" href="${l.source_url||'#'}" target="_blank" rel="noopener noreferrer">
${l.logo_path?`<img src="${l.logo_path}" onerror="this.style.display='none'">`:''}
<span>${l.sponsor_name||'Market listing'}</span></a>
</div>`;
}
function renderMap(points){
const map=L.map('map').setView([34.05,-118.32],10);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{attribution:'© OpenStreetMap © CARTO',maxZoom:19}).addTo(map);
const pts=points.filter(p=>p.lat&&p.lng);
$('#mapNote').textContent=`${pts.length.toLocaleString()} mapped`;
const cluster=(typeof L.markerClusterGroup==='function')?L.markerClusterGroup({chunkedLoading:true,maxClusterRadius:45}):L.featureGroup();
const bounds=[];
pts.forEach(p=>{
const sub=p.listing_kind==='sublease';
bounds.push([p.lat,p.lng]);
L.circleMarker([p.lat,p.lng],{radius:sub?7:5,color:sub?'#1a5f3c':'#2b6cb0',fillColor:sub?'#1a5f3c':'#2b6cb0',fillOpacity:.7,weight:1})
.bindPopup(`<b>${p.title||'Listing'}</b><br>${[p.address,p.city].filter(Boolean).join(', ')}<br>${p.sponsor_name||'Market'} · ${p.listing_kind}${p.cap_rate?' · '+p.cap_rate+'% cap':''}<br><a href="${p.source_url}" target="_blank" rel="noopener">View →</a>`).addTo(cluster);
});
map.addLayer(cluster); // default view stays LA-centered (national inventory is pannable, not auto-fit)
}
let bkStatesLoaded=false;
async function loadBrokers(){
const qs=$('#bq').value, st=$('#bstate').value;
const d=await fetch(`/api/brokers?limit=48&q=${encodeURIComponent(qs)}&state=${encodeURIComponent(st)}`).then(r=>r.json());
if(!bkStatesLoaded && d.states){bkStatesLoaded=true;$('#bstate').innerHTML='<option value="">All states ('+d.states.length+')</option>'+d.states.map(s=>`<option value="${s}">${s}</option>`).join('');}
$('#bkNote').textContent=`${d.count.toLocaleString()} brokers${st?' in '+st:''}`;
$('#bgrid').innerHTML=d.brokers.length?d.brokers.map(b=>`<div class="bk">
<div class="n"><a href="/brokers/${b.id}" style="color:inherit">${b.name}</a>${b.state?` <span class="chip" style="font-size:11px">${b.state}</span>`:''}</div><div class="f">${b.firm||''}</div>
${b.phone?`<div class="row">☎ ${b.phone}</div>`:''}
${b.email?`<div class="row">✉ <a href="mailto:${b.email}">${b.email}</a></div>`:''}
${b.office_addr?`<div class="row" style="color:var(--mut)">${b.office_addr}</div>`:''}
${b.website?`<div class="row"><a href="${b.website}" target="_blank" rel="noopener">website ↗</a></div>`:''}
${b.profile_url?`<div class="row"><a href="${b.profile_url}" target="_blank" rel="noopener" style="font-weight:600">Contact on Crexi ↗</a></div>`:''}
</div>`).join(''):`<div class="empty">No brokers match.</div>`;
}
boot();
</script>
</body>
</html>