← back to Marketing Command Center

public/panels/sounds.js

86 lines

// Sounds & Assets panel — free music + TikTok creative sources, badged by
// brand-safety. Search + badge facets. Pure link directory (data from
// /api/sounds/catalog); every card opens the source in a new tab.
window.MCC_PANELS = window.MCC_PANELS || {};
window.MCC_PANELS['sounds'] = {
  init(root) {
    const ORIGIN = location.origin;
    const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c =>
      ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
    const $ = sel => root.querySelector(sel);

    const state = { catalog: [], badges: {}, q: '', badge: 'all' };

    async function load() {
      let d;
      try { d = await fetch(ORIGIN + '/api/sounds/catalog', { credentials: 'same-origin' }).then(r => r.json()); }
      catch (_) { $('#snd-banner').textContent = 'Could not load the catalog.'; return; }
      state.catalog = d.catalog || [];
      state.badges = d.badges || {};
      const total = state.catalog.reduce((n, s) => n + s.items.length, 0);
      $('#snd-banner').innerHTML = `<b>${total}</b> free / brand-safe sources across ${state.catalog.length} groups. ` +
        `Filter by type, then click through — sources open in a new tab.`;
      renderLegend();
      renderBadgeFilter();
      renderSections();
    }

    function renderLegend() {
      $('#snd-legend').innerHTML = Object.values(state.badges)
        .map(b => `<span style="margin-right:16px"><b>${esc(b.label)}</b> — ${esc(b.desc)}</span>`).join('');
    }

    function renderBadgeFilter() {
      const box = $('#snd-badgefilter');
      const keys = ['all', ...Object.keys(state.badges)];
      box.innerHTML = keys.map(k => {
        const label = k === 'all' ? 'All' : (state.badges[k] ? state.badges[k].label : k);
        return `<button type="button" class="snd-bf${state.badge === k ? ' on' : ''}" data-b="${esc(k)}">${esc(label)}</button>`;
      }).join('');
      box.querySelectorAll('.snd-bf').forEach(b => b.onclick = () => {
        state.badge = b.getAttribute('data-b'); renderBadgeFilter(); renderSections();
      });
    }

    function match(item) {
      if (state.badge !== 'all' && item.badge !== state.badge) return false;
      const q = state.q.trim().toLowerCase();
      if (q && !((item.name || '').toLowerCase().includes(q) || (item.note || '').toLowerCase().includes(q))) return false;
      return true;
    }

    function renderSections() {
      const box = $('#snd-sections');
      let html = '';
      let shown = 0;
      for (const sec of state.catalog) {
        const items = sec.items.filter(match);
        if (!items.length) continue;
        shown += items.length;
        html += `<div class="snd-sec">
          <div class="snd-sechead"><h2>${esc(sec.title)}</h2><span class="ct">${items.length}</span></div>
          <div class="snd-secblurb">${esc(sec.blurb || '')}</div>
          <div class="snd-grid">${items.map(cardHTML).join('')}</div>
        </div>`;
      }
      box.innerHTML = shown ? html : '<div class="snd-empty">No sources match that filter.</div>';
    }

    function cardHTML(it) {
      const b = state.badges[it.badge] || { label: it.badge };
      return `<a class="snd-card" href="${esc(it.url)}" target="_blank" rel="noopener noreferrer">
        <div class="st">
          <span class="nm">${esc(it.name)}</span>
          <span class="bdg bdg-${esc(it.badge)}">${esc(b.label)}</span>
        </div>
        <div class="nt">${esc(it.note || '')}</div>
        <div class="arr">Open ↗</div>
      </a>`;
    }

    $('#snd-search').addEventListener('input', e => { state.q = e.target.value; renderSections(); });

    load();
  },
};