← back to Nationalrealestate
usre: commercial-feed.html Recorded Activity gets grid/list/table views + all-column click-to-sort via ?sort=&order= + full Sort select (view+sort persisted; featured/marquee editorial section unchanged)
7b501531ed281269a2200f23883041675f6fd6b6 · 2026-08-13 08:10:33 -0700 · steve
Files touched
M public/commercial-feed.html
Diff
commit 7b501531ed281269a2200f23883041675f6fd6b6
Author: steve <steve@designerwallcoverings.com>
Date: Thu Aug 13 08:10:33 2026 -0700
usre: commercial-feed.html Recorded Activity gets grid/list/table views + all-column click-to-sort via ?sort=&order= + full Sort select (view+sort persisted; featured/marquee editorial section unchanged)
---
public/commercial-feed.html | 114 +++++++++++++++++++++++++++++++++++++++-----
1 file changed, 103 insertions(+), 11 deletions(-)
diff --git a/public/commercial-feed.html b/public/commercial-feed.html
index 94bebfa..98fe8c6 100644
--- a/public/commercial-feed.html
+++ b/public/commercial-feed.html
@@ -34,6 +34,25 @@
.story .b{font-size:13px;color:#c3ccd8;margin-top:7px;line-height:1.45;}
.story .f{font-size:11px;color:var(--muted);margin-top:8px;}
#loading{padding:40px;text-align:center;color:var(--muted);}
+ /* view segment toggle */
+ .cg-seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;}
+ .cg-seg button{background:var(--panel);color:var(--muted);border:0;border-right:1px solid var(--line);padding:7px 9px;font:12px inherit;cursor:pointer;}
+ .cg-seg button:last-child{border-right:0;}
+ .cg-seg button.active{background:var(--gold);color:var(--bg);font-weight:700;}
+ /* GRID view: cards */
+ #feed.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;}
+ #feed.grid .item{flex-direction:column;gap:6px;border:1px solid var(--line);border-radius:10px;background:linear-gradient(160deg,#141a24,#0d1119);}
+ #feed.grid .item .date{min-width:0;}
+ /* TABLE view: dense all-columns spreadsheet with click-to-sort headers */
+ #feed.table{display:block;}
+ table.feedtbl{border-collapse:collapse;width:100%;font-size:13px;font-variant-numeric:tabular-nums;}
+ table.feedtbl thead th{position:sticky;top:57px;z-index:5;background:#0d1119;border-bottom:1px solid var(--line);color:var(--muted);
+ text-align:left;padding:7px 10px;font-weight:600;white-space:nowrap;cursor:pointer;}
+ table.feedtbl thead th:hover{color:var(--gold);} table.feedtbl thead th.sorted{color:var(--gold);}
+ table.feedtbl thead th.num{text-align:right;}
+ table.feedtbl tbody td{border-bottom:1px solid var(--line);padding:6px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:280px;}
+ table.feedtbl tbody td.num{text-align:right;}
+ table.feedtbl tbody tr{cursor:pointer;} table.feedtbl tbody tr:hover{background:#141a24;}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
<link rel="stylesheet" href="/geo-search.css">
@@ -55,6 +74,25 @@
<div id="controls">
<div id="seg"></div>
<select id="city"><option value="">All LA cities</option></select>
+ <select id="sort" title="Sort recorded activity">
+ <option value="recording_date:desc">Recorded newest</option>
+ <option value="recording_date:asc">Recorded oldest</option>
+ <option value="assessed_total:desc">Assessed ↓</option>
+ <option value="assessed_total:asc">Assessed ↑</option>
+ <option value="address:asc">Address A→Z</option>
+ <option value="address:desc">Address Z→A</option>
+ <option value="city:asc">City A→Z</option>
+ <option value="type:asc">Type A→Z</option>
+ <option value="sqft:desc">SqFt ↓</option>
+ <option value="sqft:asc">SqFt ↑</option>
+ <option value="year_built:desc">Built newest</option>
+ <option value="year_built:asc">Built oldest</option>
+ </select>
+ <span class="cg-seg" id="viewseg">
+ <button data-view="grid" title="Grid">▦ Grid</button>
+ <button data-view="list" class="active" title="List">☰ List</button>
+ <button data-view="table" title="Table">▤ Table</button>
+ </span>
</div>
<h3 style="font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:6px 0 8px;">Featured Assets</h3>
<div id="featured"></div>
@@ -64,8 +102,12 @@
</main>
<script>
const $=s=>document.querySelector(s), usd=n=>n==null?'—':'$'+Number(n).toLocaleString();
-const state={type:'',city:''};
-function dstr(d){const dt=new Date(d);return dt.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'});}
+const esc=s=>(s==null?'':String(s)).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const state={type:'',city:'',sort:localStorage.getItem('cfeed:sort')||'recording_date:desc'};
+let VIEW=localStorage.getItem('cfeed:view')||'list'; // grid | list | table
+const sortParts=()=>{const [k,d]=state.sort.split(':');return {sort:k,order:(d==='desc'?'desc':'asc')};};
+function dstr(d){if(!d) return '—'; const dt=new Date(d);return dt.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'});}
+function propUrl(ain){return '/commercial-property.html?ain='+encodeURIComponent(ain);}
async function j(u){const r=await fetch(u);return r.json();}
async function loadTypes(){
const d=await j('/api/commercial/types');
@@ -92,27 +134,77 @@ async function loadFeatured(){
<div class="f">${r.address||''}${r.city?', '+r.city:''}</div>
</div>`).join('');
}
+let ROWS=[];
async function load(){
$('#loading').style.display='block';
loadFeatured();
- const p=new URLSearchParams({limit:80});
+ const sp=sortParts();
+ const p=new URLSearchParams({limit:80, sort:sp.sort, order:sp.order});
if(state.type)p.set('type',state.type); if(state.city)p.set('city',state.city);
const d=await j('/api/commercial/feed?'+p);
- $('#feed').innerHTML=d.results.map(r=>`<div class="item" style="cursor:pointer" onclick="location.href='/commercial-property.html?ain='+encodeURIComponent('${r.ain}')">
+ ROWS=d.results||[];
+ renderFeed();
+ $('#loading').style.display='none';
+}
+
+// grid/list render (list = the editorial default; grid = card variant, same markup)
+function itemHtml(r){
+ return `<div class="item" onclick="location.href='${propUrl(r.ain)}'">
<div class="date">${dstr(r.recording_date)}</div>
<div class="body">
- <div class="hl">${r.address||'—'}${r.city?`, ${r.city}`:''}</div>
+ <div class="hl">${esc(r.address)||'—'}${r.city?`, ${esc(r.city)}`:''}</div>
<div class="meta">
- <span class="tag">${r.type_label}</span>
- <span>${r.use_desc||''}</span>
+ <span class="tag">${esc(r.type_label)}</span>
+ <span>${esc(r.use_desc)||''}</span>
<span class="val">Assessed ${usd(r.assessed_total)}</span>
${r.sqft?`<span>${Number(r.sqft).toLocaleString()} sqft</span>`:''}
+ ${r.year_built?`<span>built ${r.year_built}</span>`:''}
</div>
- ${r.brief?`<div class="brief">${r.brief}</div>`:''}
- </div></div>`).join('') || '<div id="loading">No recorded activity for this filter.</div>';
- $('#loading').style.display='none';
+ ${r.brief?`<div class="brief">${esc(r.brief)}</div>`:''}
+ </div></div>`;
+}
+// table: dense all-columns spreadsheet, click-to-sort headers -> ?sort=&order=
+const FTBL=[['recording_date','Recorded'],['address','Address'],['city','City'],['type','Type'],
+ ['use_desc','Use'],['assessed_total','Assessed','num'],['sqft','SqFt','num'],['year_built','Built','num']];
+function tableHtml(){
+ const [sk,sd]=state.sort.split(':'); const arr=k=>k===sk?(sd==='desc'?' ▾':' ▴'):'';
+ let h='<table class="feedtbl"><thead><tr>'+
+ FTBL.map(([k,l,n])=>`<th data-k="${k}" class="${n?'num':''}${k===sk?' sorted':''}">${esc(l)}${arr(k)}</th>`).join('')+
+ '</tr></thead><tbody>';
+ for(const r of ROWS){
+ h+=`<tr onclick="location.href='${propUrl(r.ain)}'">`+
+ `<td>${dstr(r.recording_date)}</td>`+
+ `<td>${esc(r.address)||'—'}</td>`+
+ `<td>${esc(r.city)||'—'}</td>`+
+ `<td>${esc(r.type_label)||'—'}</td>`+
+ `<td>${esc(r.use_desc)||'—'}</td>`+
+ `<td class="num">${usd(r.assessed_total)}</td>`+
+ `<td class="num">${r.sqft?Number(r.sqft).toLocaleString():'—'}</td>`+
+ `<td class="num">${r.year_built||'—'}</td>`+
+ `</tr>`;
+ }
+ return h+'</tbody></table>';
+}
+function renderFeed(){
+ const el=$('#feed');
+ $('#viewseg').querySelectorAll('button').forEach(b=>b.classList.toggle('active',b.dataset.view===VIEW));
+ if(!ROWS.length){ el.className=''; el.innerHTML='<div style="padding:30px;color:var(--muted)">No recorded activity for this filter.</div>'; return; }
+ if(VIEW==='table'){ el.className='table'; el.innerHTML=tableHtml();
+ el.querySelectorAll('thead th[data-k]').forEach(th=>th.onclick=()=>{
+ const k=th.dataset.k; const [ck,cd]=state.sort.split(':');
+ state.sort=(k===ck)?`${k}:${cd==='asc'?'desc':'asc'}`:`${k}:asc`;
+ localStorage.setItem('cfeed:sort',state.sort); $('#sort').value=state.sort; load();
+ });
+ return;
+ }
+ el.className=(VIEW==='grid'?'grid':'');
+ el.innerHTML=ROWS.map(itemHtml).join('');
}
-(async()=>{ await loadTypes(); await loadCities(); await load(); })();
+
+$('#sort').onchange=e=>{ state.sort=e.target.value; localStorage.setItem('cfeed:sort',state.sort); load(); };
+$('#viewseg').querySelectorAll('button').forEach(b=>b.onclick=()=>{ VIEW=b.dataset.view; localStorage.setItem('cfeed:view',VIEW); renderFeed(); });
+
+(async()=>{ $('#sort').value=state.sort; await loadTypes(); await loadCities(); await load(); })();
</script>
<script src="/nav-drawer.js"></script>
<script src="/geo-search.js"></script>
← 1227b5c usre: deals.html gets grid/list/table view toggle + every ta
·
back to Nationalrealestate
·
nav-agent: v1.1 merge-mode asset refresh 27b936f →