← 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 });
    });
  },
};