← back to Re Flyer Aggregator

public/search/index.html

111 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>Search · All Listings — CRE · 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}
 /* left nav */
 #side{position:fixed;top:0;left:0;bottom:0;width:210px;background:var(--navy);color:#cfe0ff;overflow-y:auto;z-index:20}
 #side h1{font-size:15px;color:#fff;margin:0;padding:18px 16px 4px;letter-spacing:.5px} #side .sub{color:var(--gold);font-size:10px;padding:0 16px 12px;letter-spacing:1px}
 #side .nav a{display:flex;justify-content:space-between;align-items:center;padding:9px 16px;color:#bcd0f5;text-decoration:none;font-size:13px;cursor:pointer;border-left:3px solid transparent}
 #side .nav a:hover{background:#16264a} #side .nav a.on{background:#1b2f5c;color:#fff;border-left-color:var(--gold);font-weight:600}
 #side .nav a .c{background:#0e2a4a;color:#8fb4ea;border-radius:10px;padding:1px 8px;font-size:11px}
 #side .grp{font-size:10px;color:#6f86b8;text-transform:uppercase;letter-spacing:1px;padding:14px 16px 4px}
 .main{margin-left:210px}
 header{background:#fff;border-bottom:1px solid #e6e9f2;padding:14px 26px;position:sticky;top:0;z-index:10}
 .searchbar{display:flex;align-items:center;gap:10px;background:#f4f6fb;border:2px solid #dbe2f2;border-radius:26px;padding:10px 18px;transition:.15s}
 .searchbar:focus-within{border-color:var(--blue);background:#fff;box-shadow:0 2px 12px rgba(22,53,122,.12)}
 .searchbar input{flex:1;border:none;background:transparent;font-size:16px;outline:none;color:#141414}
 .searchbar .ic{font-size:18px;color:var(--gray)} .searchbar .clr{cursor:pointer;color:var(--gray);font-size:13px}
 .stat{padding:8px 26px 0;color:var(--gray);font-size:12px;font-variant-numeric:tabular-nums}
 .rows{padding:8px 26px 60px}
 .r{display:grid;grid-template-columns:96px 90px 78px 1fr 150px 130px;gap:12px;align-items:center;padding:9px 10px;border-bottom:1px solid #eef0f6;font-size:13px}
 .r:hover{background:#fbfcff} .r .d{color:#8A5A00;font-size:12px;white-space:nowrap} .r .p{font-weight:800;color:var(--blue)}
 .r .chip{font-size:10px;font-weight:800;padding:2px 7px;border-radius:5px;color:#fff;text-align:center}
 .r .t a{color:#141414;text-decoration:none} .r .t a:hover{color:var(--blue)} .r .mk{color:#445;font-size:12px} .r .src{color:#8A5A00;font-size:12px}
 mark{background:#fff2ab;padding:0 1px}
 @media(max-width:820px){#side{width:56px} #side h1,#side .sub,#side .grp,#side .nav a .lbl{display:none} .main{margin-left:56px} .r{grid-template-columns:80px 1fr}}
</style>
<link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head><body>
<div id="side">
 <h1>CRE Search</h1><div class="sub">ABRAMS · ALL LISTINGS</div>
 <div class="grp">Views</div>
 <div class="nav" id="nav"></div>
 <div class="grp">Sources</div>
 <div class="nav" id="srcnav"></div>
</div>
<div class="main">
 <header><div class="searchbar"><span class="ic">🔍</span><input id="q" placeholder="Search all CRE listings & articles — address, buyer, market, source…" autofocus autocomplete="off"><span class="clr" id="clr">clear</span></div></header>
 <div class="stat" id="stat">loading…</div>
 <div class="rows" id="rows" data-nav-grid data-nav-title="Listings"></div>
</div>
<script>
const TC={Sale:'#0E7C4A',Listing:'#0ea5e9',Lease:'#16357A',Loan:'#8A5A00',Development:'#6d28d9',News:'#64748b',Post:'#E8A81C',Filing:'#6B7280',CRE:'#6B7280'};
const money=n=>!n?'':n>=1e9?'$'+(n/1e9).toFixed(1)+'B':n>=1e6?'$'+Math.round(n/1e6)+'M':'$'+Math.round(n/1e3)+'K';
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'});};
let DATA=[],VIEW='all',SRC='',Q='';const CAP=800;
const VIEWS=[['all','📋 All Listings'],['Sale','Sales'],['Listing','New Listings'],['Lease','Leases'],['Loan','Loans/Finance'],['Development','Development'],['News','News']];
function hl(t,q){ if(!q)return esc(t); const i=t.toLowerCase().indexOf(q); if(i<0)return esc(t); return esc(t.slice(0,i))+'<mark>'+esc(t.slice(i,i+q.length))+'</mark>'+esc(t.slice(i+q.length)); }
function match(x){ if(VIEW!=='all'&&x.type!==VIEW)return false; if(SRC&&x.source!==SRC)return false;
 if(!Q)return true; return [x.title,x.source,x.market,x.type,x.price_label,x.buyer,x.seller,dt(x.date)].join(' ').toLowerCase().includes(Q); }
function render(){
 const res=DATA.filter(match);
 document.getElementById('stat').textContent=`${res.length.toLocaleString()} of ${DATA.length.toLocaleString()} records${Q?` · “${Q}”`:''}${VIEW!=='all'?' · '+VIEW:''}${SRC?' · '+SRC:''}${res.length>CAP?` · showing first ${CAP}`:''}`;
 const rows=document.getElementById('rows');
 rows.innerHTML=res.slice(0,CAP).map(x=>{const c=TC[x.type]||'#6B7280';const t=x.link?`<a href="${x.link}" target="_blank" rel="noopener">${hl(x.title||'',Q)}</a>`:hl(x.title||'',Q);
  return `<div class="r"><span class="d" data-field="Date">${dt(x.date)}</span><span class="p" data-field="Price">${x.price_label||money(x.price)||''}</span><span class="chip" data-field="Type" style="background:${c}">${(x.type||'CRE').toUpperCase()}</span><span class="t" data-field="Title">${t}</span><span class="mk" data-field="Market">${esc(x.market||'')}</span><span class="src" data-field="Source">${esc(x.source||'')}</span></div>`;}).join('')||'<div style="padding:24px;color:#9aa">No matches.</div>';
}
function counts(){const c={all:DATA.length};VIEWS.forEach(([k])=>{if(k!=='all')c[k]=DATA.filter(x=>x.type===k).length;});return c;}
function buildNav(){const c=counts();
 document.getElementById('nav').innerHTML=VIEWS.map(([k,lbl])=>`<a data-v="${k}" class="${VIEW===k?'on':''}"><span class="lbl">${lbl}</span><span class="c">${(c[k]||0).toLocaleString()}</span></a>`).join('');
 const bySrc={};DATA.forEach(x=>bySrc[x.source]=(bySrc[x.source]||0)+1);
 const top=Object.entries(bySrc).sort((a,b)=>b[1]-a[1]).slice(0,18);
 document.getElementById('srcnav').innerHTML=`<a data-s="" class="${SRC===''?'on':''}"><span class="lbl">All sources</span></a>`+top.map(([s,n])=>`<a data-s="${esc(s)}" class="${SRC===s?'on':''}"><span class="lbl">${esc(s)}</span><span class="c">${n}</span></a>`).join('');
 document.querySelectorAll('#nav a').forEach(a=>a.onclick=()=>{VIEW=a.dataset.v;buildNav();render();});
 document.querySelectorAll('#srcnav a').forEach(a=>a.onclick=()=>{SRC=a.dataset.s;buildNav();render();});
}
let deb;document.getElementById('q').oninput=e=>{clearTimeout(deb);deb=setTimeout(()=>{Q=e.target.value.trim().toLowerCase();render();},120);};
document.getElementById('clr').onclick=()=>{document.getElementById('q').value='';Q='';render();document.getElementById('q').focus();};
// load deals (flyers.json) + article archive if present, merge + dedup
async function load(){
 const [deals,arch]=await Promise.all([
   fetch('/flyers.json?t='+Date.now()).then(r=>r.json()).catch(()=>[]),
   fetch('/article-archive.json?t='+Date.now()).then(r=>r.ok?r.json():[]).catch(()=>[])
 ]);
 const seen=new Set(),out=[];
 for(const d of [...deals,...arch]){ const k=(d.title||'').toLowerCase().slice(0,50); if(k&&!seen.has(k)){seen.add(k);out.push(d);} }
 out.sort((a,b)=>(Date.parse(b.date)||0)-(Date.parse(a.date)||0));
 DATA=out; buildNav(); render();
}
load();
</script>
<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body></html>