← 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 &amp; 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>