← 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
M public/panels/assets.js
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 →