[object Object]

← back to Marketing Command Center

Image Bank: finish Repost UI — gated modal (image preview, required @credit, rights-confirm + #ad, wallpaper-ban) → stages via /repost, never auto-posts

0c63f9fc27e1f518c9a18322e44731bf52a045cb · 2026-06-13 09:45:17 -0700 · Steve Abrams

Files touched

Diff

commit 0c63f9fc27e1f518c9a18322e44731bf52a045cb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jun 13 09:45:17 2026 -0700

    Image Bank: finish Repost UI — gated modal (image preview, required @credit, rights-confirm + #ad, wallpaper-ban) → stages via /repost, never auto-posts
---
 public/panels/assets.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 75 insertions(+)

diff --git a/public/panels/assets.js b/public/panels/assets.js
index a0dd34f..c8b6714 100644
--- a/public/panels/assets.js
+++ b/public/panels/assets.js
@@ -66,6 +66,7 @@ window.MCC_PANELS['assets'] = {
       libGrid.innerHTML = rows.map(cardHtml).join('');
       libGrid.querySelectorAll('[data-del]').forEach(b => b.addEventListener('click', () => del(b.dataset.del)));
       libGrid.querySelectorAll('[data-copy]').forEach(b => b.addEventListener('click', () => copyUrl(b.dataset.copy)));
+      libGrid.querySelectorAll('[data-repost]').forEach(b => b.addEventListener('click', () => openRepost(b.dataset.repost)));
     }
     async function loadLibrary() {
       try { allAssets = (await (await fetch('/api/assets/list')).json()).assets || []; }
@@ -91,6 +92,7 @@ window.MCC_PANELS['assets'] = {
           </div>
           <div style="display:flex;gap:6px;margin-top:3px;">
             <button class="btn ghost" data-copy="${esc(abs)}" style="font-size:11px;padding:5px 9px;flex:1;">Copy URL</button>
+            <button class="btn ghost" data-repost="${esc(a.id)}" title="Repost to DW socials (gated)" style="font-size:11px;padding:5px 9px;">↪ Repost</button>
             <button class="btn ghost" data-del="${esc(a.id)}" title="Delete" style="font-size:11px;padding:5px 9px;color:#c0563f;">✕</button>
           </div>
         </div>
@@ -314,6 +316,79 @@ window.MCC_PANELS['assets'] = {
       } catch (e) { setStatus('CC import failed: ' + e.message, true); }
     });
 
+    // ── Repost (gated, stage-only) modal ────────────────────────────────────────
+    const KRAVET_RE = /kravet|lee\s*jofa|groundworks|brunschwig|cole.*son|gp.*baker|colefax|clarke|mulberry|baker lifestyle|andrew martin|nicolette mayer|barclay butera|thom filicia/i;
+    const deWP = s => String(s || '').replace(/\bwallpapers\b/gi, 'wallcoverings').replace(/\bwallpaper\b/gi, 'wallcovering');
+    const modal = document.createElement('div');
+    modal.id = 'as-repost-modal';
+    modal.style.cssText = 'display:none;position:fixed;inset:0;background:rgba(15,12,10,.5);z-index:60;align-items:center;justify-content:center;padding:20px;';
+    modal.innerHTML = `<div style="background:#fff;border-radius:14px;max-width:520px;width:100%;max-height:90vh;overflow:auto;box-shadow:0 20px 60px rgba(0,0,0,.3);">
+      <div style="padding:16px 20px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;">
+        <b style="font:600 17px/1.2 'Cormorant Garamond',Georgia,serif;flex:1;">Repost to DW socials</b>
+        <button id="rp-x" class="btn ghost" style="padding:4px 10px;">✕</button>
+      </div>
+      <div style="padding:18px 20px;">
+        <div style="display:flex;gap:12px;margin-bottom:12px;">
+          <img id="rp-img" style="width:96px;height:96px;object-fit:cover;border-radius:10px;background:#e8e3da;flex:none;">
+          <div style="flex:1;min-width:0;">
+            <div id="rp-name" style="font-weight:600;font-size:13px;margin-bottom:6px;"></div>
+            <div id="rp-warn" class="muted" style="font-size:11.5px;"></div>
+          </div>
+        </div>
+        <label>Source @handle to credit <span class="muted" style="font-weight:400;">(required — auto-added to caption)</span></label>
+        <input id="rp-handle" type="text" placeholder="@vendor">
+        <label style="margin-top:10px;">Caption</label>
+        <textarea id="rp-caption" rows="4" placeholder="Your DW caption… (“Wallpaper” → wallcovering; credit + #ad added automatically)"></textarea>
+        <label style="display:flex;align-items:center;gap:8px;margin-top:12px;font-weight:600;color:var(--ink);">
+          <input id="rp-rights" type="checkbox" style="width:auto;"> I confirm DW has the right to repost this image</label>
+        <label style="display:flex;align-items:center;gap:8px;margin-top:8px;font-weight:600;color:var(--ink);">
+          <input id="rp-paid" type="checkbox" style="width:auto;"> Paid / affiliate relationship (adds #ad)</label>
+        <div class="muted" style="font-size:11px;margin-top:10px;background:var(--cream);border-radius:8px;padding:8px 10px;">
+          🔒 Stages only — never auto-posts. Before any live publish: the settlement image gate runs, you give final approval, and a connected Meta token is required.</div>
+        <div style="display:flex;gap:8px;align-items:center;margin-top:14px;">
+          <button id="rp-stage" class="btn gold">Stage repost</button>
+          <span id="rp-msg" class="muted" style="font-size:12px;"></span>
+        </div>
+      </div>
+    </div>`;
+    root.appendChild(modal);
+    const rp = s => modal.querySelector(s);
+    const closeRepost = () => { modal.style.display = 'none'; };
+    rp('#rp-x').onclick = closeRepost;
+    modal.addEventListener('click', e => { if (e.target === modal) closeRepost(); });
+    let rpAsset = null;
+    function openRepost(id) {
+      rpAsset = allAssets.find(a => a.id === id); if (!rpAsset) return;
+      const handle = (rpAsset.tags || []).find(t => /^@/.test(t)) || '';
+      rp('#rp-img').src = rpAsset.src;
+      rp('#rp-name').textContent = rpAsset.name || '';
+      rp('#rp-handle').value = handle;
+      rp('#rp-caption').value = deWP(`${rpAsset.name || ''} — available at Designer Wallcoverings.`);
+      rp('#rp-rights').checked = false; rp('#rp-paid').checked = false;
+      rp('#rp-msg').textContent = '';
+      rp('#rp-warn').innerHTML = handle && KRAVET_RE.test(handle)
+        ? `<span style="color:#9a5a5a;font-weight:600;">⚠ ${esc(handle)} is brand-controlled (Kravet family) — reposting is blocked.</span>`
+        : ((rpAsset.tags || []).includes('uncleared') ? 'Pulled image — confirm rights before reposting.' : '');
+      modal.style.display = 'flex';
+    }
+    rp('#rp-stage').onclick = async () => {
+      const body = {
+        id: rpAsset && rpAsset.id,
+        caption: rp('#rp-caption').value,
+        sourceHandle: rp('#rp-handle').value.trim(),
+        rightsConfirmed: rp('#rp-rights').checked,
+        paid: rp('#rp-paid').checked,
+      };
+      rp('#rp-msg').textContent = 'Staging…';
+      try {
+        const r = await fetch('/api/assets/repost', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+        const d = await r.json();
+        if (!r.ok) { rp('#rp-msg').textContent = '✗ ' + (d.error || r.status); rp('#rp-msg').style.color = '#c0563f'; return; }
+        rp('#rp-msg').textContent = '✓ Staged (not posted). ' + (d.note || ''); rp('#rp-msg').style.color = 'var(--mut)';
+        setTimeout(closeRepost, 2200);
+      } catch (e) { rp('#rp-msg').textContent = '✗ ' + e.message; rp('#rp-msg').style.color = '#c0563f'; }
+    };
+
     // ── boot ───────────────────────────────────────────────────────────────────
     loadLibrary();
   },

← 923cb9c LinkedIn module (DTD verdict A): draft-first composer w/ DW  ·  back to Marketing Command Center  ·  CC Phase 1 (DTD verdict A): CSV import (contacts/campaigns/o 54fd8bb →