← back to Nationalrealestate
usreal: dedupe residential+commercial broker desks into shared broker-desk.js (mode param) + add SORT + DENSITY + real Prev/Next pagination (standing grid rule + Cody gate). commercial-brokers now truly commercial-only (was mislabeled all) (TK-10590 cycle 2)
e61c69f30e68028c648a115d866a0711964673b0 · 2026-08-15 08:26:40 -0700 · Steve Abrams
Files touched
A public/broker-desk.jsM public/commercial-brokers.htmlM public/residential-brokers.html
Diff
commit e61c69f30e68028c648a115d866a0711964673b0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 15 08:26:40 2026 -0700
usreal: dedupe residential+commercial broker desks into shared broker-desk.js (mode param) + add SORT + DENSITY + real Prev/Next pagination (standing grid rule + Cody gate). commercial-brokers now truly commercial-only (was mislabeled all) (TK-10590 cycle 2)
---
public/broker-desk.js | 121 ++++++++++++++++++++++++++++++++++++++++
public/commercial-brokers.html | 68 +++++-----------------
public/residential-brokers.html | 68 ++++------------------
3 files changed, 144 insertions(+), 113 deletions(-)
diff --git a/public/broker-desk.js b/public/broker-desk.js
new file mode 100644
index 0000000..3695707
--- /dev/null
+++ b/public/broker-desk.js
@@ -0,0 +1,121 @@
+/* broker-desk.js — shared residential/commercial broker & firm desk for usreal (TK-10590).
+ * A page sets `window.DESK_MODE = 'residential' | 'commercial' | ''` (asset_class) BEFORE loading
+ * this script, plus an optional `window.DESK_CITY` default. One module, two surfaces — kills the
+ * drift between residential-brokers.html and commercial-brokers.html (Cody gate: they were 97% dup).
+ * Adds the standing grid requirements missing from the old clones: SORT + DENSITY + real PAGINATION,
+ * and an honest count (never show the full total next to a truncated page). */
+(function () {
+ const MODE = window.DESK_MODE || ''; // '' = all, else asset_class
+ const AGENTS = MODE === 'residential';
+ const NOUN = AGENTS ? 'agents' : 'brokers';
+ const PAGE = 200;
+ const $ = (s) => document.querySelector(s);
+ const LS = { get(k){try{return localStorage.getItem(k)}catch{return null}}, set(k,v){try{localStorage.setItem(k,v)}catch{}} };
+ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
+
+ const S = {
+ tab: 'firms',
+ city: (window.DESK_CITY !== undefined ? window.DESK_CITY : 'LOS ANGELES'),
+ q: '',
+ sort: LS.get('desk.sort') || '',
+ dens: +(LS.get('desk.dens') || 280),
+ offset: 0, total: 0, shown: 0
+ };
+
+ const CITIES = [['LOS ANGELES','Los Angeles'],['BEVERLY HILLS','Beverly Hills'],['SANTA MONICA','Santa Monica'],
+ ['LONG BEACH','Long Beach'],['BURBANK','Burbank'],['GLENDALE','Glendale'],['PASADENA','Pasadena'],['','All CA']];
+
+ const root = document.getElementById('desk-root');
+ root.innerHTML = `
+ <div id="controls">
+ <div id="tabs"><button data-t="firms" class="active">Firms</button><button data-t="brokers">${AGENTS?'Agents':'Brokers'}</button></div>
+ <select id="city">${CITIES.map(([v,l])=>`<option value="${v}"${v===S.city?' selected':''}>${l}</option>`).join('')}</select>
+ <input id="q" placeholder="Search name / firm / license…" style="width:200px">
+ <select id="sort" title="Sort this page">
+ <option value="">Sort: default</option>
+ <option value="name">Name A→Z</option>
+ <option value="firm">Firm A→Z</option>
+ <option value="agents">Agents ↓</option>
+ <option value="city">City A→Z</option>
+ </select>
+ <label id="densWrap" title="Card density">▦ <input type="range" id="dens" min="200" max="380" step="20" value="${S.dens}"></label>
+ <span id="count"></span>
+ </div>
+ <div id="grid"></div>
+ <div id="pager"><button id="pgPrev">‹ Prev</button><span id="pgInfo"></span><button id="pgNext">Next ›</button></div>
+ <div id="loading">Loading…</div>`;
+
+ // density → grid column min-width (persisted)
+ function applyDens(){ $('#grid').style.gridTemplateColumns = `repeat(auto-fill,minmax(${S.dens}px,1fr))`; }
+ applyDens();
+
+ async function j(u){ const r = await fetch(u); return r.json(); }
+ function params(extra){
+ const p = new URLSearchParams({ state:'CA', limit:PAGE, offset:S.offset });
+ if (MODE) p.set('asset_class', MODE);
+ if (S.city) p.set('city', S.city);
+ if (S.q) p.set('q', S.q);
+ if (extra) for (const k in extra) p.set(k, extra[k]);
+ return p;
+ }
+ function setCount(shown,total){
+ S.shown = shown; S.total = total;
+ $('#count').textContent = (shown < total)
+ ? `showing ${shown.toLocaleString()} of ${total.toLocaleString()} ${S.tab==='firms'?'firms':NOUN} — page ${Math.floor(S.offset/PAGE)+1} of ${Math.max(Math.ceil(total/PAGE),1).toLocaleString()}`
+ : `${total.toLocaleString()} ${S.tab==='firms'?'firms':NOUN}`;
+ const pgs = Math.max(Math.ceil(total/PAGE),1), pg = Math.floor(S.offset/PAGE)+1;
+ $('#pgInfo').textContent = `${pg} / ${pgs.toLocaleString()}`;
+ $('#pgPrev').disabled = S.offset <= 0;
+ $('#pgNext').disabled = pg >= pgs;
+ }
+ // client-side sort of the current page (the API has no sort param; honest — sorts what's shown)
+ function sortRows(rows){
+ const k = S.sort; if (!k) return rows;
+ const cmp = {
+ name: (a,b)=> String(a.name||'').localeCompare(String(b.name||'')),
+ firm: (a,b)=> String(a.firm_name||a.name||'').localeCompare(String(b.firm_name||b.name||'')),
+ agents:(a,b)=> (b.agent_count||0)-(a.agent_count||0),
+ city: (a,b)=> String(a.hq_city||a.city||'').localeCompare(String(b.hq_city||b.city||'')),
+ }[k];
+ return cmp ? rows.slice().sort(cmp) : rows;
+ }
+
+ async function loadFirms(){
+ const d = await j('/api/firms?'+params());
+ const rows = sortRows(d.rows||[]);
+ setCount(rows.length, d.total||0);
+ $('#grid').innerHTML = rows.map(f=>`<div class="card firm" data-name="${esc(f.name)}">
+ <div class="n">${esc(f.name)||'—'}</div>
+ <div class="m"><span class="agents">${f.agent_count||0} agents</span><span>${esc(f.hq_city)}</span>${f.license_no?`<span>DRE ${esc(f.license_no)}</span>`:''}</div>
+ <div class="roster"></div></div>`).join('') || '<div style="color:var(--muted);padding:20px">no firms match</div>';
+ document.querySelectorAll('.card.firm').forEach(c=>c.onclick=()=>toggleRoster(c));
+ }
+ async function toggleRoster(c){
+ const r = c.querySelector('.roster');
+ if (r.style.display==='block'){ r.style.display='none'; return; }
+ r.style.display='block';
+ if (r.dataset.loaded) return;
+ r.innerHTML='loading roster…';
+ const d = await j('/api/brokers?'+params({ limit:40, offset:0, firm:c.dataset.name }));
+ r.dataset.loaded='1';
+ r.innerHTML = (d.rows||[]).slice(0,30).map(b=>`<div>${esc(b.name)} <span style="color:var(--muted)">· ${esc(b.license_type)} · DRE ${esc(b.license_no)}</span></div>`).join('') || '<div style="color:var(--muted)">no roster rows</div>';
+ }
+ async function loadBrokers(){
+ const d = await j('/api/brokers?'+params());
+ const rows = sortRows(d.rows||[]);
+ setCount(rows.length, d.total||0);
+ $('#grid').innerHTML = rows.map(b=>`<div class="card">
+ <div class="n">${esc(b.name)||'—'}</div>
+ <div class="m"><span class="lt">${esc(b.license_type)}</span><span>${esc(b.firm_name)||'—'}</span><span>${esc(b.city)}</span>${b.license_no?`<span>DRE ${esc(b.license_no)}</span>`:''}</div></div>`).join('') || '<div style="color:var(--muted);padding:20px">no '+NOUN+' match</div>';
+ }
+ async function load(){ $('#loading').style.display='block'; S.tab==='firms'?await loadFirms():await loadBrokers(); $('#loading').style.display='none'; }
+
+ $('#tabs').querySelectorAll('button').forEach(b=>b.onclick=()=>{ $('#tabs').querySelectorAll('button').forEach(x=>x.classList.remove('active')); b.classList.add('active'); S.tab=b.dataset.t; S.offset=0; load(); });
+ $('#city').onchange=()=>{ S.city=$('#city').value; S.offset=0; load(); };
+ $('#q').oninput=()=>{ S.q=$('#q').value; S.offset=0; clearTimeout(window._t); window._t=setTimeout(load,250); };
+ $('#sort').value=S.sort; $('#sort').onchange=()=>{ S.sort=$('#sort').value; LS.set('desk.sort',S.sort); load(); };
+ $('#dens').oninput=()=>{ S.dens=+$('#dens').value; LS.set('desk.dens',S.dens); applyDens(); };
+ $('#pgPrev').onclick=()=>{ if(S.offset>0){ S.offset=Math.max(0,S.offset-PAGE); load(); } };
+ $('#pgNext').onclick=()=>{ if((Math.floor(S.offset/PAGE)+1) < Math.ceil(S.total/PAGE)){ S.offset+=PAGE; load(); } };
+ load();
+})();
diff --git a/public/commercial-brokers.html b/public/commercial-brokers.html
index 578b879..f9015ec 100644
--- a/public/commercial-brokers.html
+++ b/public/commercial-brokers.html
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230a0d13'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%23c8a24b'%3ECR%3C/text%3E%3C/svg%3E">
-<title>USRealEstate — LA Brokers & Firms</title>
+<title>USRealEstate — Commercial Brokers & Firms</title>
<style>
:root{--bg:#0a0d13;--panel:#11151d;--line:#222936;--fg:#e8ecf2;--muted:#8a93a3;--gold:#c8a24b;}
*{box-sizing:border-box;}
@@ -19,6 +19,8 @@
#tabs button{background:var(--panel);color:var(--muted);border:0;border-right:1px solid var(--line);padding:7px 14px;font-size:13px;cursor:pointer;}
#tabs button:last-child{border-right:0;} #tabs button.active{background:var(--gold);color:var(--bg);font-weight:700;}
input,select{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;}
+ #densWrap{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-size:13px;}
+ #dens{accent-color:var(--gold);}
#count{font-size:12px;color:var(--muted);margin-left:auto;}
#grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:13px;}
@@ -29,6 +31,9 @@
.lt{font-size:10px;letter-spacing:.05em;text-transform:uppercase;color:var(--gold);border:1px solid var(--line);border-radius:5px;padding:1px 6px;}
.roster{margin-top:9px;padding-top:9px;border-top:1px solid var(--line);font-size:12px;color:#c3ccd8;display:none;}
.roster div{padding:2px 0;}
+ #pager{display:flex;align-items:center;gap:10px;justify-content:center;margin-top:16px;font-size:13px;color:var(--muted);}
+ #pager button{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:6px 12px;cursor:pointer;}
+ #pager button:disabled{opacity:.4;cursor:default;}
#loading{padding:40px;text-align:center;color:var(--muted);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
@@ -37,7 +42,7 @@
</head>
<body>
<div id="topbar">
- <div class="brand"><b>USRealEstate</b> — LA Brokers & Firms</div>
+ <div class="brand"><b>USRealEstate</b> — Commercial Brokers & Firms</div>
<nav>
<a href="/">Map</a><a href="/markets.html">Markets</a>
<a href="/commercial.html">Commercial</a>
@@ -47,60 +52,13 @@
</nav>
</div>
<main>
- <p class="lede">Real estate firms & licensed brokers operating in the LA region, from the California DRE
- current-licensee registry. Firms rank by licensed-agent count — the largest are the major commercial houses.</p>
- <div id="controls">
- <div id="tabs"><button data-t="firms" class="active">Firms</button><button data-t="brokers">Brokers</button></div>
- <select id="city">
- <option value="LOS ANGELES">Los Angeles</option><option value="BEVERLY HILLS">Beverly Hills</option>
- <option value="SANTA MONICA">Santa Monica</option><option value="LONG BEACH">Long Beach</option>
- <option value="BURBANK">Burbank</option><option value="GLENDALE">Glendale</option>
- <option value="PASADENA">Pasadena</option><option value="">All CA</option>
- </select>
- <input id="q" placeholder="Search name / firm / license…" style="width:220px">
- <span id="count"></span>
- </div>
- <div id="grid"></div>
- <div id="loading">Loading…</div>
+ <p class="lede">Commercial real estate firms & licensed brokers operating in the LA region, from the California DRE
+ current-licensee registry (commercial-classified). Firms rank by licensed-agent count — the largest are the major
+ commercial houses. Sort & density controls above; page through with Prev/Next.</p>
+ <div id="desk-root"></div>
</main>
-<script>
-const $=s=>document.querySelector(s);
-const state={tab:'firms',city:'LOS ANGELES',q:''};
-async function j(u){const r=await fetch(u);return r.json();}
-function setCount(rows,total,noun){$('#count').textContent = (rows<total) ? ('showing first '+rows.toLocaleString()+' of '+total.toLocaleString()+' '+noun+' — refine by city/search') : (total.toLocaleString()+' '+noun);}
-function params(){const p=new URLSearchParams({state:'CA',limit:200});if(state.city)p.set('city',state.city);if(state.q)p.set('q',state.q);return p;}
-async function loadFirms(){
- const d=await j('/api/firms?'+params());
- setCount((d.rows||[]).length, d.total||0, 'firms');
- $('#grid').innerHTML=d.rows.map(f=>`<div class="card firm" data-id="${f.id}" data-name="${(f.name||'').replace(/"/g,'"')}">
- <div class="n">${f.name||'—'}</div>
- <div class="m"><span class="agents">${f.agent_count||0} agents</span><span>${f.hq_city||''}</span>${f.license_no?`<span>DRE ${f.license_no}</span>`:''}</div>
- <div class="roster"></div></div>`).join('');
- document.querySelectorAll('.card.firm').forEach(c=>c.onclick=()=>toggleRoster(c));
-}
-async function toggleRoster(c){
- const r=c.querySelector('.roster');
- if(r.style.display==='block'){r.style.display='none';return;}
- r.style.display='block';
- if(r.dataset.loaded){return;}
- r.innerHTML='loading roster…';
- const d=await j('/api/brokers?state=CA&limit=40&firm='+encodeURIComponent(c.dataset.name));
- r.dataset.loaded='1';
- r.innerHTML=d.rows.slice(0,30).map(b=>`<div>${b.name} <span style="color:var(--muted)">· ${b.license_type||''} · DRE ${b.license_no||''}</span></div>`).join('')||'<div style="color:var(--muted)">no roster rows</div>';
-}
-async function loadBrokers(){
- const d=await j('/api/brokers?'+params());
- setCount((d.rows||[]).length, d.total||0, 'brokers');
- $('#grid').innerHTML=d.rows.map(b=>`<div class="card">
- <div class="n">${b.name||'—'}</div>
- <div class="m"><span class="lt">${b.license_type||''}</span><span>${b.firm_name||'—'}</span><span>${b.city||''}</span>${b.license_no?`<span>DRE ${b.license_no}</span>`:''}</div></div>`).join('');
-}
-async function load(){$('#loading').style.display='block';state.tab==='firms'?await loadFirms():await loadBrokers();$('#loading').style.display='none';}
-$('#tabs').querySelectorAll('button').forEach(b=>b.onclick=()=>{$('#tabs').querySelectorAll('button').forEach(x=>x.classList.remove('active'));b.classList.add('active');state.tab=b.dataset.t;load();});
-$('#city').onchange=()=>{state.city=$('#city').value;load();};
-$('#q').oninput=()=>{state.q=$('#q').value;clearTimeout(window._t);window._t=setTimeout(load,250);};
-load();
-</script>
+<script>window.DESK_MODE='commercial';</script>
+<script src="/broker-desk.js"></script>
<script src="/nav-drawer.js"></script>
<script src="/geo-search.js"></script>
<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/residential-brokers.html b/public/residential-brokers.html
index 40525c5..58dc976 100644
--- a/public/residential-brokers.html
+++ b/public/residential-brokers.html
@@ -19,6 +19,8 @@
#tabs button{background:var(--panel);color:var(--muted);border:0;border-right:1px solid var(--line);padding:7px 14px;font-size:13px;cursor:pointer;}
#tabs button:last-child{border-right:0;} #tabs button.active{background:var(--gold);color:var(--bg);font-weight:700;}
input,select{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;}
+ #densWrap{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-size:13px;}
+ #dens{accent-color:var(--gold);}
#count{font-size:12px;color:var(--muted);margin-left:auto;}
#grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:13px;}
@@ -29,6 +31,9 @@
.lt{font-size:10px;letter-spacing:.05em;text-transform:uppercase;color:var(--gold);border:1px solid var(--line);border-radius:5px;padding:1px 6px;}
.roster{margin-top:9px;padding-top:9px;border-top:1px solid var(--line);font-size:12px;color:#c3ccd8;display:none;}
.roster div{padding:2px 0;}
+ #pager{display:flex;align-items:center;gap:10px;justify-content:center;margin-top:16px;font-size:13px;color:var(--muted);}
+ #pager button{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:6px 12px;cursor:pointer;}
+ #pager button:disabled{opacity:.4;cursor:default;}
#loading{padding:40px;text-align:center;color:var(--muted);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
@@ -47,65 +52,12 @@
</div>
<main>
<p class="lede">Residential real estate firms & licensed agents across the region, from the California DRE
- current-licensee registry. This directory is the residential counterpart to the commercial desk — the
- ~204k residential-classified agents that don't appear on the commercial (RENTV) surface. Firms rank by
- licensed-agent count.</p>
- <div id="controls">
- <div id="tabs"><button data-t="firms" class="active">Firms</button><button data-t="brokers">Agents</button></div>
- <select id="city">
- <option value="LOS ANGELES">Los Angeles</option><option value="BEVERLY HILLS">Beverly Hills</option>
- <option value="SANTA MONICA">Santa Monica</option><option value="LONG BEACH">Long Beach</option>
- <option value="BURBANK">Burbank</option><option value="GLENDALE">Glendale</option>
- <option value="PASADENA">Pasadena</option><option value="">All CA</option>
- </select>
- <input id="q" placeholder="Search name / firm / license…" style="width:220px">
- <span id="count"></span>
- </div>
- <div id="grid"></div>
- <div id="loading">Loading…</div>
+ current-licensee registry — the residential counterpart to the commercial desk (the ~204k residential-classified
+ agents that don't appear on the commercial/RENTV surface). Sort & density controls above; page through with Prev/Next.</p>
+ <div id="desk-root"></div>
</main>
-<script>
-const $=s=>document.querySelector(s);
-const state={tab:'firms',city:'LOS ANGELES',q:''};
-async function j(u){const r=await fetch(u);return r.json();}
-// Honest count: this desk pulls a capped page (limit 200). Never show the full total next to a
-// truncated card list — say "first N of TOTAL" so the number matches what's on screen (Cody gate).
-function setCount(rows,total,noun){$('#count').textContent = (rows<total) ? ('showing first '+rows.toLocaleString()+' of '+total.toLocaleString()+' '+noun+' — refine by city/search') : (total.toLocaleString()+' '+noun);}
-// Residential desk — force asset_class=residential on every registry call so this surface only ever
-// shows residential firms/agents (the mirror of RENTV forcing commercial). TK-10590.
-function params(){const p=new URLSearchParams({state:'CA',limit:200,asset_class:'residential'});if(state.city)p.set('city',state.city);if(state.q)p.set('q',state.q);return p;}
-async function loadFirms(){
- const d=await j('/api/firms?'+params());
- setCount((d.rows||[]).length, d.total||0, 'firms');
- $('#grid').innerHTML=d.rows.map(f=>`<div class="card firm" data-id="${f.id}" data-name="${(f.name||'').replace(/"/g,'"')}">
- <div class="n">${f.name||'—'}</div>
- <div class="m"><span class="agents">${f.agent_count||0} agents</span><span>${f.hq_city||''}</span>${f.license_no?`<span>DRE ${f.license_no}</span>`:''}</div>
- <div class="roster"></div></div>`).join('');
- document.querySelectorAll('.card.firm').forEach(c=>c.onclick=()=>toggleRoster(c));
-}
-async function toggleRoster(c){
- const r=c.querySelector('.roster');
- if(r.style.display==='block'){r.style.display='none';return;}
- r.style.display='block';
- if(r.dataset.loaded){return;}
- r.innerHTML='loading roster…';
- const d=await j('/api/brokers?state=CA&limit=40&asset_class=residential&firm='+encodeURIComponent(c.dataset.name));
- r.dataset.loaded='1';
- r.innerHTML=d.rows.slice(0,30).map(b=>`<div>${b.name} <span style="color:var(--muted)">· ${b.license_type||''} · DRE ${b.license_no||''}</span></div>`).join('')||'<div style="color:var(--muted)">no roster rows</div>';
-}
-async function loadBrokers(){
- const d=await j('/api/brokers?'+params());
- setCount((d.rows||[]).length, d.total||0, 'agents');
- $('#grid').innerHTML=d.rows.map(b=>`<div class="card">
- <div class="n">${b.name||'—'}</div>
- <div class="m"><span class="lt">${b.license_type||''}</span><span>${b.firm_name||'—'}</span><span>${b.city||''}</span>${b.license_no?`<span>DRE ${b.license_no}</span>`:''}</div></div>`).join('');
-}
-async function load(){$('#loading').style.display='block';state.tab==='firms'?await loadFirms():await loadBrokers();$('#loading').style.display='none';}
-$('#tabs').querySelectorAll('button').forEach(b=>b.onclick=()=>{$('#tabs').querySelectorAll('button').forEach(x=>x.classList.remove('active'));b.classList.add('active');state.tab=b.dataset.t;load();});
-$('#city').onchange=()=>{state.city=$('#city').value;load();};
-$('#q').oninput=()=>{state.q=$('#q').value;clearTimeout(window._t);window._t=setTimeout(load,250);};
-load();
-</script>
+<script>window.DESK_MODE='residential';</script>
+<script src="/broker-desk.js"></script>
<script src="/nav-drawer.js"></script>
<script src="/geo-search.js"></script>
<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
← 5a16e24 usreal brokers: honest count (show 'first 200 of N' not full
·
back to Nationalrealestate
·
usreal broker desk: honest page-scoped sort labels + roster 53f52a8 →