[object Object]

← back to Rentv 2026

watchlist: personalized Deal Watchlist (/watchlist) — pin property/txn/market, private localStorage + shareable URL params, matching deals, internal-only links

04e9cb8532cd0fb182fb0c84079fb5ad3b8fe805 · 2026-07-23 16:02:03 -0700 · Steve

Files touched

Diff

commit 04e9cb8532cd0fb182fb0c84079fb5ad3b8fe805
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 23 16:02:03 2026 -0700

    watchlist: personalized Deal Watchlist (/watchlist) — pin property/txn/market, private localStorage + shareable URL params, matching deals, internal-only links
---
 public/index.html          |   2 +-
 public/watchlist.html      | 175 +++++++++++++++++++++++++++++++++++++++++++++
 tools/specs/watchlist.json |  23 ++++++
 3 files changed, 199 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index 3887ac7e..94c7f76c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -104,7 +104,7 @@
   <div class="dh"><span class="w">REN<span>TV</span></span><button id="dclose" aria-label="Close">✕</button></div>
   <div class="snap" id="snap"></div>
   <div class="grp"><div class="gt">Discover</div></div>
-  <nav><a href="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</a><a href="/tools">🧮 CRE Toolkit</a></nav>
+  <nav><a href="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</a><a href="/tools">🧮 CRE Toolkit</a><a href="/watchlist">⭐ Deal Watchlist</a></nav>
   <div class="grp"><div class="gt">Sections</div></div>
   <nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
   <div class="grp"><div class="gt">Intelligence</div></div>
diff --git a/public/watchlist.html b/public/watchlist.html
new file mode 100644
index 00000000..33e2bc90
--- /dev/null
+++ b/public/watchlist.html
@@ -0,0 +1,175 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Deal Watchlist — RENTV.com</title>
+<meta name="description" content="Build a private RENTV deal watchlist — pin the property types and markets you care about and see only the matching commercial real estate transactions.">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,800;1,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>
+  :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--wash:#f5f6f8;--up:#0f8a4d}
+  *{box-sizing:border-box;margin:0}
+  body{font-family:'Inter',system-ui,sans-serif;color:var(--ink);background:#fbfbfc;line-height:1.45}
+  a{color:inherit;text-decoration:none}
+  .top{position:sticky;top:0;z-index:900;display:flex;align-items:center;justify-content:space-between;height:54px;padding:0 16px;background:#fff;border-bottom:1px solid var(--line)}
+  .burger{display:inline-flex;align-items:center;gap:10px;border:0;background:none;cursor:pointer;font:inherit;color:inherit;padding:7px 9px;border-radius:8px}
+  .burger:hover{background:rgba(0,0,0,.06)}
+  .burger .lbl{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sub)}
+  .brand{position:absolute;left:50%;transform:translateX(-50%);font-family:'Playfair Display',Georgia,serif;font-weight:800;font-size:25px;letter-spacing:.02em}.brand span{color:var(--red)}
+  .user{width:37px;height:37px;border-radius:50%;border:1.5px solid var(--line);background:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ink)}
+  .user:hover{border-color:var(--red);color:var(--red)}
+  .scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:opacity .22s;z-index:950}
+  .scrim.open{opacity:1;pointer-events:auto}
+  .drawer{position:fixed;top:0;left:0;height:100%;width:360px;max-width:88vw;background:#0b0e12;color:#e7ecf1;transform:translateX(-102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:951;display:flex;flex-direction:column;overflow-y:auto;box-shadow:2px 0 40px rgba(0,0,0,.4)}
+  .drawer.open{transform:none}
+  .drawer .dh{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid #1c232b}
+  .drawer .dh .w{font-family:'Playfair Display',serif;font-size:23px;font-weight:800}.drawer .dh .w span{color:var(--red)}
+  .drawer .dh button{background:none;border:0;color:#8b949e;font-size:22px;cursor:pointer;line-height:1}
+  .grp{padding:14px 18px 4px}.grp .gt{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#6b747d;margin-bottom:6px}
+  .drawer nav a{display:block;padding:9px 18px;color:#dbe1e7;font-size:14.5px;border-left:3px solid transparent}
+  .drawer nav a:hover{background:#141a21;border-left-color:var(--red);color:#fff}
+  .drawer nav a.active{background:#141a21;border-left-color:var(--red);color:#fff;font-weight:700}
+  .drawer .df{margin-top:auto;padding:16px 18px;border-top:1px solid #1c232b;font-size:11px;color:#6b747d}
+
+  .wrap{max-width:1160px;margin:0 auto;padding:0 22px;width:100%}
+  .whead{padding:34px 0 6px}
+  .whead .k{color:var(--red);font-weight:800;font-size:12px;letter-spacing:.16em;text-transform:uppercase}
+  .whead h1{font-family:'Playfair Display',serif;font-size:clamp(32px,3.6vw,46px);font-weight:700;letter-spacing:-.01em;margin-top:4px}
+  .whead p{color:var(--sub);font-size:15px;margin-top:4px}
+  .panel{background:#fff;border:1px solid var(--line);border-radius:14px;padding:18px 20px;margin:18px 0 6px}
+  .frow{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:12px}
+  .frow:last-child{margin-bottom:0}
+  .frow .lbl{font-size:11px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--sub);width:96px;flex:none}
+  .chip{border:1.5px solid var(--line);background:#fff;border-radius:20px;padding:6px 14px;font:600 13px 'Inter';color:var(--ink);cursor:pointer;transition:.14s}
+  .chip:hover{border-color:var(--red)}
+  .chip.on{background:var(--red);border-color:var(--red);color:#fff}
+  .bar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;padding:16px 0 4px}
+  .bar .cnt{font-size:15px;font-weight:700}.bar .cnt b{color:var(--red)}
+  .bar .acts{display:flex;gap:10px}
+  .bar .acts button,.bar .acts a{border:1.5px solid var(--line);background:#fff;border-radius:8px;padding:8px 14px;font:700 12.5px 'Inter';color:var(--ink);cursor:pointer}
+  .bar .acts .alert{background:var(--red);border-color:var(--red);color:#fff}
+  .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;padding:14px 0 60px}
+  .card{background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
+  .card .im{height:132px;background:#12161a center/cover;display:block}
+  .card .bd{padding:13px 15px 15px;display:flex;flex-direction:column;flex:1}
+  .card .tags{display:flex;gap:6px;margin-bottom:7px}
+  .card .tg{font-size:9px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;padding:2px 7px;border-radius:20px;background:var(--wash);color:#4a4f55}
+  .card .tg.red{background:var(--red);color:#fff}
+  .card h4{font:600 15px/1.3 'Inter';flex:1}
+  .card h4:hover{color:var(--red)}
+  .card .meta{color:var(--sub);font-size:12px;margin-top:8px;display:flex;justify-content:space-between;align-items:baseline}
+  .card .amt{font-family:'Playfair Display',serif;font-weight:800;font-size:17px;color:var(--red)}
+  .empty{grid-column:1/-1;padding:50px 0;text-align:center;color:var(--sub)}
+  .empty h3{font-family:'Playfair Display',serif;font-size:24px;color:var(--ink);margin-bottom:6px}
+  @media(max-width:900px){.grid{grid-template-columns:1fr 1fr}}
+  @media(max-width:600px){.grid{grid-template-columns:1fr}}
+</style></head><body>
+
+<header class="top">
+  <button class="burger" id="burger" aria-label="Menu">
+    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
+    <span class="lbl">Menu</span>
+  </button>
+  <a class="brand" href="/">REN<span>TV</span></a>
+  <a class="user" href="/search" aria-label="Search" title="Search">
+    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
+  </a>
+</header>
+
+<div class="scrim" id="scrim"></div>
+<aside class="drawer" id="drawer">
+  <div class="dh"><div class="w">REN<span>TV</span></div><button id="dx" aria-label="Close">&times;</button></div>
+  <div class="grp"><div class="gt">Discover</div></div>
+  <nav><a href="/search">Search</a><a href="/pulse">Market Pulse</a><a href="/tools">CRE Toolkit</a><a href="/watchlist" class="active">Deal Watchlist</a></nav>
+  <div class="grp"><div class="gt">Sections</div></div>
+  <nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
+  <div class="grp"><div class="gt">Intelligence</div></div>
+  <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker &amp; Owner Desk</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
+  <div class="grp"><div class="gt">Company</div></div>
+  <nav><a href="/about">About</a><a href="/advertise">Advertise</a><a href="/subscribe">Subscribe / Newsletters</a></nav>
+  <div class="df">RENTV.com — Commercial real estate news, Western U.S.</div>
+</aside>
+
+<main class="wrap">
+  <div class="whead">
+    <div class="k">Deal Watchlist</div>
+    <h1>Only the deals you care about</h1>
+    <p>Pin the property types and markets that matter to you. Your watchlist saves in this browser — private, no account needed.</p>
+  </div>
+
+  <div class="panel">
+    <div class="frow"><span class="lbl">Property</span><span id="fType"></span></div>
+    <div class="frow"><span class="lbl">Transaction</span><span id="fTxn"></span></div>
+    <div class="frow"><span class="lbl">Market</span><span id="fState"></span></div>
+  </div>
+
+  <div class="bar">
+    <div class="cnt" id="cnt">—</div>
+    <div class="acts">
+      <button id="clear">Clear</button>
+      <a class="alert" href="/subscribe">🔔 Get email alerts</a>
+    </div>
+  </div>
+
+  <section class="grid" id="grid"></section>
+</main>
+
+<script>
+const $=id=>document.getElementById(id);
+const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const drawer=$('drawer'),scrim=$('scrim');
+$('burger').onclick=()=>{drawer.classList.add('open');scrim.classList.add('open')};
+const close=()=>{drawer.classList.remove('open');scrim.classList.remove('open')};
+$('dx').onclick=close;scrim.onclick=close;document.addEventListener('keydown',e=>{if(e.key==='Escape')close()});
+
+const KEY='rentv_watchlist';
+let ALL=[];
+let sel=load();
+
+function load(){
+  const p=new URLSearchParams(location.search);
+  const fromUrl=k=>(p.get(k)||'').split(',').map(s=>s.trim()).filter(Boolean);
+  if(p.has('types')||p.has('txns')||p.has('states'))
+    return {types:fromUrl('types'),txns:fromUrl('txns'),states:fromUrl('states')};
+  try{const s=JSON.parse(localStorage.getItem(KEY));if(s)return {types:s.types||[],txns:s.txns||[],states:s.states||[]};}catch{}
+  return {types:[],txns:[],states:[]};
+}
+function save(){ try{localStorage.setItem(KEY,JSON.stringify(sel));}catch{} }
+
+function chips(elId,field,options){
+  const el=$(elId);
+  el.innerHTML=options.map(o=>`<button class="chip${sel[field].includes(o)?' on':''}" data-v="${esc(o)}">${esc(o)}</button>`).join('')||'<span style="color:var(--sub);font-size:13px">—</span>';
+  el.querySelectorAll('.chip').forEach(b=>b.onclick=()=>{
+    const v=b.dataset.v, i=sel[field].indexOf(v);
+    if(i>=0)sel[field].splice(i,1);else sel[field].push(v);
+    save();render();
+  });
+}
+function match(d){
+  return (!sel.types.length||sel.types.includes(d.property_type))
+      && (!sel.txns.length||sel.txns.includes(d.txn_type))
+      && (!sel.states.length||sel.states.includes(d.state));
+}
+function render(){
+  const uniq=(k)=>[...new Set(ALL.map(d=>d[k]).filter(Boolean))];
+  chips('fType','types',uniq('property_type').sort());
+  chips('fTxn','txns',['Sale','Financing','Lease','Development'].filter(t=>uniq('txn_type').includes(t)));
+  chips('fState','states',uniq('state').sort());
+  const any=sel.types.length||sel.txns.length||sel.states.length;
+  const list=ALL.filter(match);
+  $('cnt').innerHTML=any?`<b>${list.length}</b> deal${list.length===1?'':'s'} match your watchlist`:`Showing all <b>${list.length}</b> tracked deals — pin filters above to narrow`;
+  if(!list.length){ $('grid').innerHTML=`<div class="empty"><h3>No deals match yet</h3><div>Loosen a filter, or <a href="/subscribe" style="color:var(--red);font-weight:700">get alerted</a> when one lands.</div></div>`; return; }
+  $('grid').innerHTML=list.map(d=>`<div class="card">
+    ${d.image?`<a class="im" href="/deals" style="background-image:url('${esc(d.image)}')"></a>`:''}
+    <div class="bd">
+      <div class="tags">${d.property_type?`<span class="tg red">${esc(d.property_type)}</span>`:''}${d.txn_type?`<span class="tg">${esc(d.txn_type)}</span>`:''}</div>
+      <a href="/deals"><h4>${esc(d.title)}</h4></a>
+      <div class="meta"><span>${esc([d.city,d.state].filter(Boolean).join(', '))}</span><span class="amt">${esc(d.amount_label||'')}</span></div>
+    </div></div>`).join('');
+}
+$('clear').onclick=()=>{sel={types:[],txns:[],states:[]};save();history.replaceState(null,'','/watchlist');render();};
+
+(async function(){
+  try{ const r=await fetch('/api/deals'); ALL=(await r.json()).deals||[]; }catch(e){ $('grid').innerHTML='<div class="empty"><h3>Deals unavailable</h3></div>'; return; }
+  render();
+})();
+</script>
+</body></html>
diff --git a/tools/specs/watchlist.json b/tools/specs/watchlist.json
new file mode 100644
index 00000000..7c556ddb
--- /dev/null
+++ b/tools/specs/watchlist.json
@@ -0,0 +1,23 @@
+{
+  "slug": "watchlist",
+  "title": "Deal Watchlist",
+  "subtitle": "Only the deals you care about",
+  "voice": "Samantha",
+  "scenes": [
+    {
+      "url": "/watchlist",
+      "say": "The Deal Watchlist puts you in control of the feed. By default you see every tracked transaction across the Western U.S.",
+      "caption": "Every tracked deal — until you narrow it"
+    },
+    {
+      "url": "/watchlist?types=Office,Industrial",
+      "say": "Pin the property types you cover — say office and industrial — and the feed instantly narrows to just those deals.",
+      "caption": "Pin Office + Industrial — the feed narrows instantly"
+    },
+    {
+      "url": "/watchlist?types=Multifamily",
+      "say": "Your watchlist saves privately in your own browser, no account needed — and you can turn on email alerts to get new matches the moment they land.",
+      "caption": "Saved privately in your browser · turn on email alerts"
+    }
+  ]
+}

← b236f487 tools: CRE Toolkit (/tools) — cap rate, price per unit/SF, l  ·  back to Rentv 2026  ·  auto-save: 2026-07-23T16:22:30 (3 files) — data/markets.json 82f30a9a →