← back to Re Flyer Aggregator

public/deals/index.html

99 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>Unique Deals — one-stop 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}
 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:6px 9px;border:1px solid #d6dced;border-radius:6px;font-size:13px} .controls label{color:var(--gray);margin-right:5px}
 .stat{color:var(--gray);margin-left:auto;font-variant-numeric:tabular-nums}
 .rows{padding:10px 26px 60px}
 .r{padding:11px 12px;border-bottom:1px solid #eef0f6}
 .r .top{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
 .r .p{font-weight:800;color:var(--blue);font-size:16px;min-width:74px} .r .chip{font-size:10px;font-weight:800;padding:2px 7px;border-radius:5px;color:#fff}
 .r .d{color:#8A5A00;font-size:12px} .r .mk{color:#445;font-size:12px}
 .r .ttl{font-size:14px;margin:3px 0 4px} .r .ttl a{color:#141414;text-decoration:none} .r .ttl a:hover{color:var(--blue)}
 .r .srcs{font-size:12px;color:var(--gray)} .r .srcs .n{background:#eef0f6;color:#334;border-radius:10px;padding:1px 8px;font-weight:700;font-size:11px}
 .r .srcs a{color:#8A5A00;text-decoration:none;margin-left:8px} .r .srcs a:hover{text-decoration:underline}
 mark{background:#fff2ab}
</style></head><body>
<header><div><h1>Unique Deals</h1><div class="s">ABRAMS · one-stop CRE — same deal across outlets, merged</div></div>
 <div><a href="/search">🔍 Search all</a><a href="/">📊 Sources</a><a href="/live.html">◂ Live</a></div></header>
<div class="controls">
 <input id="q" placeholder="search deals…" size="22">
 <div><label>Type</label><select id="ft"><option value="">All</option><option>Sale</option><option>Listing</option><option>Lease</option><option>Loan</option><option>Development</option></select></div>
 <div><label>Sort</label><select id="sort"><option value="price">Biggest $</option><option value="date">Newest</option><option value="src">Most-reported</option></select></div>
 <div class="stat" id="stat">loading…</div>
</div>
<div class="rows" id="rows"></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'});};
// cross-source cluster key: price + the first Proper-Noun phrase in the title (the entity)
const entity=t=>{const m=(t||'').replace(/^(exclusive|news|report|podcast)\s*[:|-]\s*/i,'').match(/[A-Z][\w.&'’-]+(?:\s+[A-Z][\w.&'’-]+){0,2}/);return m?m[0].toLowerCase():'';};
const priceNum=x=>x.price&&typeof x.price==='number'?x.price:(x.price_label||x.price||'').toString().match(/[\d.]+/)?parseFloat((x.price_label||x.price).toString().replace(/[^\d.]/g,''))*((x.price_label||x.price).toString().match(/b/i)?1e9:(x.price_label||x.price).toString().match(/k/i)?1e3:1e6):0;
let CLUS=[],Q='';
const pbucket=n=>{n=+n||0;if(!n)return '0';const s=Math.round(n/Math.pow(10,Math.floor(Math.log10(n))-1))*Math.pow(10,Math.floor(Math.log10(n))-1);return ''+s;};   // 2-sig-fig so $1.2B label variants collide
function cluster(items){
 const map=new Map();
 for(const x of items){ const pl=x.price_label||(typeof x.price==='string'?x.price:money(x.price)); const ent=entity(x.title); if(!ent)continue; const key=pbucket(priceNum(x))+'|'+ent+'|'+(x.type||'');
  if(!map.has(key)){map.set(key,{...x,price_label:pl,sources:[],links:[]});}
  const g=map.get(key); if(!g.sources.includes(x.source)){g.sources.push(x.source);if(x.link)g.links.push({s:x.source,l:x.link});}
  if((Date.parse(x.date)||0)>(Date.parse(g.date)||0)){g.date=x.date;g.title=x.title;g.link=x.link||g.link;} }
 return [...map.values()];
}
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 render(){
 const ft=document.getElementById('ft').value,sort=document.getElementById('sort').value;
 let a=CLUS.filter(x=>(!ft||x.type===ft)&&(!Q||(x.title+' '+x.sources.join(' ')+' '+(x.market||'')).toLowerCase().includes(Q)));
 a.sort(sort==='date'?(x,y)=>(Date.parse(y.date)||0)-(Date.parse(x.date)||0):sort==='src'?(x,y)=>y.sources.length-x.sources.length||priceNum(y)-priceNum(x):(x,y)=>priceNum(y)-priceNum(x));
 document.getElementById('stat').textContent=`${a.length.toLocaleString()} unique deals${ft?' · '+ft:''}${Q?` · “${Q}”`:''}`;
 document.getElementById('rows').innerHTML=a.slice(0,700).map(x=>{const c=TC[x.type]||'#6B7280';
  const srcs=x.sources.length>1?`<span class="n">${x.sources.length} sources</span> ${x.links.slice(0,6).map(o=>`<a href="${o.l}" target="_blank" rel="noopener">${esc(o.s)}↗</a>`).join('')}`:`<a href="${x.link||'#'}" target="_blank" rel="noopener">${esc(x.sources[0]||'')}↗</a>`;
  return `<div class="r"><div class="top"><span class="p">${x.price_label||money(x.price)||'—'}</span><span class="chip" style="background:${c}">${(x.type||'CRE').toUpperCase()}</span><span class="d">${dt(x.date)}</span><span class="mk">📍 ${esc(x.market||'')}</span></div>
   <div class="ttl">${x.link?`<a href="${x.link}" target="_blank" rel="noopener">${hl(x.title||'')}</a>`:hl(x.title||'')}</div>
   <div class="srcs">${srcs}</div></div>`;}).join('')||'<div style="padding:24px;color:#9aa">No deals.</div>';
}
let deb;document.getElementById('q').oninput=e=>{clearTimeout(deb);deb=setTimeout(()=>{Q=e.target.value.trim().toLowerCase();render();},120);};
['ft','sort'].forEach(id=>document.getElementById(id).onchange=render);
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(()=>[])]);
 // only real transactions (has a $ or a deal type) — cluster those
 const items=[...deals,...arch].filter(x=>(x.price||x.price_label)&&['Sale','Listing','Lease','Loan','Development'].includes(x.type));
 CLUS=cluster(items); render();
}
load();
</script></body></html>