← back to Marketing Command Center

public/panels/templates.js

252 lines

// templates panel — Campaign Templates browser + filler.
// Loads /api/templates/list, renders card-per-template, opens a modal with a
// placeholder form on "Use this", POSTs /api/templates/:id/use to get the
// filled template, and provides copy-to-clipboard for subject and body.
window.MCC_PANELS = window.MCC_PANELS || {};
window.MCC_PANELS['templates'] = {
  async init(root) {
    const $ = s => root.querySelector(s);
    const $$ = s => Array.from(root.querySelectorAll(s));
    const esc = s => String(s == null ? '' : s)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

    const banner    = $('#templates-banner');
    const out       = $('#templates-out');
    const search    = $('#templates-search');
    const catSel    = $('#templates-category');
    const modal     = $('#templates-modal');
    const modalBody = $('#templates-modal-body');
    const modalClose = $('#templates-modal-close');

    let TEMPLATES = [];
    let META = null;

    // ── load ─────────────────────────────────────────────────────────────────
    try {
      const [metaRes, listRes] = await Promise.all([
        fetch('/api/templates/meta').then(r => r.json()),
        fetch('/api/templates/list').then(r => r.json()),
      ]);
      META = metaRes;
      TEMPLATES = Array.isArray(listRes.templates) ? listRes.templates : [];
    } catch (e) {
      out.innerHTML = '<div class="muted-banner">Could not reach the campaign templates service.</div>';
      return;
    }

    if (META && META.gateMessage) {
      banner.innerHTML = `<div class="muted-banner">📋 STAGED — ${esc(META.gateMessage)}</div>`;
    }

    // ── category select ─────────────────────────────────────────────────────
    const cats = ['All categories', ...(META && META.categories ? META.categories : [])];
    catSel.innerHTML = cats.map((c, i) =>
      `<option value="${i === 0 ? '' : esc(c)}">${esc(c)}</option>`).join('');

    // ── render ──────────────────────────────────────────────────────────────
    function visible() {
      const q = (search.value || '').trim().toLowerCase();
      const cat = catSel.value;
      return TEMPLATES.filter(t => {
        if (cat && t.category !== cat) return false;
        if (!q) return true;
        const hay = `${t.title} ${t.category} ${t.description} ${t.subject} ${t.audience} ${(t.productLines || []).join(' ')}`.toLowerCase();
        return hay.includes(q);
      });
    }

    function render() {
      const list = visible();
      if (!list.length) {
        out.innerHTML = '<div class="muted-banner">No templates match that filter.</div>';
        return;
      }
      out.innerHTML = list.map(t => `
        <div class="card" style="margin-bottom:0;display:flex;flex-direction:column">
          <div class="row" style="justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:8px">
            <span class="pill">${esc(t.category || '—')}</span>
            <span class="muted" style="font-size:11px;font-weight:600;letter-spacing:.5px;text-transform:uppercase">${esc(t.sequencePosition || '')}</span>
          </div>
          <h3 style="margin:0 0 6px;font:600 16px/1.25 'Cormorant Garamond',Georgia,serif">${esc(t.title)}</h3>
          <div style="font-size:13px;font-weight:600;color:var(--ink);margin-bottom:6px">${esc(t.subject)}</div>
          <div class="muted" style="font-size:12.5px;line-height:1.5;margin-bottom:12px;flex:1">${esc(t.description)}</div>
          <div class="row" style="gap:6px;margin-bottom:12px;flex-wrap:wrap">
            ${esc(t.audience) ? `<span class="pill" style="background:#efece5;color:var(--ink)">${esc(t.audience)}</span>` : ''}
            ${(t.productLines || []).slice(0, 3).map(p => `<span class="pill" style="background:#f4efe7;color:var(--accent)">${esc(p)}</span>`).join('')}
          </div>
          <div class="row" style="justify-content:flex-end;gap:8px;margin-top:auto">
            <button class="btn ghost preview-btn" data-id="${esc(t.id)}" style="padding:7px 14px">👁 Preview</button>
            <button class="btn gold use-btn" data-id="${esc(t.id)}" style="padding:7px 14px">📋 Use this</button>
          </div>
        </div>`).join('');

      out.querySelectorAll('.preview-btn').forEach(b =>
        b.addEventListener('click', () => openModal(b.dataset.id, 'preview')));
      out.querySelectorAll('.use-btn').forEach(b =>
        b.addEventListener('click', () => openModal(b.dataset.id, 'use')));
    }

    search.addEventListener('input', render);
    catSel.addEventListener('change', render);
    render();

    // ── modal ────────────────────────────────────────────────────────────────
    function showModal() { modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; }
    function hideModal() { modal.style.display = 'none'; modalBody.innerHTML = ''; document.body.style.overflow = ''; }
    modalClose.addEventListener('click', hideModal);
    modal.addEventListener('click', (e) => { if (e.target === modal) hideModal(); });

    function openModal(id, mode) {
      const tpl = TEMPLATES.find(t => t.id === id);
      if (!tpl) return;
      if (mode === 'preview') renderPreview(tpl);
      else renderUseForm(tpl);
      showModal();
    }

    function renderPreview(tpl) {
      modalBody.innerHTML = `
        <span class="pill">${esc(tpl.category)}</span>
        <h2 style="margin:10px 0 4px">${esc(tpl.title)}</h2>
        <div class="muted" style="font-size:12.5px;margin-bottom:14px">${esc(tpl.description)}</div>

        <label>Subject (template)</label>
        <div style="background:var(--paper);border:1px solid var(--line);border-radius:9px;padding:10px 12px;font-weight:600;margin-bottom:14px">${esc(tpl.subject)}</div>

        ${tpl.preheader ? `
          <label>Preheader (template)</label>
          <div style="background:var(--paper);border:1px solid var(--line);border-radius:9px;padding:10px 12px;margin-bottom:14px;font-size:13px;color:var(--mut)">${esc(tpl.preheader)}</div>
        ` : ''}

        <label>Body (template — with {{placeholders}})</label>
        <pre style="white-space:pre-wrap;font:13px/1.6 'Inter',sans-serif;background:var(--paper);border:1px solid var(--line);border-radius:9px;padding:14px;margin:0 0 14px;color:var(--ink)">${esc(tpl.body)}</pre>

        <div class="muted-banner" style="margin:0 0 14px">⚠ Add CAN-SPAM footer (physical mailing address + working unsubscribe) before this template is queued in Constant Contact.</div>

        <div class="row" style="justify-content:flex-end">
          <button class="btn gold" id="from-preview-use" style="padding:8px 16px">📋 Use this template</button>
        </div>`;
      modalBody.querySelector('#from-preview-use').addEventListener('click', () => renderUseForm(tpl));
    }

    function renderUseForm(tpl) {
      const placeholders = tpl.placeholders || [];
      modalBody.innerHTML = `
        <span class="pill">${esc(tpl.category)}</span>
        <h2 style="margin:10px 0 4px">${esc(tpl.title)} — fill in</h2>
        <div class="muted" style="font-size:12.5px;margin-bottom:14px">Fill the fields and click <b>Generate draft</b>. STAGED only — nothing sends.</div>

        ${placeholders.length === 0
          ? '<div class="muted-banner" style="margin-bottom:14px">This template has no placeholders — it\'s ready to copy as-is.</div>'
          : `<div class="grid" style="grid-template-columns:repeat(2,1fr);margin-bottom:16px">
              ${placeholders.map(p => `
                <div>
                  <label for="ph-${esc(p.key)}">${esc(p.label || p.key)}</label>
                  <input id="ph-${esc(p.key)}" data-key="${esc(p.key)}" type="text" placeholder="${esc(p.default || '')}" value="" />
                </div>`).join('')}
            </div>`}

        <div class="row" style="justify-content:flex-end;gap:8px;margin-bottom:14px">
          <button class="btn ghost" id="use-cancel" style="padding:7px 14px">Cancel</button>
          <button class="btn gold" id="use-generate" style="padding:7px 14px">📋 Generate draft</button>
        </div>

        <div id="use-result"></div>`;

      modalBody.querySelector('#use-cancel').addEventListener('click', hideModal);
      modalBody.querySelector('#use-generate').addEventListener('click', async () => {
        const fields = {};
        modalBody.querySelectorAll('input[data-key]').forEach(el => {
          fields[el.dataset.key] = el.value;
        });
        const btn = modalBody.querySelector('#use-generate');
        btn.disabled = true; const orig = btn.textContent; btn.textContent = 'Drafting…';
        try {
          const r = await fetch(`/api/templates/${encodeURIComponent(tpl.id)}/use`, {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ fields }),
          });
          const data = await r.json();
          renderUseResult(tpl, data);
        } catch (e) {
          modalBody.querySelector('#use-result').innerHTML =
            '<div class="muted-banner">Could not generate the draft. Try again.</div>';
        } finally {
          btn.disabled = false; btn.textContent = orig;
        }
      });
    }

    function renderUseResult(tpl, data) {
      const target = modalBody.querySelector('#use-result');
      if (!target) return;
      const unfilledNote = (data.unfilled && data.unfilled.length)
        ? `<div class="muted-banner" style="margin:14px 0 0">Still unfilled: ${data.unfilled.map(esc).join(', ')} — the placeholders are kept in the draft so you can finish them in Constant Contact.</div>`
        : '';
      target.innerHTML = `
        <div style="margin-top:8px;padding-top:14px;border-top:1px solid var(--line)">
          <div class="row" style="justify-content:space-between;align-items:center;margin-bottom:8px">
            <h3 style="margin:0;font:600 15px/1.2 'Cormorant Garamond',Georgia,serif">Filled draft (STAGED)</h3>
            <span class="pill" style="background:#fff0e6;color:var(--accent)">staged · no send</span>
          </div>

          <label>Subject</label>
          <div class="row" style="align-items:center;gap:8px;margin-bottom:14px">
            <div style="flex:1;background:var(--paper);border:1px solid var(--line);border-radius:9px;padding:10px 12px;font-weight:600">${esc(data.subject)}</div>
            <button class="btn ghost copy-subject" style="padding:7px 12px">📋 Copy</button>
          </div>

          ${data.preheader ? `
            <label>Preheader</label>
            <div class="row" style="align-items:center;gap:8px;margin-bottom:14px">
              <div style="flex:1;background:var(--paper);border:1px solid var(--line);border-radius:9px;padding:10px 12px;font-size:13px;color:var(--mut)">${esc(data.preheader)}</div>
              <button class="btn ghost copy-preheader" style="padding:7px 12px">📋 Copy</button>
            </div>` : ''}

          <label>Body</label>
          <pre style="white-space:pre-wrap;font:13px/1.6 'Inter',sans-serif;background:var(--paper);border:1px solid var(--line);border-radius:9px;padding:14px;margin:0 0 8px;max-height:400px;overflow:auto">${esc(data.body)}</pre>
          <div class="row" style="justify-content:flex-end;gap:8px;margin-bottom:14px">
            <button class="btn ghost copy-body" style="padding:7px 12px">📋 Copy body</button>
            <button class="btn ghost copy-all" style="padding:7px 12px">📋 Copy subject + body</button>
          </div>

          <div class="muted-banner" style="margin:0">⚠ Before this ships in Constant Contact: ${esc(data.complianceReminder || 'add CAN-SPAM footer (physical mailing address + working unsubscribe).')}</div>
          ${unfilledNote}
        </div>`;

      const setCopier = (sel, text) => {
        const b = target.querySelector(sel);
        if (!b) return;
        b.addEventListener('click', async () => {
          const ok = await copyText(text);
          const prev = b.textContent;
          b.textContent = ok ? '✓ Copied' : 'Copy failed';
          setTimeout(() => { b.textContent = prev; }, 1400);
        });
      };
      setCopier('.copy-subject', data.subject || '');
      setCopier('.copy-preheader', data.preheader || '');
      setCopier('.copy-body', data.body || '');
      setCopier('.copy-all', `Subject: ${data.subject || ''}\n\n${data.body || ''}`);
    }

    async function copyText(text) {
      try {
        if (navigator.clipboard && navigator.clipboard.writeText) {
          await navigator.clipboard.writeText(text);
          return true;
        }
      } catch { /* fall through to legacy */ }
      try {
        const ta = document.createElement('textarea');
        ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
        document.body.appendChild(ta); ta.select();
        const ok = document.execCommand('copy');
        document.body.removeChild(ta);
        return ok;
      } catch { return false; }
    }
  },
};