← back to Commercialrealestate
public/linkedin.html
118 lines
<!doctype html>
<html lang="en">
<head>
<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='11' text-anchor='middle' fill='%230a66c2'%3Ein%3C/text%3E%3C/svg%3E">
<title>LinkedIn — Follow list</title>
<style>
:root{ --bg:#0c0f15; --panel:#141925; --panel2:#1d2230; --fg:#E6E8EC; --muted:#8A93A2; --gold:#C8A24B;
--active:#70AD47; --draft:#5B9BD5; --in:#0a66c2; --line:#232a38; --del:#E74C3C; }
*{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;}
header{display:flex;align-items:center;gap:14px;padding:14px 22px;border-bottom:1px solid var(--line);background:linear-gradient(180deg,#10141d,#0c0f15);position:sticky;top:0;z-index:10;}
header h1{margin:0;font-size:18px;letter-spacing:.5px;} header h1 b{color:var(--in);}
header .nav{margin-left:auto;display:flex;gap:14px;} header .nav a{color:var(--draft);text-decoration:none;font-size:13px;} header .nav a:hover{color:var(--gold);}
.wrap{padding:18px 22px 50px;max-width:1100px;margin:0 auto;}
.intro{color:var(--muted);font-size:13px;margin-bottom:14px;border-left:2px solid var(--in);padding-left:10px;}
.bar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:14px;}
.bar input[type=text]{flex:1;min-width:220px;background:var(--panel2);border:1px solid var(--line);color:var(--fg);border-radius:9px;padding:9px 12px;font-size:14px;}
.seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;}
.seg button{background:var(--panel2);border:0;color:var(--muted);padding:8px 12px;font-size:12px;cursor:pointer;}
.seg button.on{background:var(--in);color:#fff;}
.count{font-size:12px;color:var(--muted);} .count b{color:var(--active);}
.list{display:flex;flex-direction:column;gap:8px;}
.row{display:flex;align-items:center;gap:12px;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:11px 14px;}
.row.followed{border-color:rgba(112,173,71,.5);background:linear-gradient(90deg,rgba(112,173,71,.07),var(--panel));}
.row .who{flex:1;min-width:0;}
.row .nm{font-weight:600;}
.row .fm{color:var(--muted);font-size:12px;}
.atype{display:inline-block;font-size:10px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;padding:1px 6px;border-radius:9px;margin-left:6px;vertical-align:middle;}
.atype.res{background:rgba(91,155,213,.16);color:var(--draft);border:1px solid rgba(91,155,213,.4);}
.atype.com{background:rgba(200,162,75,.14);color:var(--gold);border:1px solid rgba(200,162,75,.4);}
.lc{font-size:11px;color:var(--muted);white-space:nowrap;}
.open{display:inline-flex;align-items:center;gap:5px;background:var(--in);color:#fff;text-decoration:none;border-radius:8px;padding:7px 12px;font-size:12px;font-weight:600;white-space:nowrap;}
.open.srch{background:var(--panel2);color:var(--draft);border:1px solid var(--line);}
.open:hover{filter:brightness(1.1);}
.fbtn{background:var(--panel2);border:1px solid var(--line);color:var(--muted);border-radius:8px;padding:7px 11px;font-size:12px;cursor:pointer;white-space:nowrap;}
.fbtn.on{background:rgba(112,173,71,.16);border-color:var(--active);color:var(--active);}
.gate{margin-top:16px;font-size:12px;color:var(--muted);border-top:1px solid var(--line);padding-top:12px;}
.empty{color:var(--muted);padding:30px;text-align:center;}
@media(max-width:680px){ header{flex-wrap:wrap;} header .nav{flex-wrap:wrap;gap:10px;width:100%;} .row{flex-wrap:wrap;} .row .lc{order:5;width:100%;} }
</style>
</head>
<body>
<header>
<h1><b>in</b> Follow List</h1>
<div class="nav">
<a href="/" title="Investment Explorer">← Explorer</a>
<a href="/crcp.html" title="Control Panel">🛰️ CRCP</a>
<a href="/brokers.html" title="Broker map">🕸️ Broker map</a>
</div>
</header>
<div class="wrap">
<div class="intro">Every broker & agent in the CRE graph, ready to follow on LinkedIn.
Open each profile and follow manually — this list tracks who you've followed (saved in this browser).
<b>No automated following</b> — LinkedIn's TOS prohibits 3rd-party automation, so this is a links-only playbook.</div>
<div class="bar">
<input type="text" id="q" placeholder="search name or firm… e.g. Marcus & Millichap, Luther">
<div class="seg" id="filt">
<button data-f="all" class="on">All</button>
<button data-f="profile">Has profile</button>
<button data-f="followed">Followed</button>
</div>
<div class="count" id="count">loading…</div>
</div>
<div class="list" id="list"></div>
<div class="gate">⚑ Manual & TOS-compliant: profile/search links only; the followed state is stored locally in this browser.
Actual connecting/messaging happens in LinkedIn by hand (or via Sales Navigator) — never automated here.</div>
</div>
<script>
const $=s=>document.querySelector(s);
const FOLLOW_KEY='crcp_li_followed';
// one-time migration off the legacy persona key
try{const _o=localStorage.getItem('frank_li_followed');if(_o!==null&&localStorage.getItem(FOLLOW_KEY)===null){localStorage.setItem(FOLLOW_KEY,_o);localStorage.removeItem('frank_li_followed');}}catch(_){}
let followed=new Set(JSON.parse(localStorage.getItem(FOLLOW_KEY)||'[]'));
let people=[], filt='all', q='';
const atype=t=>t==='residential'?'<span class="atype res">Residential</span>':'<span class="atype com">Commercial</span>';
const esc=s=>(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
function saveFollowed(){ localStorage.setItem(FOLLOW_KEY, JSON.stringify([...followed])); }
function toggle(id){ id=String(id); followed.has(id)?followed.delete(id):followed.add(id); saveFollowed(); render(); }
function visible(){
return people.filter(p=>{
if(filt==='profile' && !p.linkedin) return false;
if(filt==='followed' && !followed.has(String(p.id))) return false;
return true;
});
}
function render(){
const rows=visible();
$('#count').innerHTML=`<b>${followed.size}</b> followed · ${rows.length} shown · ${people.length} total`;
if(!rows.length){ $('#list').innerHTML='<div class="empty">no matches</div>'; return; }
$('#list').innerHTML=rows.map(p=>{
const isF=followed.has(String(p.id));
const link=p.linkedin
? `<a class="open" href="${esc(p.linkedin)}" target="_blank" rel="noopener noreferrer">in · Open profile</a>`
: `<a class="open srch" href="${esc(p.search)}" target="_blank" rel="noopener noreferrer">🔎 Find on LinkedIn</a>`;
return `<div class="row${isF?' followed':''}">
<div class="who"><span class="nm">${esc(p.name)}</span>${atype(p.agent_type)}<div class="fm">${esc(p.firm||'—')}</div></div>
<span class="lc">${p.listings||0} listing${p.listings===1?'':'s'}</span>
${link}
<button class="fbtn${isF?' on':''}" onclick="toggle('${p.id}')">${isF?'✓ Followed':'+ Follow'}</button>
</div>`;
}).join('');
}
let t;
async function load(){
$('#count').textContent='loading…';
try{ const d=await (await fetch('/api/brokers/linkedin?limit=800'+(q?'&q='+encodeURIComponent(q):''))).json();
people=d.people||[]; render();
}catch(e){ $('#list').innerHTML='<div class="empty">failed to load — is the server up? '+e.message+'</div>'; }
}
$('#q').oninput=()=>{ q=$('#q').value.trim(); clearTimeout(t); t=setTimeout(load,250); };
$('#filt').onclick=e=>{ const b=e.target.closest('button'); if(!b) return;
document.querySelectorAll('#filt button').forEach(x=>x.classList.toggle('on',x===b)); filt=b.dataset.f; render(); };
load();
</script>
</body>
</html>