← back to Re Flyer Aggregator

public/index.html

98 lines

<!doctype html><html lang="en"><head>
<script src="/drill.js"></script>
<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>Deal marketing assets — RE flyer aggregator</title>
<style>
 :root{--cols:2}
 body{font-family:Inter,Helvetica,Arial,sans-serif;margin:0;background:#f4f6fb;color:#141414}
 header{background:#0E2350;color:#fff;padding:18px 28px}
 header h1{margin:0;font-size:20px} header .s{color:#E8A81C;font-size:12px;margin-top:3px}
 .controls{display:flex;gap:18px;align-items:center;flex-wrap:wrap;padding:14px 28px;background:#fff;border-bottom:1px solid #e6e9f2;position:sticky;top:0;z-index:5}
 .controls label{font-size:12px;color:#6B7280;margin-right:6px}
 select,input[type=range]{vertical-align:middle}
 .wrap{padding:20px 28px;display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px}
 .card{background:#fff;border-radius:10px;padding:16px 18px;box-shadow:0 2px 10px rgba(0,0,0,.06)}
 .price{float:right;font-weight:800;color:#0E7C4A;font-size:18px}
 .card h2{margin:0 0 2px;font-size:17px;color:#16357A}
 .sub{color:#6B7280;font-size:12px;margin-bottom:10px}
 ul{list-style:none;padding:0;margin:0} li{padding:5px 0;border-top:1px solid #eef0f6;font-size:13px}
 a{color:#16357A;text-decoration:none} a:hover{text-decoration:underline}
 .b{color:#fff;font-size:9px;font-weight:700;padding:2px 6px;border-radius:4px;letter-spacing:.4px;margin-right:5px}
 .meta{color:#6B7280;font-size:11px}
</style></head><body>
<header><h1>Deal marketing assets — aggregated (usre · deal_assets_v)</h1>
<div class="s">Read-only consumer surface · TK-10708 · GATED marketplace assets excluded</div></header>
<input type="hidden" id="q" value=""><input type="hidden" id="pattern" value="">
<div class="controls">
  <div><label>Sort</label><select id="sort">
    <option value="price">Price ↓</option><option value="date">Newest</option>
    <option value="assets">Most assets</option><option value="conf">Best match</option>
    <option value="city">City A→Z</option>
  </select></div>
  <div><label>Density</label><input type="range" id="dens" min="1" max="4" step="1" value="2"></div>
  <div class="meta" id="stat"></div>
</div>
<div class="wrap" id="grid"></div>
<script>
// href-drill primitives (TK-10903)
var FIELDS=[['city','City'],['ctype','Type'],['county_name','County']];
var FIL={city:'',ctype:'',county_name:''};
var $=function(s){return document.querySelector(s);};
function esc(s){return String(s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
function applyFilters(push){if(push)writeURL(true);render();}

const BADGE={self_generated:['#16357A','RECAP'],broker_owned:['#0E7C4A','BROKER'],first_party:['#8A5A00','FIRST-PARTY']};
const money=n=>'$'+(+n).toLocaleString('en-US');
const tc=s=>(s||'').toLowerCase().replace(/\b\w/g,c=>c.toUpperCase());
let DEALS=[];
function assetLi(a){const[bg,lbl]=BADGE[a.rights_basis]||['#6B7280',(a.rights_basis||'').toUpperCase()];
  const href=a.source_landing_url||(a.local_path?a.local_path.replace(/^out\//,'out/'):'#');
  return `<li><span class="b" style="background:${bg}">${lbl}</span>
   <a rel="noopener noreferrer" href="${href}" target="_blank">${a.title||a.source_name}</a>
   <span class="meta">· ${a.asset_type}${a.match_confidence!=null?' · '+a.match_confidence:''}</span></li>`;}
function card(d){return `<div class="card"><span class="price">${money(d.sale_price)}</span>
  <h2>${tc(d.address)}</h2>
  <div class="sub">${drill('city',d.city,tc(d.city))}${d.county_name?', '+drill('county_name',d.county_name,tc(d.county_name)+' County'):''}${d.ctype?' · '+drill('ctype',d.ctype,tc(d.ctype)):''} · sold ${d.sale_date} · doc ${d.doc_number}</div>
  <ul>${(d.assets||[]).map(assetLi).join('')}</ul></div>`;}
function sortKey(m){return{
  price:(a,b)=>b.sale_price-a.sale_price,
  date:(a,b)=>(b.sale_date||'').localeCompare(a.sale_date||''),
  assets:(a,b)=>b.assets.length-a.assets.length,
  conf:(a,b)=>Math.max(0,...b.assets.map(x=>+x.match_confidence||0))-Math.max(0,...a.assets.map(x=>+x.match_confidence||0)),
  city:(a,b)=>(a.city||'').localeCompare(b.city||'')}[m];}
function render(){const m=sort.value;localStorage.reflyersSort=m;
  document.getElementById('grid').innerHTML=[...DEALS].sort(sortKey(m)).map(card).join('');
  document.getElementById('stat').textContent=DEALS.length+' deals · '+DEALS.reduce((n,d)=>n+d.assets.length,0)+' assets';}
function dens(){document.documentElement.style.setProperty('--cols',document.getElementById('dens').value);localStorage.reflyersDens=document.getElementById('dens').value;}
sort.onchange=render; document.getElementById('dens').oninput=dens;
if(localStorage.reflyersSort)sort.value=localStorage.reflyersSort;
if(localStorage.reflyersDens)document.getElementById('dens').value=localStorage.reflyersDens;
dens();
readURL();
fetch('/api/deal-assets').then(r=>r.json()).then(j=>{DEALS=j.deals||[];render();});
</script></body></html>