[object Object]

← back to Marketing Command Center

clients panel: chip clicks open target in a right-side drawer (website embeds live; LinkedIn/IG show one-click card since they block framing)

922c32fe57e631ed9a2cdf256e74abcc8013b91b · 2026-07-17 08:48:11 -0700 · steve

Files touched

Diff

commit 922c32fe57e631ed9a2cdf256e74abcc8013b91b
Author: steve <steve@designerwallcoverings.com>
Date:   Fri Jul 17 08:48:11 2026 -0700

    clients panel: chip clicks open target in a right-side drawer (website embeds live; LinkedIn/IG show one-click card since they block framing)
---
 public/panels/clients.html | 31 ++++++++++++++++++++++++++++++
 public/panels/clients.js   | 47 ++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 72 insertions(+), 6 deletions(-)

diff --git a/public/panels/clients.html b/public/panels/clients.html
index 90d7db3..a9ecde4 100644
--- a/public/panels/clients.html
+++ b/public/panels/clients.html
@@ -28,6 +28,17 @@
   .cl-note::placeholder { color: #aaa; }
   .cl-note.has-note { border-color: #d4af37; background: #fffaf0; }
   #cl-list.cl-compact .cl-note { font-size: 9.5px; min-height: 20px; padding: 2px 6px; }
+  /* Right slide-in drawer — opens a chip target in-panel instead of a new tab */
+  #cl-scrim { position: fixed; inset: 0; background: rgba(0,0,0,.28); opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 9998; }
+  #cl-scrim.open { opacity: 1; pointer-events: auto; }
+  #cl-drawer { position: fixed; top: 0; right: 0; height: 100vh; width: 46vw; max-width: 780px; min-width: 340px;
+    background: #fff; box-shadow: -8px 0 30px rgba(0,0,0,.18); transform: translateX(100%); transition: transform .22s ease;
+    z-index: 9999; display: flex; flex-direction: column; }
+  #cl-drawer.open { transform: translateX(0); }
+  .cl-dr-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--line,#e6e6e6); }
+  .cl-dr-title { font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+  #cl-dr-frame { flex: 1; border: 0; width: 100%; background: #fafafa; }
+  .cl-dr-fallback { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 28px; gap: 8px; }
 </style>
 <div id="cl-root" style="max-width:960px;">
   <div class="muted-banner" id="cl-banner">Loading client & prospect groups…</div>
@@ -85,3 +96,23 @@
 
   <div id="cl-list"></div>
 </div>
+
+<div id="cl-scrim"></div>
+<div id="cl-drawer" aria-hidden="true">
+  <div class="cl-dr-head">
+    <div style="min-width:0;">
+      <div id="cl-dr-title" class="cl-dr-title"></div>
+      <div id="cl-dr-kind" class="muted" style="font-size:11px;"></div>
+    </div>
+    <span style="flex:1;"></span>
+    <a id="cl-dr-open" class="btn ghost" target="_blank" rel="noopener" style="font-size:11px;padding:4px 9px;">Open ↗</a>
+    <button id="cl-dr-close" class="btn ghost" style="font-size:14px;padding:2px 10px;">✕</button>
+  </div>
+  <iframe id="cl-dr-frame" hidden referrerpolicy="no-referrer" sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe>
+  <div id="cl-dr-fallback" hidden class="cl-dr-fallback">
+    <div style="font-size:34px;">🔗</div>
+    <p id="cl-dr-fb-msg" class="muted" style="font-size:12.5px;max-width:340px;"></p>
+    <code id="cl-dr-fb-url" style="font-size:11px;word-break:break-all;color:#555;"></code>
+    <a id="cl-dr-fb-open" class="btn gold" target="_blank" rel="noopener" style="margin-top:12px;font-size:12px;padding:7px 14px;">Open in a new tab ↗</a>
+  </div>
+</div>
diff --git a/public/panels/clients.js b/public/panels/clients.js
index 3c538da..31e2c59 100644
--- a/public/panels/clients.js
+++ b/public/panels/clients.js
@@ -230,6 +230,35 @@ window.MCC_PANELS['clients'] = {
       document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
     }
 
+    // ── Right-side drawer: open a chip target in-panel instead of a new tab ──
+    // Firm websites embed fine; LinkedIn/Instagram/Google refuse framing
+    // (X-Frame-Options / CSP frame-ancestors), so those show a one-click card.
+    const NO_FRAME = /(^|\.)(linkedin\.com|instagram\.com|google\.[a-z.]+|facebook\.com|twitter\.com|x\.com|threads\.net)$/i;
+    const embeddable = url => { try { return !NO_FRAME.test(new URL(url).hostname.replace(/^www\./, '')); } catch { return false; } };
+    function openDrawer(url, title, kind) {
+      const dr = $('#cl-drawer'); if (!dr) { window.open(url, '_blank', 'noopener'); return; }
+      $('#cl-dr-title').textContent = title || '';
+      $('#cl-dr-kind').textContent = kind || '';
+      $('#cl-dr-open').href = url; $('#cl-dr-fb-open').href = url;
+      const frame = $('#cl-dr-frame'), fb = $('#cl-dr-fallback');
+      if (embeddable(url)) {
+        fb.hidden = true; frame.hidden = false; frame.src = url;
+      } else {
+        frame.hidden = true; frame.src = 'about:blank'; fb.hidden = false;
+        $('#cl-dr-fb-msg').textContent = `${kind || 'This site'} blocks being shown inside another page, so it can’t load here. Open it directly:`;
+        $('#cl-dr-fb-url').textContent = url;
+      }
+      dr.classList.add('open'); $('#cl-scrim').classList.add('open'); dr.setAttribute('aria-hidden', 'false');
+    }
+    function closeDrawer() {
+      const dr = $('#cl-drawer'); if (!dr) return;
+      dr.classList.remove('open'); $('#cl-scrim').classList.remove('open'); dr.setAttribute('aria-hidden', 'true');
+      const f = $('#cl-dr-frame'); if (f) f.src = 'about:blank';
+    }
+    if ($('#cl-dr-close')) $('#cl-dr-close').onclick = closeDrawer;
+    if ($('#cl-scrim')) $('#cl-scrim').onclick = closeDrawer;
+    if (!window.__clDrawerEsc) { window.__clDrawerEsc = true; document.addEventListener('keydown', e => { if (e.key === 'Escape') closeDrawer(); }); }
+
     function render() {
       const d = current; const set = done(d.id);
       const matches = currentMatches();
@@ -270,12 +299,18 @@ window.MCC_PANELS['clients'] = {
           </span>
         </label>`;
       }).join('')}</div>${more > 0 ? `<div class="muted" style="font-size:11px;margin-top:8px;">Showing ${shown.length} of ${matches.length.toLocaleString()} — type in the filter to narrow.</div>` : ''}</div>`;
-      // Stamp the date+time when a LinkedIn/Instagram Follow/message chip is clicked.
-      $('#cl-list').querySelectorAll('.cl-chip.in, .cl-chip.ig').forEach(el => el.addEventListener('click', ev => {
-        ev.stopPropagation();
-        const idx = +el.dataset.idx, ch = el.dataset.ch, rec = shown[idx];
-        const A2 = acts(d.id); (A2[rec.id] = A2[rec.id] || {})[ch] = Date.now(); saveActs(d.id, A2);
-        const span = document.getElementById(`acted-${ch}-${idx}`); if (span) span.textContent = '🕓 ' + fmtWhen(A2[rec.id][ch]);
+      // Chip clicks open the target in the RIGHT DRAWER (not a new tab); in/ig also stamp the date+time.
+      $('#cl-list').querySelectorAll('a.cl-chip.site, a.cl-chip.in, a.cl-chip.ig').forEach(el => el.addEventListener('click', ev => {
+        ev.preventDefault(); ev.stopPropagation();
+        const url = el.getAttribute('href');
+        const isIn = el.classList.contains('in'), isIg = el.classList.contains('ig');
+        if (isIn || isIg) {
+          const idx = +el.dataset.idx, ch = el.dataset.ch, rec = shown[idx];
+          const A2 = acts(d.id); (A2[rec.id] = A2[rec.id] || {})[ch] = Date.now(); saveActs(d.id, A2);
+          const span = document.getElementById(`acted-${ch}-${idx}`); if (span) span.textContent = '🕓 ' + fmtWhen(A2[rec.id][ch]);
+        }
+        const row = el.closest('.cl-row'); const name = row ? (row.querySelector('b') ? row.querySelector('b').textContent : '') : '';
+        openDrawer(url, name, isIn ? 'LinkedIn' : isIg ? 'Instagram' : 'Website');
       }));
       $('#cl-progress').textContent = `${set.size.toLocaleString()} / ${RECS.length.toLocaleString()} contacted`;
       $('#cl-list').querySelectorAll('input[type=checkbox]').forEach(cb => cb.onchange = () => {

← 2aac093 MCC Clients: per-contact notes field below each firm card  ·  back to Marketing Command Center  ·  Vendor IG Reporting: render last 3 posts inline per vendor ( 075ec0c →