← back to Re Flyer Aggregator

public/flyers/index.html

98 lines

<!doctype html><html lang="en"><head><meta charset="utf-8">
<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 name="viewport" content="width=device-width,initial-scale=1">
<title>Flyers — Abrams | CRE deal flyers</title>
<style>
 :root{--cols:3;--navy:#0E2350;--blue:#16357A;--gold:#E8A81C;--ink:#141414;--gray:#6B7280}
 *{box-sizing:border-box} body{margin:0;font-family:Inter,-apple-system,Helvetica,Arial,sans-serif;background:#f4f6fb;color:var(--ink)}
 header{background:var(--navy);color:#fff;padding:22px 34px} header h1{margin:0;font-size:24px;letter-spacing:.5px}
 header .s{color:var(--gold);font-size:13px;margin-top:4px}
 .controls{position:sticky;top:0;z-index:5;background:#fff;border-bottom:1px solid #e6e9f2;padding:12px 34px;display:flex;gap:16px;align-items:center;flex-wrap:wrap;font-size:13px}
 .controls label{color:var(--gray);margin-right:5px} select,input[type=range]{vertical-align:middle}
 .stat{color:var(--gray);margin-left:auto}
 .wrap{padding:22px 34px;display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px}
 .flyer{background:#fff;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,.07);padding:18px 20px;display:flex;flex-direction:column;min-height:190px}
 .flyer .top{display:flex;justify-content:space-between;align-items:flex-start}
 .chip{color:#fff;font-size:10px;font-weight:700;padding:3px 9px;border-radius:5px;letter-spacing:.5px}
 .price{font-size:30px;font-weight:800;color:var(--blue);margin:8px 0 2px;letter-spacing:-.5px}
 .ttl{font-family:'Playfair Display',Georgia,serif;font-size:16px;line-height:1.25;color:var(--navy);margin:2px 0 8px}
 .meta{font-size:12px;color:var(--gray);line-height:1.7;margin-top:auto}
 .meta b{color:var(--ink);font-weight:600}
 .src{margin-top:10px;font-size:12px} .src a{color:#8A5A00;text-decoration:none;font-weight:600}
 .mk{display:inline-block;background:#eef0f6;color:#445;border-radius:4px;padding:1px 7px;font-size:11px}
 @media(max-width:700px){.wrap{grid-template-columns:1fr}}
</style></head><body>
<header><h1>Flyers · Abrams</h1><div class="s">Commercial real-estate deal flyers — one place · updated from 19 CRE news sources</div></header>
<div class="controls">
 <div><label>Type</label><select id="ftype"><option value="">All</option><option>Sale</option><option>Listing</option><option>Lease</option><option>Loan</option><option>Development</option><option>News</option></select></div>
 <div><label>Market</label><select id="fmk"><option value="">All</option></select></div>
 <div><label>Show</label><select id="recency"><option value="0">All time</option><option value="7">Past 7 days</option><option value="30">Past 30 days</option><option value="90">Past 90 days</option></select></div>
 <div><label>Sort</label><select id="sort"><option value="price">Price ↓</option><option value="date">Newest</option><option value="type">Type</option></select></div>
 <div><label>Density</label><input type="range" id="dens" min="1" max="5" step="1" value="3"></div>
 <div class="stat" id="stat"></div>
</div>
<div class="wrap" id="grid"></div>
<script>
const TC={"Sale":"#0E7C4A","Loan":"#8A5A00","Lease":"#16357A","Listing":"#0ea5e9","Development":"#6d28d9","News":"#64748b","Filing":"#6B7280","Post":"#E8A81C"};
const money=n=>n?'$'+(+n).toLocaleString('en-US'):'';
const tc=s=>(s||'').toLowerCase().replace(/\b\w/g,c=>c.toUpperCase());
let DEALS=[];
function card(d){const c=TC[d.type]||'#6B7280';
 return `<div class="flyer">
  <div class="top"><span class="chip" style="background:${c}">${(d.type||'CRE').toUpperCase()}</span><span class="mk">${d.market||'US'}</span></div>
  <div class="price">${d.price_label||money(d.price)||'—'}</div>
  <div class="ttl">${(d.title||'').replace(/</g,'&lt;')}</div>
  <div class="meta">
   ${d.buyer?'<div><b>Buyer:</b> '+d.buyer+'</div>':''}
   ${d.seller?'<div><b>Seller:</b> '+d.seller+'</div>':''}
   ${d.date?'<div><b>Reported:</b> '+new Date(d.date).toLocaleDateString()+'</div>':''}
  </div>
  <div class="src"><a href="${d.link}" target="_blank" rel="noopener noreferrer">${d.source} ↗</a></div>
 </div>`;}
function markets(){return [...new Set(DEALS.map(d=>d.market).filter(Boolean))].sort();}
function render(){let a=[...DEALS];
 const ft=ftype.value,fk=fmk.value;
 if(ft)a=a.filter(d=>d.type===ft); if(fk)a=a.filter(d=>d.market===fk);
 const rc=+recency.value; if(rc){const cut=Date.now()-rc*864e5;a=a.filter(d=>{const t=Date.parse(d.date||'');return !isNaN(t)&&t>=cut;});}
 const s=sort.value;
 a.sort(s==='date'?(x,y)=>(y.date||'').localeCompare(x.date||''):s==='type'?(x,y)=>(x.type||'').localeCompare(y.type||''):(x,y)=>(y.price||0)-(x.price||0));
 grid.innerHTML=a.map(card).join(''); stat.textContent=a.length+' flyers'+(rc?' · past '+rc+'d':'');
 localStorage.flyType=ft;localStorage.flyMk=fk;localStorage.flySort=s;localStorage.flyRecency=recency.value;}
function dens(){document.documentElement.style.setProperty('--cols',document.getElementById('dens').value);localStorage.flyDens=document.getElementById('dens').value;}
['ftype','fmk','sort','recency'].forEach(id=>document.getElementById(id).onchange=render);
document.getElementById('dens').oninput=dens;
fetch('flyers.json').then(r=>r.json()).then(j=>{DEALS=j;
 fmk.innerHTML='<option value="">All</option>'+markets().map(m=>'<option>'+m+'</option>').join('');
 if(localStorage.flyType)ftype.value=localStorage.flyType;
 if(localStorage.flyMk)fmk.value=localStorage.flyMk;
 if(localStorage.flyRecencyV!=='2'){delete localStorage.flyRecency;localStorage.flyRecencyV='2';}  // one-time reset: old 90d default -> All time
 sort.value=localStorage.flySort||'date';          // default: Newest first (freshest deals on top)
 recency.value=localStorage.flyRecency||'0';        // default: All time (so full inventory shows, not just the ~47 fresh)
 if(localStorage.flyDens)document.getElementById('dens').value=localStorage.flyDens;
 dens();render();}).catch(e=>{
 grid.innerHTML='<div style="grid-column:1/-1;padding:40px;text-align:center;color:#b91c1c;font-size:14px">Could not load flyers ('+(e&&e.message||e)+'). Please reload.</div>';
 stat.textContent='load failed';});
</script></body></html>