← 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 =>
({ '&': '&', '<': '<', '>': '>', '"': '"' }[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();
},
};