← back to Re Flyer Aggregator

public/properties/index.html

86 lines

<!doctype html><html lang="en"><head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Properties — per-property CRE data · Abrams</title>
<style>
 :root{--navy:#0E2350;--blue:#16357A;--gold:#E8A81C;--gray:#6B7280} *{box-sizing:border-box}
 body{margin:0;font-family:Inter,-apple-system,Helvetica,Arial,sans-serif;background:#f4f6fb;color:#141414}
 header{background:var(--navy);color:#fff;padding:16px 26px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}
 header h1{margin:0;font-size:22px} header .s{color:var(--gold);font-size:12px;margin-top:3px} header a{color:#7fd7ff;font-size:13px;text-decoration:none;margin-left:14px}
 .controls{position:sticky;top:0;z-index:5;background:#fff;border-bottom:1px solid #e6e9f2;padding:11px 26px;display:flex;gap:14px;align-items:center;flex-wrap:wrap;font-size:13px}
 .controls input,.controls select{padding:7px 11px;border:1px solid #d6dced;border-radius:8px;font-size:14px} .controls #q{min-width:320px} .controls label{color:var(--gray);margin-right:5px}
 .stat{color:var(--gray);margin-left:auto;font-variant-numeric:tabular-nums}
 .rows{padding:8px 26px 60px}
 .p{border-bottom:1px solid #eef0f6;padding:11px 12px;cursor:pointer}
 .p:hover{background:#fbfcff}
 .p .top{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
 .p .addr{font-size:15px;font-weight:700;color:var(--navy)} .p .loc{color:#445;font-size:12px}
 .p .chip{font-size:10px;font-weight:800;padding:2px 7px;border-radius:5px;color:#fff;background:#6d28d9} .p .pr{font-weight:800;color:var(--blue)}
 .p .mn{background:#eef0f6;color:#334;border-radius:10px;padding:1px 9px;font-size:11px;font-weight:700}
 .p .sub{color:var(--gray);font-size:12px;margin-top:3px}
 .hist{margin:8px 0 2px 8px;padding-left:12px;border-left:2px solid #e6e9f2}
 .hist .e{padding:5px 0;font-size:12px;border-top:1px solid #f4f6fb} .hist .e:first-child{border-top:none}
 .hist .e .d{color:#8A5A00} .hist .e b{color:#0E7C4A} .hist .e a{color:#16357A;text-decoration:none} .hist .e .src{color:#9aa}
 mark{background:#fff2ab}
</style></head><body>
<header><div><h1>Properties</h1><div class="s">ABRAMS · per-property CRE data — every property we've seen, with its full history</div></div>
 <div><a href="/search">🔍 Search</a><a href="/deals">Deals</a><a href="/">📊 Sources</a></div></header>
<div class="controls">
 <input id="q" placeholder="Search by address, city, buyer, seller…" autofocus autocomplete="off">
 <div><label>Type</label><select id="pt"><option value="">All</option><option>Office</option><option>Industrial</option><option>Retail</option><option>Multifamily</option><option>Hotel</option><option>Land</option></select></div>
 <div><label>County</label><select id="county"><option value="">All</option><option>Los Angeles County</option><option>Miami-Dade County</option><option>Wake County</option><option>Clackamas County</option></select></div>
 <div><label>Sort</label><select id="sort"><option value="mentions">Most-tracked</option><option value="date">Newest</option><option value="price">Biggest $</option><option value="assessed">Assessed value</option></select></div>
 <div class="stat" id="stat">loading…</div>
</div>
<div class="rows" id="rows"></div>
<script>
const esc=s=>(s||'').replace(/[<>&]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]));
const dt=s=>{const d=new Date(s);return isNaN(d)?'':d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'2-digit'});};
const pn=l=>{if(!l)return 0;const m=(''+l).match(/[\d.]+/);if(!m)return 0;return parseFloat(m[0])*(/b/i.test(l)?1e9:/k/i.test(l)?1e3:1e6);};
// compact $ for the raw assessed_total on no-sale public-record parcels (they carry no transaction price)
const am=n=>{n=+n;if(!n||n<=0)return '';return n>=1e9?'$'+(n/1e9).toFixed(1)+'B':n>=1e6?'$'+Math.round(n/1e6)+'M':'$'+Math.round(n/1e3)+'K';};
let DATA=[],Q='',EXP=new Set();
function hl(t){if(!Q)return esc(t);const i=t.toLowerCase().indexOf(Q);return i<0?esc(t):esc(t.slice(0,i))+'<mark>'+esc(t.slice(i,i+Q.length))+'</mark>'+esc(t.slice(i+Q.length));}
function match(p){ if(document.getElementById('pt').value&&p.property_type!==document.getElementById('pt').value)return false;
 if(document.getElementById('county').value&&p.county!==document.getElementById('county').value)return false;
 if(!Q)return true; return (p.address+' '+(p.city||'')+' '+(p.state||'')+' '+(p.property_type||'')+' '+p.entries.map(e=>e.title+' '+(e.buyer||'')+' '+(e.seller||'')).join(' ')).toLowerCase().includes(Q); }
function hist(p){return `<div class="hist">${p.entries.map(e=>`<div class="e"><span class="d">${dt(e.date)}</span> ${e.price_label?'<b>'+esc(e.price_label)+'</b> ':''}${esc(e.txn||'')} · ${e.link?`<a href="${e.link}" target="_blank" rel="noopener">${hl(e.title||'')}</a>`:hl(e.title||'')} <span class="src">[${esc(e.source)}]</span>${e.buyer?' · buyer '+esc(e.buyer):''}${e.seller?' · seller '+esc(e.seller):''}</div>`).join('')}</div>`;}
function render(){
 const sort=document.getElementById('sort').value;
 let a=DATA.filter(match);
 a.sort(sort==='date'?(x,y)=>(Date.parse(y.latest)||0)-(Date.parse(x.latest)||0):sort==='price'?(x,y)=>pn(y.top_price)-pn(x.top_price):sort==='assessed'?(x,y)=>(+y.assessed_total||0)-(+x.assessed_total||0):(x,y)=>y.mentions-x.mentions||(Date.parse(y.latest)||0)-(Date.parse(x.latest)||0));
 document.getElementById('stat').textContent=`${a.length.toLocaleString()} properties${Q?` · “${Q}”`:''}`;
 document.getElementById('rows').innerHTML=a.slice(0,600).map((p,i)=>{const k=p.address+'|'+(p.city||'');const open=EXP.has(k);
  return `<div class="p" data-k="${esc(k)}"><div class="top"><span class="addr">${open?'▾ ':'▸ '}${hl(p.address)}</span><span class="chip">${(p.property_type||'CRE').toUpperCase()}</span><span class="pr">${p.top_price||p.latest_price||(p.assessed_total?'<span style="color:#9aa;font-weight:500">assessed '+am(p.assessed_total)+'</span>':'')}</span><span class="loc">📍 ${esc([p.city,p.state].filter(Boolean).join(', '))}</span>${p.units?`<span class="mn" style="background:#e0f2fe;color:#075985">${p.units} units</span>`:''}${p.sqft?`<span class="mn" style="background:#e0f2fe;color:#075985">${(+p.sqft).toLocaleString()} sf</span>`:''}${p.occupancy_pct?`<span class="mn" style="background:#dcfce7;color:#166534">${p.occupancy_pct}% occ</span>`:''}<span class="mn">${p.mentions}× · ${p.source_count} src</span></div>
   <div class="sub">${p.entries&&p.entries.length?`latest: ${esc(p.latest_txn||'')} ${dt(p.latest)}`:'public-record parcel · no recorded sale'}</div>${open?hist(p):''}</div>`;}).join('')||'<div style="padding:24px;color:#9aa">No properties.</div>';
}
document.getElementById('rows').addEventListener('click',e=>{ if(e.target.closest('a'))return; const row=e.target.closest('.p'); if(!row)return; const k=row.dataset.k; EXP.has(k)?EXP.delete(k):EXP.add(k); render(); });
let deb;document.getElementById('q').oninput=e=>{clearTimeout(deb);deb=setTimeout(()=>{Q=e.target.value.trim().toLowerCase();render();},120);};
['pt','county','sort'].forEach(id=>document.getElementById(id).onchange=render);
fetch('/properties/property-index.json?t='+Date.now()).then(r=>r.json()).then(j=>{DATA=j;render();}).catch(()=>document.getElementById('stat').textContent='load error');
</script></body></html>