← back to Commercialrealestate

public/licensed.html

120 lines

<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Licensed Directory — CRCP</title>
<style>
:root{--bg:#0f1216;--card:#161b22;--line:#262d38;--ink:#e6e9ef;--mut:#8a93a3;--blue:#4c8dff;--good:#3fb984;--warn:#d8a24a;--cols:4;}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,Segoe UI,Roboto,sans-serif}
a{color:var(--blue);text-decoration:none}
header{position:sticky;top:0;z-index:5;background:var(--bg);border-bottom:1px solid var(--line);padding:10px 16px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
h1{font-size:17px;margin:0;font-weight:600}.sub{color:var(--mut);font-size:12px}
.tabs{display:flex;gap:6px}.tab{padding:5px 11px;border:1px solid var(--line);border-radius:20px;background:var(--card);color:var(--ink);cursor:pointer;font-size:12.5px}
.tab.on{background:var(--blue);border-color:var(--blue);color:#fff}
.ctrl{display:flex;gap:10px;align-items:center;margin-left:auto;flex-wrap:wrap}
input,select{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:6px 9px;font-size:13px}
input#q{min-width:230px}
.rng{display:flex;align-items:center;gap:6px;color:var(--mut);font-size:12px}
.wrap{padding:14px 16px}.count{color:var(--mut);font-size:12px;margin-bottom:10px}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px}
.card{border:1px solid var(--line);border-radius:12px;background:var(--card);padding:12px;display:flex;flex-direction:column;gap:7px}
.ctop{display:flex;justify-content:space-between;gap:8px;align-items:flex-start}
.nm{font-weight:600;font-size:14px;line-height:1.25}
.kind{font-size:10px;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:20px;border:1px solid var(--line);color:var(--mut);white-space:nowrap}
.kind.firm{color:var(--blue);border-color:var(--blue)}.kind.title{color:var(--warn);border-color:var(--warn)}.kind.escrow{color:var(--good);border-color:var(--good)}.kind.agent{color:var(--mut)}
.meta{color:var(--mut);font-size:12px}
.actions{display:flex;gap:7px;margin-top:2px}
.btn{background:transparent;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:5px 10px;font-size:12px;cursor:pointer}.btn:hover{border-color:var(--blue)}
.chip{font-size:11px;color:var(--good);border:1px solid var(--good);border-radius:20px;padding:2px 8px}
/* firm-IG inline strip (link-free) */
.figwrap{margin-top:4px;display:flex;flex-direction:column;gap:4px}
.fighd{font-size:11px;color:var(--mut)}
.figli{display:flex;gap:8px;align-items:center;padding:5px 6px;border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.02)}
.figt{width:34px;height:34px;flex:0 0 34px;border-radius:6px;object-fit:cover;background:var(--line);display:inline-block}
.figb{display:flex;flex-direction:column;min-width:0;gap:1px}
.figc{font-size:11.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.figm{font-size:10px;color:var(--mut);font-variant-numeric:tabular-nums}
.empty{color:var(--mut);padding:40px;text-align:center}
</style></head><body>
<header>
  <div><h1>📋 Licensed Directory <span class="sub" id="scope"></span></h1></div>
  <div class="tabs" id="tabs"></div>
  <div class="ctrl">
    <input id="q" placeholder="Search name / firm / license #…">
    <select id="sort">
      <option value="name">Name A→Z</option>
      <option value="count">Most agents</option>
      <option value="recent">License newest</option>
    </select>
    <span class="rng">Density <input type="range" id="dens" min="2" max="7" value="4"></span>
    <a href="/" class="sub">← CRCP</a>
  </div>
</header>
<div class="wrap"><div class="count" id="count">Loading…</div><div class="grid" id="grid"></div></div>
<script>
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const $=s=>document.querySelector(s);
let DATA={agents:[],firms:[],title:[],escrow:[]}, FIRM_IG={}, FIRM_IDX={}, CONTACTS={};
let tab='firms', sort=localStorage.getItem('ld_sort')||'name';
const normFirm=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]/g,'');
function firmHandle(firm){const n=normFirm(firm);if(!n)return null;if(FIRM_IDX[n])return FIRM_IDX[n];
  for(const[a,h]of Object.entries(FIRM_IDX)){if(n===a||(a.length>=5&&n.includes(a))||(a.length>=3&&n.startsWith(a)))return h;}return null;}
function igStrip(firm){const h=firmHandle(firm);if(!h)return '';const c=FIRM_IG[h];if(!c||!c.posts||!c.posts.length)return '';
  const items=c.posts.slice(0,3).map(p=>{const cap=esc((p.caption||'').replace(/\s+/g,' ').slice(0,80))||'(no caption)';
    const when=p.timestamp?String(p.timestamp).slice(0,10):'';const vid=p.media_type==='VIDEO'?'▶ ':'';
    const thumb=p.image?`<img class="figt" loading="lazy" src="${esc(p.image)}" alt="">`:'<span class="figt"></span>';
    return `<div class="figli">${thumb}<span class="figb"><span class="figc">${vid}${cap}</span><span class="figm">${when?when+' · ':''}♥ ${p.likes||0} · 💬 ${p.comments||0}</span></span></div>`;}).join('');
  return `<div class="figwrap"><div class="fighd">📷 @${esc(h)} · firm’s latest 3</div>${items}</div>`;}

async function boot(){
  const [ld,cache,map,contacts]=await Promise.all([
    fetch('data/license-directory.json').then(r=>r.json()),
    fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
    fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}})),
    fetch('/api/contacts').then(r=>r.json()).then(j=>j.contacts||{}).catch(()=>({})),
  ]);
  DATA={agents:ld.agents||[],firms:ld.firms||[],title:ld.title||[],escrow:ld.escrow||[]};
  FIRM_IG=cache; CONTACTS=contacts;
  for(const[h,al]of Object.entries(map.handles||{}))for(const a of al)FIRM_IDX[a]=h;
  $('#scope').textContent=`· ${(ld.scope&&ld.scope.state)||'CA'} · ${(ld.scope&&ld.scope.metros||[]).join(', ')}`;
  const T=[['firms','Firms',DATA.firms.length],['title','Title',DATA.title.length],['escrow','Escrow',DATA.escrow.length],['agents','Agents',DATA.agents.length]];
  $('#tabs').innerHTML=T.map(([k,l,n])=>`<span class="tab${k===tab?' on':''}" data-tab="${k}">${l} <b>${n}</b></span>`).join('');
  $('#tabs').onclick=e=>{const t=e.target.closest('[data-tab]');if(!t)return;tab=t.dataset.tab;document.querySelectorAll('.tab').forEach(x=>x.classList.toggle('on',x.dataset.tab===tab));render();};
  $('#q').oninput=render; $('#sort').value=sort; $('#sort').onchange=e=>{sort=e.target.value;localStorage.setItem('ld_sort',sort);render();};
  const dens=$('#dens'); dens.value=localStorage.getItem('ld_dens')||4; document.documentElement.style.setProperty('--cols',dens.value);
  dens.oninput=e=>{document.documentElement.style.setProperty('--cols',e.target.value);localStorage.setItem('ld_dens',e.target.value);};
  render();
}
function render(){
  const q=$('#q').value.trim().toLowerCase();
  let rows=DATA[tab]||[];
  if(q) rows=rows.filter(r=>((r.name||'')+' '+(r.firm||'')+' '+(r.license_number||'')).toLowerCase().includes(q));
  rows=[...rows].sort((a,b)=> sort==='count'?((b.agent_count||0)-(a.agent_count||0))
    : sort==='recent'?String(b.expiration||'').localeCompare(String(a.expiration||''))
    : String(a.name||'').localeCompare(String(b.name||'')));
  const CAP=300, shown=rows.slice(0,CAP);
  $('#count').textContent=`${rows.length.toLocaleString()} ${tab}${rows.length>CAP?` · showing first ${CAP} (search to narrow)`:''}`;
  $('#grid').innerHTML = shown.length? shown.map(card).join('') : '<div class="empty">No matches.</div>';
}
function card(r){
  const id=r.id, c=CONTACTS[id]||{};
  const firmForIg = r.kind==='agent' ? r.firm : r.name;
  const metaBits=[];
  if(r.kind==='firm') metaBits.push(`${r.agent_count} licensed agent${r.agent_count===1?'':'s'}`, r.metro);
  if(r.kind==='agent') metaBits.push(r.firm||'—', r.license_type, r.license_number?('#'+r.license_number):'', r.city);
  if(r.kind==='title'||r.kind==='escrow') metaBits.push(r.underwriter?('underwriter: '+r.underwriter):'deal-side');
  const contacted = c.contacted_at?`<span class="chip">✓ ${esc(String(c.contacted_at).slice(0,10))}</span>`:'';
  return `<div class="card" data-id="${esc(id)}">
    <div class="ctop"><div class="nm">${esc(r.name)}</div><span class="kind ${r.kind}">${r.kind}</span></div>
    <div class="meta">${metaBits.filter(Boolean).map(esc).join(' · ')}</div>
    ${igStrip(firmForIg)}
    <div class="actions"><button class="btn mark" data-id="${esc(id)}">${c.contacted_at?'↻ Update':'✓ Mark contacted'}</button>${contacted}</div>
  </div>`;
}
document.addEventListener('click',async e=>{
  const b=e.target.closest('.mark'); if(!b) return;
  b.disabled=true;
  try{ const j=await fetch('/api/contacts/'+encodeURIComponent(b.dataset.id)+'/contacted',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(r=>r.json());
    if(j.contact){ CONTACTS[b.dataset.id]=j.contact; render(); } }catch{} finally{ b.disabled=false; }
});
boot().catch(e=>{$('#count').textContent='Failed to load: '+e.message;});
</script></body></html>