[object Object]

← 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

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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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 →