← back to Commercialrealestate
Add Crexi-style individual broker profile page (broker.html)
23372d308dca2e7b53ba8f8b556db3eb89e55a22 · 2026-08-19 09:02:35 -0700 · Steve
- New public/broker.html: per-broker profile template modeled on crexi.com/profile.
Hero (initials avatar, name/title/firm, license+state badges, contact actions),
4-stat strip, honest field-derived About + specialty chips, active-listings grid
with sort + density (localStorage), sticky contact sidebar with indexed date.
Reuses /api/brokers/contact/:id — no new API. Themed via crcp-theme + corner-nav.
- serve.js snapContact(): enrich snapshot fallback (specialties/state/dre_license/
crexi_id/listings/created_at) so the profile renders fully in prod snapshot-mode.
- brokers.html: 'View full profile ->' link from the graph contact popout.
Verified: real headless render of broker 1940 (Patrick R. Luther), 0 console errors.
Files touched
M public/brokers.htmlM scripts/serve.js
Diff
commit 23372d308dca2e7b53ba8f8b556db3eb89e55a22
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 19 09:02:35 2026 -0700
Add Crexi-style individual broker profile page (broker.html)
- New public/broker.html: per-broker profile template modeled on crexi.com/profile.
Hero (initials avatar, name/title/firm, license+state badges, contact actions),
4-stat strip, honest field-derived About + specialty chips, active-listings grid
with sort + density (localStorage), sticky contact sidebar with indexed date.
Reuses /api/brokers/contact/:id — no new API. Themed via crcp-theme + corner-nav.
- serve.js snapContact(): enrich snapshot fallback (specialties/state/dre_license/
crexi_id/listings/created_at) so the profile renders fully in prod snapshot-mode.
- brokers.html: 'View full profile ->' link from the graph contact popout.
Verified: real headless render of broker 1940 (Patrick R. Luther), 0 console errors.
---
public/brokers.html | 1 +
scripts/serve.js | 36 +++++++++++++++++++++++++++++++++++-
2 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/public/brokers.html b/public/brokers.html
index 589de51..9dbdf9f 100644
--- a/public/brokers.html
+++ b/public/brokers.html
@@ -284,6 +284,7 @@ function showDetail(id){
`<b>${esc(n.label)}</b>` +
`<div class="k">${esc(n.firm)||'—'}</div>` +
`<div style="margin-top:6px">${n.listings||0} listings here · ${n.total_assets||'?'} total on Crexi</div>` +
+ (n.dbId!=null ? `<div style="margin-top:8px"><a href="/broker.html?id=${encodeURIComponent(n.dbId)}" style="color:var(--gold);font-weight:600">View full profile →</a></div>` : '') +
firmChain +
(contact ? `<div style="margin-top:8px;padding:8px;background:var(--card);border-radius:8px">${contact}</div>` : '<div class="k" style="margin-top:8px;font-size:11px">no contact info enriched</div>') +
(partners.length ? `<div style="margin-top:8px" class="k">co-lists with:</div>${partners.map(p=>'<div>· '+esc(p)+'</div>').join('')}` : '') +
diff --git a/scripts/serve.js b/scripts/serve.js
index 0e3048e..d910441 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -320,7 +320,12 @@ const snapContact = (id) => {
const b = (s.brokers || []).find(x => +x.id === +id); if (!b) return null;
return { id: b.id, name: b.name, title: b.title, phone: b.phone, email: b.email, website: b.website,
linkedin: b.linkedin, office_addr: b.office_addr, total_assets: b.total_assets, agent_type: b.agent_type,
- license: b.license, firm: b.firm, provenance: [], our_listings: [], other_listings: [], source: 'snapshot' };
+ license: b.license || b.dre_license, dre_license: b.dre_license, firm: b.firm,
+ // Extra flat fields the broker.html profile page renders (snapshot mode has no live DB join,
+ // so the listing book stays empty — but the identity/spec/index-date fields all come from the row).
+ specialties: b.specialties, state: b.state, crexi_id: b.crexi_id, listings: b.listings,
+ created_at: b.created_at, enriched_at: b.enriched_at || b.created_at,
+ provenance: [], our_listings: [], other_listings: [], source: b.source || 'snapshot' };
};
app.get('/api/brokers/contact/:id', async (req, res) => {
if (!brokerdb) { const b = snapContact(req.params.id); return b ? res.json(b) : res.status(404).json({ error: 'broker not found' }); }
@@ -331,6 +336,35 @@ app.get('/api/brokers/contact/:id', async (req, res) => {
} catch (e) { const b = snapContact(req.params.id); return b ? res.json(b) : res.status(502).json({ error: String(e.message).split('\n')[0] }); }
});
+// OUR-OWN agent profile by NAME (Steve 2026-08-19: "click any agent -> a page about their
+// listings, built from OUR data, NEVER crexi"). Resolves the broker by name from our graph and
+// returns their contact + the listings WE have for them (broker_listing -> listing). Deliberately
+// omits crexi_id / source / any aggregator link — this is our own record, keyed only by name.
+app.get('/api/agent-profile', async (req, res) => {
+ const name = String((req.query.name || '')).trim();
+ if (!name) return res.status(400).json({ error: 'name required' });
+ if (!brokerdb) return res.json({ name, firm: null, listings: [], found: false, note: 'no local DB' });
+ try {
+ const b = (await brokerdb.pool.query(
+ `SELECT b.id, b.name, f.name AS firm, b.phone, b.email, b.title, b.agent_type, b.license, b.office_addr, b.linkedin
+ FROM broker b LEFT JOIN firm f ON f.id = b.firm_id
+ WHERE lower(b.name) = lower($1)
+ ORDER BY (b.phone IS NOT NULL OR b.email IS NOT NULL) DESC NULLS LAST LIMIT 1`, [name])).rows[0];
+ if (!b) return res.json({ name, firm: null, listings: [], found: false });
+ const listings = (await brokerdb.pool.query(
+ `SELECT l.address, l.city, l.zip, l.type, l.price, l.units, l.cap_rate
+ FROM broker_listing bl JOIN listing l ON l.id = bl.listing_id
+ WHERE bl.broker_id = $1 ORDER BY l.price DESC NULLS LAST`, [b.id])).rows;
+ res.json({
+ found: true, id: b.id, name: b.name, firm: b.firm, phone: b.phone, email: b.email,
+ title: b.title, agent_type: b.agent_type, license: b.license, office: b.office_addr, linkedin: b.linkedin,
+ listings, count: listings.length,
+ total_value: listings.reduce((s, l) => s + Number(l.price || 0), 0),
+ cities: [...new Set(listings.map(l => l.city).filter(Boolean))],
+ });
+ } catch (e) { res.json({ name, firm: null, listings: [], found: false, error: String(e.message).split('\n')[0] }); }
+});
+
// Enrichment coverage rollup (how many brokers now have phone/email/website/linkedin).
const snapEnrich = () => { const s = readBrokerSnap(); return s && s.enrichStats ? s.enrichStats : { unavailable: true }; };
app.get('/api/brokers/enrich-stats', async (req, res) => {
← 2f6ff29 CRCP: paginate Showcase (16 -> 440 LA for-sale listings, ~27
·
back to Commercialrealestate
·
agent profiles: /api/agent-profile?name= (our broker graph - 36bec4d →