[object Object]

← back to Commercialrealestate

feat(condos): List/Grid toggle (both DB-listing & FHA-directory schemas) + density max to 20

5a3ee55f0b882e5ab156ca8b954907e2c13c5070 · 2026-07-02 09:40:54 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 5a3ee55f0b882e5ab156ca8b954907e2c13c5070
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 09:40:54 2026 -0700

    feat(condos): List/Grid toggle (both DB-listing & FHA-directory schemas) + density max to 20
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 public/condos.html | 55 ++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 49 insertions(+), 6 deletions(-)

diff --git a/public/condos.html b/public/condos.html
index afe0255..26febfb 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -32,6 +32,18 @@
   .src{font-size:11px;color:var(--mut);border-top:1px solid var(--line);padding-top:7px;margin-top:2px;}
   .broker{font-size:11px;color:var(--purp);}
   .empty{padding:50px;text-align:center;color:var(--mut);}
+  .viewseg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;}
+  .viewseg button{background:var(--card);color:var(--mut);border:0;border-right:1px solid var(--line);padding:7px 12px;font-size:12px;cursor:pointer;}
+  .viewseg button:last-child{border-right:0;} .viewseg button.active{background:#1f6feb;color:#fff;}
+  .grid.listhost{display:block;}
+  .listwrap{overflow-x:auto;border:2px solid #fff;border-radius:12px;}
+  .listtbl{width:100%;border-collapse:collapse;font-size:12.5px;white-space:nowrap;}
+  .listtbl th,.listtbl td{text-align:right;padding:9px 13px;border-top:1px solid var(--line);}
+  .listtbl th{position:sticky;top:0;background:#0b0e13;color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;border-top:0;}
+  .listtbl td.stick,.listtbl th.stick{text-align:left;position:sticky;left:0;background:var(--card);}
+  .listtbl th.stick{background:#0b0e13;}
+  .listtbl tr:hover td{background:rgba(88,166,255,.06);}
+  .listtbl .la{font-weight:600;color:var(--ink);} .listtbl .lc{color:var(--mut);font-size:11px;}
 </style>
 </head>
 <body>
@@ -50,7 +62,11 @@
       <option value="all">All</option>
     </select>
   </label>
-  <label>Density <input type="range" id="cols" min="1" max="6" step="1" value="3"></label>
+  <span class="viewseg" id="viewseg">
+    <button data-view="grid" class="active">▦ Grid</button>
+    <button data-view="list">☰ List</button>
+  </span>
+  <label>Density <input type="range" id="cols" min="1" max="20" step="1" value="3"></label>
   <div class="count" id="count"></div>
 </div>
 
@@ -68,9 +84,33 @@ function statusBadge(s){
   return `<span class="badge ${s}">${txt}</span>`;
 }
 
+const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+const money=n=>n?'$'+(+n).toLocaleString():'—';
+let VIEW=localStorage.getItem('condoView')||'grid';
+let LAST={rows:[],mode:'db'};
+function paint(){ const g=$('#grid'); if(!LAST.rows.length){ g.className='grid'; g.innerHTML='<div class="empty">no projects match.</div>'; return; }
+  if(VIEW==='list'){ g.className='grid listhost'; g.innerHTML=(LAST.mode==='db'?dbList:fhaList)(LAST.rows); }
+  else { g.className='grid'; g.innerHTML=(LAST.mode==='db'?dbCards:fhaCards)(LAST.rows); } }
+function dbList(rows){
+  const head=['Project','Status','Price','HOA','City','ZIP','Broker','Firm'];
+  return `<div class="listwrap"><table class="listtbl"><thead><tr><th class="stick">${head[0]}</th>${head.slice(1).map(h=>`<th>${h}</th>`).join('')}</tr></thead><tbody>`
+    +rows.map(c=>`<tr><td class="stick"><div class="la">${esc(c.project_name||c.address||'Condo')}</div><div class="lc">${esc(c.address||'')}</div></td>`
+      +`<td>${esc(c.warrantable_status||'—')}</td><td>${money(c.price)}</td><td>${c.hoa?'$'+c.hoa+'/mo':'—'}</td>`
+      +`<td>${esc(c.city||'—')}</td><td>${esc(c.zip||'—')}</td><td>${esc(c.broker_name||'—')}</td><td>${esc(c.firm_name||'—')}</td></tr>`).join('')
+    +'</tbody></table></div>';
+}
+function fhaList(rows){
+  const head=['Project','Signal','FHA concentration','Expires','City','ZIP','Method','HUD ID'];
+  return `<div class="listwrap"><table class="listtbl"><thead><tr><th class="stick">${head[0]}</th>${head.slice(1).map(h=>`<th>${h}</th>`).join('')}</tr></thead><tbody>`
+    +rows.map(c=>`<tr><td class="stick"><div class="la">${esc(c.project_name||'')}</div><div class="lc">${esc(c.address||'')}</div></td>`
+      +`<td>${esc(c.warrant_signal||'—')}</td><td>${esc(c.fha_concentration||'—')}</td><td>${esc(c.expiration_date||'—')}</td>`
+      +`<td>${esc(c.city||'—')}</td><td>${esc(c.zip||'—')}</td><td>${esc(c.approval_method||'—')}</td><td>${esc(c.condo_id||'—')}</td></tr>`).join('')
+    +'</tbody></table></div>';
+}
 // Render either real DB condo listings (preferred) OR the FHA-approved project directory.
-function renderDbCondos(rows){
-  $('#grid').innerHTML = rows.map(c=>{
+function renderDbCondos(rows){ LAST={rows,mode:'db'}; paint(); }
+function dbCards(rows){
+  return rows.map(c=>{
     const ws=c.warrant_signals; const sigArr=Array.isArray(ws)?ws:((ws&&ws.signals)||[]);
     const sig=sigArr.map(x=>`• ${(x&&x.note)||x}`).join('<br>');
     return `<div class="condo">
@@ -85,8 +125,9 @@ function renderDbCondos(rows){
   }).join('');
 }
 
-function renderFhaDirectory(rows){
-  $('#grid').innerHTML = rows.map(c=>`
+function renderFhaDirectory(rows){ LAST={rows:rows||[],mode:'fha'}; paint(); }
+function fhaCards(rows){
+  return rows.map(c=>`
     <div class="condo">
       <div class="name">${c.project_name}</div>
       <div class="addr">${c.address||''}${c.city?', '+c.city:''} ${c.zip||''}</div>
@@ -95,7 +136,7 @@ function renderFhaDirectory(rows){
       <div class="row"><span class="k">Composition</span><span style="text-align:right;max-width:60%">${(c.composition||'').slice(0,60)}</span></div>
       <div class="row"><span class="k">Expires</span><span>${c.expiration_date||'—'}</span></div>
       <div class="src">HUD ${c.condo_id||''} ${c.submission||''} · method ${c.approval_method||'—'} · status ${c.status||c.hud_status||''}</div>
-    </div>`).join('') || '<div class="empty">no projects match.</div>';
+    </div>`).join('');
 }
 
 async function load(){
@@ -122,6 +163,8 @@ $('#q').oninput=()=>{ clearTimeout(window._t); window._t=setTimeout(load,200); }
 $('#status').onchange=load;
 $('#cols').oninput=e=>{ document.documentElement.style.setProperty('--cols',e.target.value); localStorage.setItem('condoCols',e.target.value); };
 const savedCols=localStorage.getItem('condoCols'); if(savedCols){ $('#cols').value=savedCols; document.documentElement.style.setProperty('--cols',savedCols); }
+document.querySelectorAll('#viewseg button').forEach(b=>b.classList.toggle('active',b.dataset.view===VIEW));
+$('#viewseg').onclick=e=>{ const b=e.target.closest('button[data-view]'); if(!b) return; VIEW=b.dataset.view; localStorage.setItem('condoView',VIEW); document.querySelectorAll('#viewseg button').forEach(x=>x.classList.toggle('active',x.dataset.view===VIEW)); paint(); };
 load();
 </script>
 </body>

← c02f5b2 feat(index): Amazon-style docked left filter rail + List/Gri  ·  back to Commercialrealestate  ·  auto-save: 2026-07-02T09:46:55 (1 files) — public/mls.html d74ce02 →