← 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>&gt;</option><option>&lt;</option><option>&gt;=</option><option>&lt;=</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>