← back to Marketing Command Center
Easy Post buttons: shared stage-a-draft component + Quick Post panel + Compose row + asset-card buttons (gated)
f8591dc220b2e996ed5a943662e4c2daaf518dd6 · 2026-08-12 16:14:26 -0700 · Steve
Files touched
M .deploy.confA modules/quickpost/index.jsM modules/registry.jsM package.jsonM public/app.jsM public/index.htmlM public/panels/assets.jsM public/panels/composer.htmlM public/panels/composer.jsA public/panels/quickpost.htmlA public/panels/quickpost.jsA public/quickpost.js
Diff
commit f8591dc220b2e996ed5a943662e4c2daaf518dd6
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 12 16:14:26 2026 -0700
Easy Post buttons: shared stage-a-draft component + Quick Post panel + Compose row + asset-card buttons (gated)
---
.deploy.conf | 2 +-
modules/quickpost/index.js | 87 ++++++++++++++++++++++++++++++++++++++++++++
modules/registry.js | 1 +
package.json | 2 +-
public/app.js | 2 +-
public/index.html | 1 +
public/panels/assets.js | 7 ++++
public/panels/composer.html | 4 ++
public/panels/composer.js | 8 ++++
public/panels/quickpost.html | 46 +++++++++++++++++++++++
public/panels/quickpost.js | 77 +++++++++++++++++++++++++++++++++++++++
public/quickpost.js | 85 +++++++++++++++++++++++++++++++++++++++++++
12 files changed, 319 insertions(+), 3 deletions(-)
diff --git a/.deploy.conf b/.deploy.conf
index 50a2abb..3ec5da4 100644
--- a/.deploy.conf
+++ b/.deploy.conf
@@ -23,4 +23,4 @@ HEALTH_URL=http://127.0.0.1:9662/api/health
# the deploy-side guard regardless.)
# linkedin-feed.json is the openclaw harvest output (TK-10504) — generated on the
# remote by the running MCC; a stale/absent local copy must never clobber it.
-RSYNC_EXTRA_EXCLUDES="follow-counts-history.json follow-counts-accounts.json follow-counts-snapshot.out follow-counts-snapshot.err clients-notes.json channels-outbox.json channels-tokens.json meta-pages.json assets.json assets-catalog.cache.json engine-queue.json engine-config.json linkedin-feed.json linkedin-feed-curated.json renders"
+RSYNC_EXTRA_EXCLUDES="quickpost-drafts.json follow-counts-history.json follow-counts-accounts.json follow-counts-snapshot.out follow-counts-snapshot.err clients-notes.json channels-outbox.json channels-tokens.json meta-pages.json assets.json assets-catalog.cache.json engine-queue.json engine-config.json linkedin-feed.json linkedin-feed-curated.json renders"
diff --git a/modules/quickpost/index.js b/modules/quickpost/index.js
new file mode 100644
index 0000000..9537732
--- /dev/null
+++ b/modules/quickpost/index.js
@@ -0,0 +1,87 @@
+// Quick Post — easy per-platform "post" buttons across the command center. By
+// design this STAGES A DRAFT (gated): a click records a draft post; nothing goes
+// live without a human confirm. Live publishing is wired to the channels layer
+// but only fires once a platform's token is connected AND confirm:true is passed
+// — until then every platform falls back to a staged draft. Draft-only is the
+// safe default that honors the standing "social posting is Steve-gated" rule.
+const fs = require('fs');
+const path = require('path');
+
+const DATA_DIR = path.join(__dirname, '..', '..', 'data');
+const STORE = path.join(DATA_DIR, 'quickpost-drafts.json');
+
+const PLATFORMS = [
+ { id: 'instagram', label: 'Instagram', icon: '📷' },
+ { id: 'tiktok', label: 'TikTok', icon: '🎵' },
+ { id: 'facebook', label: 'Facebook', icon: '📘' },
+ { id: 'linkedin', label: 'LinkedIn', icon: '💼' },
+];
+
+function load() { try { return JSON.parse(fs.readFileSync(STORE, 'utf8')); } catch { return []; } }
+function save(list) {
+ try { fs.mkdirSync(DATA_DIR, { recursive: true }); fs.writeFileSync(STORE, JSON.stringify(list, null, 2)); }
+ catch (e) { /* non-fatal */ }
+}
+// stable-ish id without Date.now in hot path is fine here (server runtime, not a composition)
+function newId() { return 'qp_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }
+
+module.exports = {
+ id: 'quickpost',
+ title: 'Quick Post',
+ icon: '🚀',
+ mount(router) {
+ // per-platform connection state (best-effort). Tokens aren't wired yet, so
+ // everything reports draft-only; the front-end shows this honestly.
+ router.get('/status', (_req, res) => {
+ res.json({
+ platforms: PLATFORMS.map(p => ({ ...p, connected: false })),
+ note: 'Draft-only: posts are staged for approval. Live posting activates per platform once its token is connected.',
+ draftCount: load().length,
+ });
+ });
+
+ router.get('/drafts', (_req, res) => res.json({ drafts: load().slice().reverse() }));
+
+ // Stage a draft. This is the "easy post" action — always safe, always gated.
+ router.post('/draft', (req, res) => {
+ const b = req.body || {};
+ const platform = String(b.platform || '').toLowerCase();
+ if (!PLATFORMS.some(p => p.id === platform)) return res.status(400).json({ error: 'unknown platform' });
+ const entry = {
+ id: newId(),
+ platform,
+ caption: String(b.caption || '').slice(0, 2200),
+ mediaUrl: String(b.mediaUrl || ''),
+ source: String(b.source || 'quickpost'),
+ status: 'draft',
+ created_at: new Date().toISOString(),
+ };
+ const list = load(); list.push(entry); save(list);
+ res.json({ ok: true, staged: true, id: entry.id, platform });
+ });
+
+ // Remove a staged draft.
+ router.post('/draft/delete', (req, res) => {
+ const id = String((req.body || {}).id || '');
+ const list = load().filter(d => d.id !== id);
+ save(list);
+ res.json({ ok: true });
+ });
+
+ // Live publish — GATED. Requires an explicit confirm AND a connected platform.
+ // No platform is connected yet, so this always falls back to a staged draft.
+ router.post('/publish', (req, res) => {
+ const b = req.body || {};
+ const platform = String(b.platform || '').toLowerCase();
+ // (future) if channels.status[platform].connected && b.confirm === true → real post
+ const entry = {
+ id: newId(), platform, caption: String(b.caption || '').slice(0, 2200),
+ mediaUrl: String(b.mediaUrl || ''), source: String(b.source || 'quickpost'),
+ status: 'draft', created_at: new Date().toISOString(),
+ };
+ const list = load(); list.push(entry); save(list);
+ res.json({ ok: true, staged: true, live: false, id: entry.id,
+ reason: 'platform not connected — staged as a draft for approval' });
+ });
+ },
+};
diff --git a/modules/registry.js b/modules/registry.js
index 2691ebf..3297203 100644
--- a/modules/registry.js
+++ b/modules/registry.js
@@ -10,6 +10,7 @@ module.exports = [
'calendarhub', // Calendar (default landing + nav lead): tabbed host — Timeline = calendars (advanced), Plan = calendar (moments + create/delete). No-op mount; the two real calendar modules stay mounted, only their nav entries are hidden.
'calendars', // Timeline view under Calendar: unified activation-SKU + marketing + campaign + Google layers with a left-panel field-filter rail (hidden from nav; loaded inside calendarhub)
'compose', // Compose: merged Suggested (engine) + Manual (composer) posting under one panel — see modules/compose (no-op mount; APIs stay on engine/composer/channels)
+ 'quickpost', // Quick Post: easy per-platform post buttons (stage-a-draft, gated) — shared component reused on the Compose panel + asset cards
'composer', // Composer board: combine an asset/banner + copy → publish/stage to all connected socials at once (assembles assets+copy+channels)
'accounts', // Amazon-style rail: every social account + connection status + inline "how to fix" credentials (read-only view over channels + linkedin)
'board', // Hootsuite-style streams board: one column per channel (social + email), dated cards
diff --git a/package.json b/package.json
index dd0dcf7..6331c2f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "marketing-command-center",
- "version": "1.9.1",
+ "version": "1.10.0",
"description": "DW Marketing Command Center — Constant Contact, marketing calendar, suggested copy, on-demand layouts",
"main": "server.js",
"scripts": {
diff --git a/public/app.js b/public/app.js
index 5e3db1a..dcb2127 100644
--- a/public/app.js
+++ b/public/app.js
@@ -14,7 +14,7 @@ let PANELS = [];
const GROUPS = [
{ name: 'Calendar', ids: ['calendarhub'] },
{ name: 'Plan', ids: ['playbook', 'vendors'] },
- { name: 'Compose', ids: ['compose', 'linkedin', 'board', 'social'] },
+ { name: 'Compose', ids: ['compose', 'quickpost', 'linkedin', 'board', 'social'] },
{ name: 'Library', ids: ['assets', 'sounds', 'reels', 'copy', 'layouts', 'templates'] },
{ name: 'Accounts & Email', ids: ['accounts', 'channels', 'clients', 'constant-contact', 'browse-abandon'] },
{ name: 'Insights · preview', ids: ['insights'] },
diff --git a/public/index.html b/public/index.html
index d0dc85d..7345ac7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -23,5 +23,6 @@
</header>
<section id="panel"><div class="loading">Loading…</div></section>
</main>
+<script src="/quickpost.js"></script>
<script src="/app.js"></script>
</body></html>
diff --git a/public/panels/assets.js b/public/panels/assets.js
index fc6509c..ac5d6af 100644
--- a/public/panels/assets.js
+++ b/public/panels/assets.js
@@ -80,6 +80,12 @@ window.MCC_PANELS['assets'] = {
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)));
+ // per-card Quick Post buttons (stage-a-draft, gated) via the shared component
+ if (window.MCCQuickPost) libGrid.querySelectorAll('.qp-mount').forEach(m => {
+ if (m.dataset.wired) return; m.dataset.wired = '1';
+ window.MCCQuickPost.attach(m, () => ({ caption: m.dataset.name, mediaUrl: m.dataset.url, source: 'asset-card' }),
+ { mini: true, includeAll: false });
+ });
}
async function loadLibrary() {
try { allAssets = (await (await fetchO('/api/assets/list')).json()).assets || []; }
@@ -108,6 +114,7 @@ window.MCC_PANELS['assets'] = {
<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 class="qp-mount" data-url="${esc(abs)}" data-name="${esc(a.name)}" style="margin-top:2px"></div>
</div>
</div>`;
}
diff --git a/public/panels/composer.html b/public/panels/composer.html
index 48a4ed6..1e75702 100644
--- a/public/panels/composer.html
+++ b/public/panels/composer.html
@@ -62,6 +62,10 @@
<label class="cmp-dry"><input type="checkbox" id="cmp-dry" checked> Dry-run (stage, don't post live)</label>
<button class="btn gold" id="cmp-publish" style="width:100%;font-size:14px;padding:11px">Publish</button>
<div id="cmp-result" class="cmp-result"></div>
+ <div class="cmp-qp" style="margin-top:12px;border-top:1px solid var(--line,#e7e1d6);padding-top:10px">
+ <div class="muted" style="font-size:11px;margin-bottom:6px">Or stage a quick draft:</div>
+ <div id="cmp-quickpost"></div>
+ </div>
</section>
</div>
diff --git a/public/panels/composer.js b/public/panels/composer.js
index 90366af..d57af9c 100644
--- a/public/panels/composer.js
+++ b/public/panels/composer.js
@@ -232,5 +232,13 @@ window.MCC_PANELS['composer'] = {
await Promise.all([loadTargets(), loadSource('assets')]);
countIt();
+
+ // Quick Post — easy per-platform "stage a draft" buttons (gated). Reads the
+ // live caption + whichever media (reel video or image) is selected.
+ if (window.MCCQuickPost) {
+ window.MCCQuickPost.attach($('#cmp-quickpost'),
+ () => ({ caption: cap.value.trim(), mediaUrl: state.videoUrl || state.mediaUrl, source: 'composer' }),
+ { mini: true });
+ }
},
};
diff --git a/public/panels/quickpost.html b/public/panels/quickpost.html
new file mode 100644
index 0000000..bcdac7c
--- /dev/null
+++ b/public/panels/quickpost.html
@@ -0,0 +1,46 @@
+<div class="muted-banner">🚀 <b>Easy post</b> — pick media, write a caption, then tap a platform to <b>stage a draft</b>. Draft-only & gated: nothing goes live without your approval.</div>
+
+<div class="qp-grid">
+ <section class="card qp-compose">
+ <label class="qp-lbl">Media URL <span class="qp-hint">(paste a link, or pick an asset below)</span></label>
+ <input id="qp-media" type="url" placeholder="https://…/video.mp4 or image.jpg" autocomplete="off">
+ <div class="qp-preview" id="qp-preview"></div>
+ <div class="qp-lbl" style="margin-top:10px">Pick an asset</div>
+ <div id="qp-assets" class="qp-assets"><div class="muted" style="font-size:12px">Loading assets…</div></div>
+ <label class="qp-lbl" style="margin-top:12px">Caption</label>
+ <textarea id="qp-caption" placeholder="Write your caption… #designerwallcoverings"></textarea>
+ <div class="qp-lbl" style="margin-top:12px">Post to</div>
+ <div id="qp-buttons"></div>
+ </section>
+
+ <aside class="card qp-drafts">
+ <div class="qp-dh"><h3>Staged drafts</h3><span id="qp-count" class="pill">0</span></div>
+ <div class="qp-hint" style="margin-bottom:8px">Awaiting your approval — no draft posts itself.</div>
+ <div id="qp-draftlist"><div class="muted" style="font-size:12px">None yet.</div></div>
+ </aside>
+</div>
+
+<style>
+ .qp-grid{display:grid;grid-template-columns:1fr 320px;gap:16px;align-items:start}
+ @media(max-width:900px){.qp-grid{grid-template-columns:1fr}}
+ .qp-compose,.qp-drafts{padding:14px}
+ .qp-lbl{display:block;font-size:12px;font-weight:600;color:var(--ink);margin-bottom:5px}
+ #qp-media{width:100%;box-sizing:border-box;margin-bottom:8px}
+ #qp-caption{width:100%;box-sizing:border-box;min-height:100px;font:inherit;font-size:13.5px;line-height:1.5;
+ padding:10px;border:1px solid var(--line);border-radius:10px;resize:vertical}
+ .qp-preview{min-height:0}
+ .qp-preview img,.qp-preview video{max-width:220px;max-height:220px;border-radius:10px;border:1px solid var(--line);display:block}
+ .qp-assets{display:grid;grid-template-columns:repeat(auto-fill,minmax(72px,1fr));gap:6px;max-height:190px;overflow-y:auto}
+ .qp-thumb{aspect-ratio:1/1;border:1px solid var(--line);border-radius:8px;overflow:hidden;cursor:pointer;background:#efeae1}
+ .qp-thumb.sel{outline:3px solid var(--accent);outline-offset:-1px}
+ .qp-thumb img{width:100%;height:100%;object-fit:cover;display:block}
+ .qp-dh{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
+ .qp-dh h3{margin:0;font:600 16px/1 "Cormorant Garamond",Georgia,serif}
+ .qp-draftitem{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:9px;padding:8px 10px;margin-bottom:6px}
+ .qp-draftitem .pl{font-size:16px}
+ .qp-draftitem .mid{flex:1;min-width:0}
+ .qp-draftitem .cap{font-size:11.5px;color:#5a544b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ .qp-draftitem .when{font-size:10px;color:var(--mut)}
+ .qp-draftitem .x{border:0;background:transparent;cursor:pointer;color:var(--mut);font:700 14px/1 Inter}
+ .qp-draftitem .x:hover{color:var(--accent)}
+</style>
diff --git a/public/panels/quickpost.js b/public/panels/quickpost.js
new file mode 100644
index 0000000..cc1d299
--- /dev/null
+++ b/public/panels/quickpost.js
@@ -0,0 +1,77 @@
+// Quick Post panel — pick media + caption, stage a draft to any platform via the
+// shared MCCQuickPost component (loaded globally in index.html). Draft-only/gated.
+window.MCC_PANELS = window.MCC_PANELS || {};
+window.MCC_PANELS['quickpost'] = {
+ init(root) {
+ const O = location.origin;
+ const $ = s => root.querySelector(s);
+ const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c =>
+ ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+ const media = $('#qp-media'), cap = $('#qp-caption'), preview = $('#qp-preview');
+ const PICON = { instagram: '📷', tiktok: '🎵', facebook: '📘', linkedin: '💼' };
+
+ function renderPreview() {
+ const u = media.value.trim();
+ if (!u) { preview.innerHTML = ''; return; }
+ preview.innerHTML = /\.(mp4|mov|webm|m4v)(\?|$)/i.test(u)
+ ? `<video src="${esc(u)}" muted playsinline preload="metadata"></video>`
+ : `<img src="${esc(u)}" alt="" onerror="this.style.display='none'">`;
+ }
+ media.addEventListener('input', renderPreview);
+
+ // wire the shared button row — payload is read live at click time
+ if (window.MCCQuickPost) {
+ window.MCCQuickPost.attach($('#qp-buttons'),
+ () => ({ caption: cap.value.trim(), mediaUrl: media.value.trim(), source: 'quickpost-panel' }));
+ } else {
+ $('#qp-buttons').innerHTML = '<div class="muted" style="font-size:12px">Quick-post component not loaded.</div>';
+ }
+
+ // asset quick-pick
+ (async function loadAssets() {
+ const box = $('#qp-assets');
+ let items = [];
+ try { const d = await fetch(O + '/api/assets/list', { credentials: 'same-origin' }).then(r => r.json()); items = (d && d.assets) || []; }
+ catch (_) {}
+ const withImg = items.filter(a => a.src).slice(0, 60);
+ if (!withImg.length) { box.innerHTML = '<div class="muted" style="font-size:12px">No assets yet — add some in the Library.</div>'; return; }
+ box.innerHTML = withImg.map(a =>
+ `<div class="qp-thumb" data-url="${esc(location.origin + a.src)}" title="${esc(a.name || '')}"><img loading="lazy" src="${esc(a.src)}"></div>`).join('');
+ box.querySelectorAll('.qp-thumb').forEach(el => el.onclick = () => {
+ box.querySelectorAll('.qp-thumb').forEach(x => x.classList.remove('sel'));
+ el.classList.add('sel');
+ media.value = el.getAttribute('data-url'); renderPreview();
+ });
+ })();
+
+ // staged-drafts list
+ async function loadDrafts() {
+ const box = $('#qp-draftlist'), cnt = $('#qp-count');
+ let drafts = [];
+ try { const d = await fetch(O + '/api/quickpost/drafts', { credentials: 'same-origin' }).then(r => r.json()); drafts = (d && d.drafts) || []; }
+ catch (_) {}
+ if (cnt) cnt.textContent = drafts.length;
+ if (!drafts.length) { box.innerHTML = '<div class="muted" style="font-size:12px">None yet.</div>'; return; }
+ box.innerHTML = drafts.map(d => {
+ const when = new Date(d.created_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
+ return `<div class="qp-draftitem">
+ <span class="pl">${PICON[d.platform] || '•'}</span>
+ <div class="mid">
+ <div class="cap" title="${esc(d.caption)}">${esc(d.caption || '(no caption)')}</div>
+ <div class="when">🕓 ${esc(when)} · ${esc(d.platform)}${d.mediaUrl ? ' · has media' : ''}</div>
+ </div>
+ <button class="x" data-del="${esc(d.id)}" title="Remove draft">✕</button>
+ </div>`;
+ }).join('');
+ box.querySelectorAll('[data-del]').forEach(b => b.onclick = async () => {
+ await fetch(O + '/api/quickpost/draft/delete', {
+ method: 'POST', credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: b.getAttribute('data-del') }),
+ }).catch(() => {});
+ loadDrafts();
+ });
+ }
+ document.addEventListener('qp:staged', loadDrafts);
+ loadDrafts();
+ },
+};
diff --git a/public/quickpost.js b/public/quickpost.js
new file mode 100644
index 0000000..fe8463e
--- /dev/null
+++ b/public/quickpost.js
@@ -0,0 +1,85 @@
+// MCCQuickPost — the shared "easy post" component reused in 3 places (the Quick
+// Post panel, the Compose panel, and each asset/video card). Renders a row of
+// per-platform buttons; a click STAGES A DRAFT (gated) via /api/quickpost/draft
+// and shows a toast. Live posting is intentionally NOT wired here — staging is
+// the safe default that honors the "social posting is Steve-gated" rule.
+(function () {
+ const ORIGIN = location.origin;
+ const PLATFORMS = [
+ { id: 'instagram', label: 'Instagram', icon: '📷' },
+ { id: 'tiktok', label: 'TikTok', icon: '🎵' },
+ { id: 'facebook', label: 'Facebook', icon: '📘' },
+ { id: 'linkedin', label: 'LinkedIn', icon: '💼' },
+ ];
+
+ // one-time CSS
+ if (!document.getElementById('qp-css')) {
+ const s = document.createElement('style'); s.id = 'qp-css';
+ s.textContent = `
+ .qp-row{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
+ .qp-btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line,#e3ddd0);
+ background:#fff;border-radius:9px;cursor:pointer;font:600 12px/1 Inter,sans-serif;color:var(--ink,#14110f);
+ padding:8px 12px;transition:background .12s,border-color .12s}
+ .qp-btn:hover{background:var(--cream,#f4efe7);border-color:var(--accent,#8a5a44)}
+ .qp-btn.busy{opacity:.6;pointer-events:none}
+ .qp-btn.ok{background:#e3efe0;border-color:#bcd8b6;color:#2f6b34}
+ .qp-btn .qp-i{font-size:14px}
+ .qp-btn.mini{padding:5px 8px;font-size:10.5px;border-radius:7px}
+ .qp-all{border-style:dashed;color:var(--accent,#8a5a44)}
+ .qp-hint{font-size:11px;color:var(--mut,#8a8275)}
+ #qp-toast{position:fixed;right:18px;bottom:18px;z-index:9999;display:flex;flex-direction:column;gap:8px}
+ #qp-toast .t{background:var(--ink,#14110f);color:#fff;border-radius:10px;padding:11px 15px;font:600 12.5px/1.3 Inter,sans-serif;
+ box-shadow:0 10px 30px rgba(0,0,0,.25);opacity:0;transform:translateY(8px);transition:opacity .2s,transform .2s;max-width:340px}
+ #qp-toast .t.show{opacity:1;transform:none}
+ #qp-toast .t .g{color:var(--gold-soft,#d8c19a)}`;
+ document.head.appendChild(s);
+ }
+ function toast(msg) {
+ let box = document.getElementById('qp-toast');
+ if (!box) { box = document.createElement('div'); box.id = 'qp-toast'; document.body.appendChild(box); }
+ const t = document.createElement('div'); t.className = 't'; t.innerHTML = msg;
+ box.appendChild(t);
+ requestAnimationFrame(() => t.classList.add('show'));
+ setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 250); }, 2600);
+ }
+
+ async function stage(platform, payload) {
+ const r = await fetch(ORIGIN + '/api/quickpost/draft', {
+ method: 'POST', credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ platform, ...payload }),
+ });
+ return r.json().catch(() => ({ error: 'bad response' }));
+ }
+
+ // Render a button row into `mount`. getPayload(platform) → {caption, mediaUrl, source}.
+ // opts: { mini:false, includeAll:true }
+ function attach(mount, getPayload, opts) {
+ opts = opts || {};
+ const cls = opts.mini ? 'qp-btn mini' : 'qp-btn';
+ let html = PLATFORMS.map(p =>
+ `<button type="button" class="${cls}" data-qp="${p.id}" title="Stage a draft post to ${p.label}">` +
+ `<span class="qp-i">${p.icon}</span>${opts.mini ? '' : p.label}</button>`).join('');
+ if (opts.includeAll !== false && !opts.mini)
+ html += `<button type="button" class="qp-btn qp-all" data-qp="__all" title="Stage a draft to all platforms">+ All</button>`;
+ mount.insertAdjacentHTML('beforeend', `<div class="qp-row">${html}</div>`);
+
+ mount.querySelectorAll('[data-qp]').forEach(btn => btn.addEventListener('click', async () => {
+ const which = btn.getAttribute('data-qp');
+ const targets = which === '__all' ? PLATFORMS.map(p => p.id) : [which];
+ const payload = (typeof getPayload === 'function' ? getPayload() : getPayload) || {};
+ if (!payload.mediaUrl && !payload.caption) { toast('Nothing to post — add a caption or pick media first.'); return; }
+ btn.classList.add('busy'); const orig = btn.innerHTML;
+ let ok = 0;
+ for (const pl of targets) { const res = await stage(pl, payload); if (res && res.ok) ok++; }
+ btn.classList.remove('busy'); btn.classList.add('ok');
+ btn.innerHTML = '✓ Staged';
+ const label = which === '__all' ? `all ${ok} platforms` : PLATFORMS.find(p => p.id === which).label;
+ toast(`🚀 Draft staged for <span class="g">${label}</span> — awaiting your approval.`);
+ setTimeout(() => { btn.classList.remove('ok'); btn.innerHTML = orig; }, 1600);
+ document.dispatchEvent(new CustomEvent('qp:staged'));
+ }));
+ }
+
+ window.MCCQuickPost = { PLATFORMS, attach, stage, toast };
+})();
← eacbee8 MCC promo v3: DTD fix — hub screenshots as intentional app-w
·
back to Marketing Command Center
·
deploy: Easy Post buttons live on marketing.dw (v1.10.0) a03f7e3 →