← back to Rentv 2026
public/admin/pr-intelligence/linkedin-posting.html
127 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>CRE PR Intelligence — LinkedIn Posting</title>
<link rel="stylesheet" href="/admin/pr-intelligence/pr-shared.css">
<style>
.lp-banner{display:flex;align-items:center;gap:10px;border-radius:10px;padding:12px 16px;margin:0 0 18px;
background:#fff6e5;border:1px solid #f0d38a;color:#8a5a00;font-size:13.5px;font-weight:600}
.lp-banner .dot{width:9px;height:9px;border-radius:999px;background:#e0a020;flex:0 0 auto}
.lp-card{border:1px solid var(--line,#e4e7ea);border-radius:10px;padding:18px 20px;margin:0 0 18px;background:var(--card,#fff)}
.lp-card h3{margin:0 0 6px;font-size:15px}
.lp-note{font-size:12.5px;color:var(--muted,#5b636b);margin:8px 0 0}
.lp-check{list-style:none;margin:6px 0 0;padding:0}
.lp-check li{position:relative;padding:9px 0 9px 30px;border-bottom:1px solid var(--line,#eee);font-size:13.5px;line-height:1.5}
.lp-check li:last-child{border-bottom:0}
.lp-check li::before{position:absolute;left:0;top:9px;font-size:15px}
.lp-check li.done::before{content:"✅"} .lp-check li.todo::before{content:"⬜"}
.lp-check code{background:var(--wash,#f5f6f8);padding:1px 5px;border-radius:4px;font-size:12px}
.lp-two{display:grid;grid-template-columns:1fr 1fr;gap:20px} @media(max-width:820px){.lp-two{grid-template-columns:1fr}}
.lp-field{margin:0 0 12px} .lp-field label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted,#5b636b);margin:0 0 4px}
.lp-field select,.lp-field textarea{width:100%;box-sizing:border-box;font:inherit;font-size:13px;padding:8px 10px;border:1px solid var(--line,#d7dde3);border-radius:8px;background:#fff}
.lp-field textarea{min-height:120px;resize:vertical}
/* LinkedIn card preview */
.li-card{border:1px solid #d7dde3;border-radius:10px;overflow:hidden;background:#fff;max-width:100%}
.li-head{display:flex;gap:10px;padding:12px 14px 0}
.li-av{width:44px;height:44px;border-radius:999px;background:#0a66c2;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;flex:0 0 auto}
.li-who b{font-size:13.5px} .li-who span{display:block;font-size:11.5px;color:#5b636b}
.li-body{padding:10px 14px 12px;font-size:13.5px;white-space:pre-wrap;line-height:1.5}
.li-link{border-top:1px solid #eef1f4;background:#f3f6f8}
.li-link img{width:100%;display:block;aspect-ratio:1.91/1;object-fit:cover;background:#e6ebf0}
.li-link .m{padding:10px 14px;font-size:12.5px} .li-link .m b{display:block;font-size:13px;color:#111}
.li-link .m .u{color:#5b636b;font-size:11.5px;margin-top:2px}
.disabled-wrap{position:relative}
.disabled-wrap .btn[disabled]{opacity:.55;cursor:not-allowed}
.lp-gate{font-size:12px;color:#8a5a00;margin-top:8px}
</style>
</head>
<body>
<script src="/admin/pr-intelligence/pr-shared.js"></script>
<script>
(async function () {
const c = PR.frame('linkedin-posting', 'LinkedIn Posting',
'Publish RENTV articles to Steve Boom’s LinkedIn network — the official, TOS-safe way (OAuth, no password). This section is SET UP but NOT LIVE: nothing can post until the LinkedIn app is connected and Steve greenlights it.');
c.insertAdjacentHTML('beforeend',
`<div class="lp-banner"><span class="dot"></span>Status: <b style="margin:0 4px">Parked — pending green light.</b> Posting is disabled. This is the setup + preview surface only; no post can be sent yet.</div>`);
// ── Setup checklist ────────────────────────────────────────────────────────
const setup = document.createElement('div'); setup.className = 'lp-card';
setup.innerHTML = `
<h3>Setup checklist</h3>
<p class="lp-note">One-time setup. Steps 2–4 unlock the composer below. Everything here is TOS-compliant: LinkedIn’s official OAuth + Posts API — we store a revocable token, never Boom’s password.</p>
<ul class="lp-check">
<li class="done"><b>RENTV LinkedIn Company Page</b> — confirmed to exist. LinkedIn requires an app to be linked to a Page, so this prerequisite is met.</li>
<li class="todo"><b>Create a LinkedIn Developer App</b> linked to the RENTV Page → at <a href="https://www.linkedin.com/developers/apps" target="_blank" rel="noopener noreferrer">linkedin.com/developers</a>. Add products <em>“Sign In with LinkedIn using OpenID Connect”</em> + <em>“Share on LinkedIn”</em>. Whitelist redirect URI <code>https://rentv.agentabrams.com/api/pr/linkedin/callback</code>.</li>
<li class="todo"><b>Add the app credentials</b> — hand Steve the <code>client_id</code> + <code>client_secret</code>; they’re routed via the secrets manager (stored server-side, never in the browser).</li>
<li class="todo"><b>Connect & authorize</b> — Boom clicks <em>Connect LinkedIn</em> once and approves the scopes. That mints the revocable access token used to post.</li>
</ul>
<div style="margin-top:14px" class="disabled-wrap">
<button class="btn" id="connect" disabled title="Available once the LinkedIn app is created and credentials are added">🔗 Connect LinkedIn</button>
<div class="lp-gate">⚠ Disabled until the Developer App is created and Steve greenlights go-live.</div>
</div>`;
c.appendChild(setup);
// ── Composer preview (non-live) ────────────────────────────────────────────
const comp = document.createElement('div'); comp.className = 'lp-card';
comp.innerHTML = `
<h3>Composer — preview</h3>
<p class="lp-note">This is exactly what a post will look like. It pulls real RENTV articles so you can see the flow, but the <b>Post</b> button is inert until connected.</p>
<div class="lp-two" style="margin-top:14px">
<div>
<div class="lp-field"><label>RENTV article</label><select id="art"></select></div>
<div class="lp-field"><label>Caption</label><textarea id="cap"></textarea></div>
<div class="disabled-wrap">
<button class="btn" id="post" disabled title="Connect LinkedIn to enable posting">📣 Post to LinkedIn</button>
<button class="btn" id="queue" disabled title="Connect LinkedIn to enable queuing">🗓️ Add to queue</button>
<div class="lp-gate">⚠ Draft-and-approve by design — nothing auto-posts. Both actions stay disabled until go-live.</div>
</div>
</div>
<div>
<label style="display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted,#5b636b);margin:0 0 4px">Preview</label>
<div class="li-card" id="preview"></div>
</div>
</div>`;
c.appendChild(comp);
// ── Wire the (read-only) preview ───────────────────────────────────────────
const origin = location.origin; // NEVER rentv.com — always the RENTV site's own article URL
let articles = [];
try {
const j = await fetch('/api/news').then((r) => r.json());
articles = (j.items || j || []).slice(0, 30).map((a) => ({
id: a.id, title: a.title, cat: a.cat || '', image: a.image || '',
url: origin + '/article?id=' + encodeURIComponent(a.id), // site article page, not rentv.com
}));
} catch (e) { /* preview degrades gracefully */ }
const sel = document.getElementById('art'), cap = document.getElementById('cap'), preview = document.getElementById('preview');
if (!articles.length) { articles = [{ id: '0', title: 'RENTV article headline', cat: 'Multifamily', image: '', url: origin + '/article?id=0' }]; }
sel.innerHTML = articles.map((a, i) => `<option value="${i}">${PR.esc(a.title.slice(0, 80))}</option>`).join('');
function caption(a) {
const tag = (a.cat || '').replace(/[^a-z0-9]/gi, '');
return `🏢 ${a.title}\n\nThe latest in commercial real estate across the Western U.S. — read it on RENTV.\n\n#CommercialRealEstate #CRE${tag ? ' #' + tag : ''}`;
}
function render() {
const a = articles[+sel.value] || articles[0];
if (document.activeElement !== cap) cap.value = caption(a);
const img = a.image ? (a.image.startsWith('http') ? a.image : origin + a.image) : '';
preview.innerHTML = `
<div class="li-head"><div class="li-av">SB</div>
<div class="li-who"><b>Steve Boom</b><span>RENTV — Commercial Real Estate News · Now</span></div></div>
<div class="li-body">${PR.esc(cap.value)}</div>
<div class="li-link">
${img ? `<img src="${PR.esc(img)}" alt="" onerror="this.style.display='none'">` : ''}
<div class="m"><b>${PR.esc(a.title)}</b><div class="u">${PR.esc(a.url)}</div></div>
</div>`;
}
sel.addEventListener('change', () => { cap.value = ''; render(); });
cap.addEventListener('input', render);
render();
})();
</script>
</body>
</html>