← back to Nationalrealestate
public/watchlist.html
168 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230a0d13'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%23c8a24b'%3EUS%3C/text%3E%3C/svg%3E">
<title>USRealEstate — Watchlist</title>
<style>
:root{--bg:#0a0d13;--panel:#11151d;--line:#222936;--fg:#e8ecf2;--muted:#8a93a3;--gold:#c8a24b;--red:#f85149;--green:#3fb950;}
*{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;}
#topbar{position:sticky;top:0;z-index:10;display:flex;align-items:center;gap:18px;padding:10px 18px;
background:rgba(17,21,29,.94);border-bottom:1px solid var(--line);backdrop-filter:blur(6px);}
#topbar .brand{font-size:16px;font-weight:700;} #topbar .brand b{color:var(--gold);}
nav{display:flex;gap:14px;font-size:13px;} nav a{color:var(--muted);text-decoration:none;}
nav a:hover,nav a.active{color:var(--gold);}
main{max-width:1100px;margin:0 auto;padding:22px 18px;}
h2{font-size:15px;color:var(--gold);margin:22px 0 10px;}
.addrow{display:flex;gap:8px;flex-wrap:wrap;align-items:center;background:var(--panel);
border:1px solid var(--line);border-radius:12px;padding:12px 14px;}
.addrow input,.addrow select{background:var(--bg);border:1px solid var(--line);color:var(--fg);
border-radius:8px;padding:7px 10px;font:13px -apple-system,Segoe UI,Roboto,sans-serif;}
.addrow input#q{flex:1;min-width:220px;}
.addrow button{background:var(--gold);color:var(--bg);border:0;border-radius:8px;padding:8px 16px;font-weight:700;cursor:pointer;}
#sugg{position:relative;} #sugglist{position:absolute;top:100%;left:0;right:0;z-index:20;background:var(--panel);
border:1px solid var(--line);border-radius:0 0 10px 10px;max-height:260px;overflow:auto;display:none;}
#sugglist div{padding:7px 12px;cursor:pointer;font-size:13px;} #sugglist div:hover{background:var(--bg);color:var(--gold);}
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin:10px 0;
display:flex;gap:16px;align-items:center;flex-wrap:wrap;}
.card .nm{font-weight:700;min-width:220px;} .card .nm a{color:var(--fg);text-decoration:none;} .card .nm a:hover{color:var(--gold);}
.card .stat{text-align:center;} .card .stat .v{font-weight:700;color:var(--gold);font-variant-numeric:tabular-nums;}
.card .stat .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;}
.card .thr{font-size:12px;color:var(--muted);}
.card .del{margin-left:auto;background:none;border:1px solid var(--line);color:var(--red);border-radius:8px;padding:5px 12px;cursor:pointer;}
.fired{border-color:var(--gold);}
.alert-row{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--gold);border-radius:10px;
padding:10px 14px;margin:6px 0;font-size:13px;display:flex;gap:12px;align-items:center;}
.alert-row .when{color:var(--muted);font-size:11px;margin-left:auto;white-space:nowrap;}
.empty{color:var(--muted);padding:18px;text-align:center;}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
<div class="brand"><b>USRealEstate</b> — Watchlist</div>
<nav>
<a href="/">Map</a>
<a href="/markets.html">Markets</a><a href="/commercial.html">Commercial</a>
<a href="/compare.html">Compare</a>
<a href="/brokers.html">Brokers</a>
<a href="/sources.html">Sources</a>
<a href="/watchlist.html" class="active">Watchlist</a>
<a href="/admin.html">Admin</a>
</nav>
</div>
<main>
<h2>Add a market</h2>
<div class="addrow">
<div id="sugg" style="flex:1;min-width:240px">
<input id="q" placeholder="Search county or metro… (e.g. Los Angeles)" autocomplete="off">
<div id="sugglist"></div>
</div>
<select id="thr-metric">
<option value="">no alert threshold</option>
<option value="zhvi_yoy">ZHVI YoY</option>
<option value="median_sale_price_yoy">Sale Price YoY</option>
<option value="months_of_supply">Months Supply</option>
<option value="dom">DOM</option>
<option value="rent_yield">Rent Yield</option>
</select>
<select id="thr-op"><option>></option><option><</option><option>>=</option><option><=</option></select>
<input id="thr-val" type="number" step="any" placeholder="value" style="width:90px">
<button id="add" disabled>Watch</button>
</div>
<h2>Watched markets</h2>
<div id="list"><div class="empty">loading…</div></div>
<h2>Recent alerts</h2>
<div id="alerts"><div class="empty">loading…</div></div>
</main>
<script>
let regions = [];
let selected = null;
const fmt = {
score: v => v==null?'—':v.toFixed(1),
usd: v => v==null?'—':'$'+Math.round(v).toLocaleString(),
pct: v => v==null?'—':(v*100).toFixed(1)+'%',
num: v => v==null?'—':(+v).toLocaleString(),
};
async function loadRegions(){
const [c, m] = await Promise.all([
fetch('/api/regions?type=county').then(r=>r.json()),
fetch('/api/regions?type=metro').then(r=>r.json()),
]);
regions = [...c.regions, ...m.regions];
}
const q = document.getElementById('q'), sl = document.getElementById('sugglist'), addBtn = document.getElementById('add');
q.addEventListener('input', () => {
selected = null; addBtn.disabled = true;
const s = q.value.trim().toLowerCase();
if (s.length < 2){ sl.style.display='none'; return; }
const hits = regions.filter(r => r.name.toLowerCase().includes(s)).slice(0, 12);
sl.innerHTML = hits.map(r =>
`<div data-key="${r.canonical_key}">${r.name}${r.state_code?', '+r.state_code:''} <span style="color:var(--muted)">· ${r.region_type}</span></div>`).join('');
sl.style.display = hits.length ? 'block' : 'none';
[...sl.children].forEach(div => div.onclick = () => {
selected = div.dataset.key;
q.value = div.textContent.trim();
sl.style.display = 'none';
addBtn.disabled = false;
});
});
addBtn.onclick = async () => {
const metric = document.getElementById('thr-metric').value;
const val = parseFloat(document.getElementById('thr-val').value);
const thresholds = metric && Number.isFinite(val)
? { metric, op: document.getElementById('thr-op').value, value: val } : null;
await fetch('/api/watchlist', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ key: selected, thresholds }) });
q.value=''; selected=null; addBtn.disabled=true;
loadList();
};
async function loadList(){
const d = await fetch('/api/watchlist').then(r=>r.json());
const el = document.getElementById('list');
if (!d.items.length){ el.innerHTML = '<div class="empty">Nothing watched yet — search above to add a market.</div>'; return; }
el.innerHTML = d.items.map(w => `
<div class="card">
<div class="nm"><a href="/market.html?key=${w.key}">${w.name}${w.state?', '+w.state:''}</a>
<div style="font-size:11px;color:var(--muted)">${w.region_type}</div></div>
<div class="stat"><div class="v">${fmt.score(w.opportunity_score)}</div><div class="l">Score</div></div>
<div class="stat"><div class="v">${fmt.usd(w.zhvi)}</div><div class="l">ZHVI</div></div>
<div class="stat"><div class="v">${fmt.pct(w.zhvi_yoy)}</div><div class="l">YoY</div></div>
<div class="stat"><div class="v">${fmt.num(w.dom)}</div><div class="l">DOM</div></div>
<div class="thr">${w.thresholds ? 'alert: '+w.thresholds.metric+' '+w.thresholds.op+' '+w.thresholds.value : 'no threshold'}</div>
<button class="del" data-id="${w.id}">remove</button>
</div>`).join('');
[...el.querySelectorAll('.del')].forEach(b => b.onclick = async () => {
await fetch('/api/watchlist/' + b.dataset.id, { method:'DELETE' });
loadList();
});
}
async function loadAlerts(){
const d = await fetch('/api/alerts').then(r=>r.json());
const el = document.getElementById('alerts');
if (!d.alerts.length){ el.innerHTML = '<div class="empty">No alerts fired.</div>'; return; }
el.innerHTML = d.alerts.map(a => `
<div class="alert-row">
<b>${a.name||'?'}</b> ${a.metric} hit ${(+a.observed).toLocaleString()}
<span style="color:var(--muted)">(threshold ${a.detail?.threshold?.op} ${a.detail?.threshold?.value})</span>
<span class="when" title="${a.fired_at}">🕓 ${new Date(a.fired_at).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}</span>
</div>`).join('');
}
loadRegions().then(()=>{ loadList(); loadAlerts(); });
</script>
<script src="/nav-drawer.js"></script>
<script src="/geo-search.js"></script>
</body>
</html>