← 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>