← back to Marketing Command Center
modules/composer/index.js
68 lines
// Composer — the central "combine everything" board. Pull an ASSET, a rendered
// BANNER, or a generated REEL/STORY, write/GENERATE the COPY, pick the target SOCIALS
// (and, for Facebook, which Page), and publish (or stage) to all of them in one shot.
// Pure client-side aggregation over the EXISTING module APIs (/api/assets,
// /api/layouts, /api/copy, /api/channels) plus the two data endpoints below.
const fs = require('fs');
const path = require('path');
const os = require('os');
// The reels live in the SEPARATE dw-marketing-reels app; both run on the same host,
// so we read its manifest by path. REELS_MANIFEST overrides; otherwise try the known
// local (Mac) + prod (Kamatera) locations in order. The public video URL is served by
// that app's /reels/*.mp4 passthrough under REELS_PUBLIC_BASE.
const REELS_PUBLIC_BASE = (process.env.REELS_PUBLIC_BASE || 'https://marketing.designerwallcoverings.com').replace(/\/$/, '');
const REELS_MANIFEST_CANDIDATES = [
process.env.REELS_MANIFEST,
path.join(__dirname, '..', '..', '..', 'dw-marketing-reels', 'data', 'reels.json'),
'/root/public-projects/dw-marketing-reels/data/reels.json',
path.join(os.homedir(), 'Projects', 'dw-marketing-reels', 'data', 'reels.json'),
].filter(Boolean);
function readReelsManifest() {
for (const p of REELS_MANIFEST_CANDIDATES) {
try { if (fs.existsSync(p)) return { path: p, reels: JSON.parse(fs.readFileSync(p, 'utf8')) }; } catch { /* try next */ }
}
return { path: null, reels: [] };
}
module.exports = {
id: 'composer',
title: 'Composer',
icon: '🎨',
mount(router) {
// Convenience: the ordered channel list the composer offers as publish targets,
// so the front-end doesn't hard-code it. Connection state is layered in client-
// side from /api/channels/status (a channel with no creds still shows, disabled).
router.get('/targets', (_req, res) => res.json({
targets: [
{ id: 'facebook', label: 'Facebook', icon: '📘' },
{ id: 'instagram', label: 'Instagram', icon: '📸' },
{ id: 'bluesky', label: 'Bluesky', icon: '🦋' },
{ id: 'linkedin', label: 'LinkedIn', icon: '💼' },
{ id: 'tiktok', label: 'TikTok', icon: '🎵' },
{ id: 'youtube', label: 'YouTube', icon: '▶️' },
{ id: 'threads', label: 'Threads', icon: '🧵' },
],
}));
// Reel / Story assets — the generated portrait videos from dw-marketing-reels,
// exposed as selectable post media (videoUrl). Newest first, capped so the picker
// stays snappy. Each carries a public .mp4 URL Meta/Bluesky can fetch.
router.get('/reels', (_req, res) => {
const { path: manPath, reels } = readReelsManifest();
// cap at 40 so the picker stays snappy (more than enough for a scroll)
const list = (Array.isArray(reels) ? reels : (reels.reels || [])).slice(0, 40).map(r => ({
file: r.file,
url: `${REELS_PUBLIC_BASE}/reels/${encodeURIComponent(r.file)}`,
title: (r.titles && r.titles[0]) || r.kind || r.file,
kind: r.kind || 'reel',
caption: r.caption || '',
seconds: r.seconds || null,
created_at: r.created_at || null,
})).filter(r => r.file);
res.json({ source: manPath, count: list.length, reels: list });
});
},
};