← back to Re Flyer Aggregator
public/flyers-found/index.html
80 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>Property Flyers — found on broker sites · Abrams</title>
<style>
:root{--navy:#0E2350;--gold:#E8A81C;--cols:3} *{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:18px 26px} header h1{margin:0;font-size:22px} header .s{color:var(--gold);font-size:12px;margin-top:3px}
.controls{position:sticky;top:0;z-index:5;background:#fff;border-bottom:1px solid #e6e9f2;padding:12px 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:#6B7280;margin-right:5px}
.stat{color:#6B7280;margin-left:auto;font-variant-numeric:tabular-nums}
.wrap{padding:20px 26px;display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px}
.fly{background:#fff;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.07);padding:16px 18px;display:flex;flex-direction:column;min-height:130px;text-decoration:none;color:inherit;transition:.12s}
.fly:hover{box-shadow:0 6px 20px rgba(14,35,80,.16);transform:translateY(-2px)}
.fly .pdf{display:inline-block;background:#E8342710;color:#c0392b;border:1px solid #f0c9c4;border-radius:5px;font-size:10px;font-weight:800;padding:2px 7px;letter-spacing:.5px;align-self:flex-start}
.fly .prop{font-family:'Playfair Display',Georgia,serif;font-size:16px;color:var(--navy);margin:10px 0 6px;line-height:1.3}
.fly .firm{font-size:12px;color:#6B7280;margin-top:auto} .fly .firm b{color:#0E7C4A;font-weight:600}
.fly .open{color:#16357A;font-size:12px;font-weight:600;margin-top:8px}
@media(max-width:800px){.wrap{grid-template-columns:1fr}}
</style></head><body>
<header><h1>Property Flyers</h1><div class="s">Real broker marketing one-sheets — harvested from CA CRE broker listing pages · Abrams</div></header>
<div class="controls">
<input id="q" placeholder="search property / broker…" size="24">
<div><label>Broker</label><select id="fb"><option value="">All</option></select></div>
<div><label>Sort</label><select id="sort"><option value="property">Property A→Z</option><option value="firm">Broker A→Z</option><option value="found_at">Newest found</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>
let ALL=[];
const esc=s=>(s||'').replace(/[<>&]/g,c=>({'<':'<','>':'>','&':'&'}[c]));
function card(f){return `<a class="fly" href="${f.pdf}" target="_blank" rel="noopener">
<span class="pdf">📄 PDF FLYER</span>
<div class="prop">${esc(f.property)}</div>
<div class="firm">Listed by <b>${esc(f.firm)}</b><br>${esc(f.domain)}</div>
<div class="open">Open flyer ↗</div></a>`;}
function brokers(){return [...new Set(ALL.map(f=>f.firm))].sort();}
function render(){const q=document.getElementById('q').value.toLowerCase(),fb=document.getElementById('fb').value,s=document.getElementById('sort').value;
let a=ALL.filter(f=>(!fb||f.firm===fb)&&(f.property+' '+f.firm+' '+f.domain).toLowerCase().includes(q));
a.sort(s==='found_at'?(x,y)=>(''+(y.found_at||'')).localeCompare(''+(x.found_at||'')):(x,y)=>(''+(x[s]||'')).localeCompare(''+(y[s]||'')));
document.getElementById('grid').innerHTML=a.map(card).join('')||'<div style="color:#9aa;padding:20px">No property flyers yet — the crawl may still be running. Reload shortly.</div>';
document.getElementById('stat').textContent=`${a.length} property flyers · ${new Set(a.map(f=>f.firm)).size} brokers`;
localStorage.pfSort=s;localStorage.pfBroker=fb;}
function dens(){document.documentElement.style.setProperty('--cols',document.getElementById('dens').value);localStorage.pfDens=document.getElementById('dens').value;}
['q','fb','sort'].forEach(id=>document.getElementById(id).oninput=document.getElementById(id).onchange=render);
document.getElementById('dens').oninput=dens;
function load(){fetch('property-flyers.json?t='+Date.now()).then(r=>r.json()).then(j=>{ALL=j;
document.getElementById('fb').innerHTML='<option value="">All</option>'+brokers().map(b=>`<option>${esc(b)}</option>`).join('');
if(localStorage.pfBroker)document.getElementById('fb').value=localStorage.pfBroker;
if(localStorage.pfSort)document.getElementById('sort').value=localStorage.pfSort;
if(localStorage.pfDens)document.getElementById('dens').value=localStorage.pfDens;
dens();render();}).catch(()=>document.getElementById('stat').textContent='no data yet — crawl running');}
load();setInterval(load,15000);
</script></body></html>