← back to Rentv 2026
auto-save: 2026-07-31T16:29:30 (1 files) — public/watchlist.html
b4f527159d78534fda955b47c88b0735d4eb368f · 2026-07-31 16:29:31 -0700 · Steve Abrams
Files touched
Diff
commit b4f527159d78534fda955b47c88b0735d4eb368f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 31 16:29:31 2026 -0700
auto-save: 2026-07-31T16:29:30 (1 files) — public/watchlist.html
---
public/watchlist.html | 41 ++++++++++++++++++++++++++++++++++++++---
1 file changed, 38 insertions(+), 3 deletions(-)
diff --git a/public/watchlist.html b/public/watchlist.html
index 33e2bc90..43e47425 100644
--- a/public/watchlist.html
+++ b/public/watchlist.html
@@ -46,7 +46,11 @@
.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}
+ .grid{display:grid;grid-template-columns:repeat(var(--cols,3),1fr);gap:18px;padding:14px 0 60px}
+ .wbar{display:flex;flex-wrap:wrap;gap:14px;align-items:center;margin:0 0 14px}
+ .wctl{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--sub)}
+ .wctl select{font:600 13px 'Inter',system-ui,sans-serif;text-transform:none;letter-spacing:0;color:var(--ink);padding:7px 10px;border:1px solid var(--line);border-radius:6px;background:#fff;cursor:pointer}
+ .wctl.dens input[type=range]{width:110px;accent-color:var(--red);cursor:pointer}
.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}
@@ -60,7 +64,7 @@
.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}}
+ @media(max-width:600px){.grid{grid-template-columns:1fr!important}}
</style></head><body>
<header class="top">
@@ -109,12 +113,38 @@
</div>
</div>
+ <div class="wbar">
+ <label class="wctl"><span>Sort</span>
+ <select id="wsort">
+ <option value="newest">Newest</option>
+ <option value="price-desc">Price ↓</option>
+ <option value="price-asc">Price ↑</option>
+ <option value="type">Property Type A→Z</option>
+ <option value="loc">Location A→Z</option>
+ <option value="title">Title A→Z</option>
+ </select>
+ </label>
+ <label class="wctl dens"><span>Density</span>
+ <input id="wdens" type="range" min="2" max="5" step="1" value="3" aria-label="Grid density (columns)">
+ </label>
+ </div>
+
<section class="grid" id="grid"></section>
</main>
<script>
const $=id=>document.getElementById(id);
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+// sort + density (persisted) — HARD RULE: every data grid gets both
+let wsort=localStorage.getItem('rentv.watchlist.sort')||'newest';
+function amtNum(d){const s=String(d.amount_label||d.amount||'').replace(/[,\s$]/g,'');const m=s.match(/([\d.]+)\s*([KMB])?/i);if(!m)return -1;let n=parseFloat(m[1]);if(isNaN(n))return -1;const u=(m[2]||'').toUpperCase();if(u==='K')n*=1e3;else if(u==='M')n*=1e6;else if(u==='B')n*=1e9;return n;}
+function sortRows(rows){const r=rows.slice();
+ if(wsort==='price-desc')r.sort((a,b)=>amtNum(b)-amtNum(a));
+ else if(wsort==='price-asc')r.sort((a,b)=>{const x=amtNum(a),y=amtNum(b);if(x<0)return 1;if(y<0)return -1;return x-y;});
+ else if(wsort==='type')r.sort((a,b)=>(a.property_type||'').localeCompare(b.property_type||''));
+ else if(wsort==='loc')r.sort((a,b)=>[a.city,a.state].filter(Boolean).join(', ').localeCompare([b.city,b.state].filter(Boolean).join(', ')));
+ else if(wsort==='title')r.sort((a,b)=>(a.title||'').localeCompare(b.title||''));
+ return r;} // 'newest' = natural feed order
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')};
@@ -154,7 +184,7 @@ function render(){
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);
+ const list=sortRows(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">
@@ -169,6 +199,11 @@ $('clear').onclick=()=>{sel={types:[],txns:[],states:[]};save();history.replaceS
(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; }
+ const ws=$('wsort'), wd=$('wdens');
+ ws.value=wsort; ws.onchange=()=>{wsort=ws.value;localStorage.setItem('rentv.watchlist.sort',wsort);render();};
+ const cols0=localStorage.getItem('rentv.watchlist.cols')||'3';
+ wd.value=cols0; document.documentElement.style.setProperty('--cols',cols0);
+ wd.oninput=()=>{document.documentElement.style.setProperty('--cols',wd.value);localStorage.setItem('rentv.watchlist.cols',wd.value);};
render();
})();
</script>
← fccef3ba feat(deals): add sort + density controls to the CRE deal gri
·
back to Rentv 2026
·
feat(watchlist): add sort + density to the deal watchlist gr afcf869a →