/* Creative Preview =================================================== */ const CREATIVE_FORMATS = [ { id:'image', label:'Image' }, { id:'video', label:'Video' }, { id:'carousel', label:'Carousel' }, { id:'story', label:'Story' }, ]; const CREATIVE_OBJECTIVES = [ { id:'conversions', label:'Conversions' }, { id:'traffic', label:'Traffic' }, { id:'awareness', label:'Awareness' }, { id:'installs', label:'App Installs'}, { id:'leads', label:'Leads' }, { id:'engagement', label:'Engagement' }, ]; const CREATIVE_STATUSES = [ { id:'active', label:'Active', tone:'good' }, { id:'paused', label:'Paused', tone:'neutral' }, { id:'review', label:'In Review', tone:'warn' }, { id:'learning', label:'Learning', tone:'info' }, { id:'rejected', label:'Rejected', tone:'bad' }, ]; const CREATIVE_SORTS = [ { id:'spend_desc', label:'Spend — highest first' }, { id:'spend_asc', label:'Spend — lowest first' }, { id:'roas_desc', label:'ROAS — highest first', metric:'roas' }, { id:'ctr_desc', label:'CTR — highest first' }, { id:'imp_desc', label:'Impressions — highest' }, { id:'recent', label:'Most recently updated' }, ]; /* sorts visible for the active account (per-account metric blocklist) */ const creativeSorts = ()=> CREATIVE_SORTS.filter(s=>!s.metric || !(typeof metricHiddenHere==='function' && metricHiddenHere(s.metric))); const _crGrid = ()=> (typeof metricHiddenHere==='function' && metricHiddenHere('roas')) ? '56px 1fr 100px 110px 110px 90px 90px 90px' : '56px 1fr 100px 110px 110px 90px 90px 90px 90px'; /* ---------- Map live creatives → card shape ----------------------- */ /* Metrics are real (from the warehouse); only presentational props (aspect ratio, placeholder pattern) are derived for rendering. */ function buildCreatives(accountId, platformId){ const list = getCreatives(accountId, platformId || 'overview'); const aspects = { image:'4/5', video:'1/1', carousel:'1/1', story:'9/16' }; return list.map((c, i) => ({ id: c.id, name: c.name, platform: c.platform, format: c.format, aspect: aspects[c.format] || '1/1', objective: c.objective || 'conversions', status: c.status, headline: c.name, cta: 'Learn more', bodyText: '', spend: c.spend, ctr: c.ctr, cpm: c.cpm, imp: c.impressions, clicks: c.clicks, conv: c.conv, cpa: c.cpa, roas: c.roas, updated: '', pattern: ['blob','grid','rings','diag','dots','bars'][i % 6], hue: (i*37) % 360, thumb: c.thumb, })); } function mulberryFor(seed){ let s = (typeof seed === 'string') ? seed.split('').reduce((a,c)=>a + c.charCodeAt(0), 0) * 91 : seed; return function(){ s = (s + 0x6D2B79F5) | 0; let t = Math.imul(s ^ (s >>> 15), s | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /* ---------- Creative Thumbnail SVG (placeholder ad rendering) ----- */ function CreativeThumb({ c, status }){ const tone = (PLATFORMS.find(p=>p.id===c.platform)||PLATFORMS[0]).tone; const isDimmed = status === 'paused' || status === 'rejected'; const id = useMemo(()=>'cr'+Math.random().toString(36).slice(2,8), []); // 6 distinct visual treatments to differentiate creatives const PatternBg = ({ kind }) => { if (kind==='blob') return ( <> ); if (kind==='grid') return ( <> ); if (kind==='rings') return ( <> ); if (kind==='diag') return ( <> ); if (kind==='dots') return ( <> ); /* bars */ return ( <> {Array.from({length:6}).map((_,i)=>( ))} ); }; return (
{c.thumb ? {c.name} : } {/* placeholder marker (subtle) */}
creative · {c.format}
{/* Top row: format chip + status */}
{c.format==='video' && } {c.format==='carousel' && } {c.format==='story' && } {c.format==='image' && } {c.format}
{/* Headline (ad copy) */}
{c.headline}
{/* CTA + brand */}
{c.cta} → sponsored
{/* Dim overlay if paused/rejected */} {isDimmed &&
}
); } /* ---------- Filter pill with active count ------------------------- */ function FilterPill({ label, count, onClick, active, icon }){ return ( ); } /* Multi-select dropdown content */ function MultiOptionList({ options, selected, onToggle, allLabel='All' }){ return (
{options.map(opt => { const checked = selected.includes(opt.id); return ( ); })}
); } /* ---------- Creative card ----------------------------------------- */ function CreativeCard({ c, selectable, selected, onToggleSelect }){ const status = CREATIVE_STATUSES.find(s=>s.id===c.status) || { label:c.status, tone:'neutral' }; return (
{/* Select checkbox */} {selectable && ( )}
selectable && onToggleSelect()}>
{c.name}
{c.status==='active' && } {status.label}
{(CREATIVE_OBJECTIVES.find(o=>o.id===c.objective)||{label:c.objective}).label} · {c.updated}
Spend
{curPrefix()}{(c.spend/1000).toFixed(1)}k
CTR
{c.ctr}%
CPM
{curPrefix()}{c.cpm}
{!(typeof metricHiddenHere==='function' && metricHiddenHere('roas')) && (
ROAS
=2.5?'text-emerald-600 dark:text-emerald-400':c.roas<1.5?'text-rose-600 dark:text-rose-400':''}`}>{c.roas}×
)}
{(c.imp/1000).toFixed(0)}k imp · {c.conv} conv
); } /* ---------- Creative row (list view) ------------------------------ */ function CreativeRow({ c }){ const status = CREATIVE_STATUSES.find(s=>s.id===c.status) || { label:c.status, tone:'neutral' }; return (
{c.headline}
{c.name} · {c.format}
{(CREATIVE_OBJECTIVES.find(o=>o.id===c.objective)||{label:c.objective}).label}
{status.label}
{fmt(c.spend,'currency')}
{c.ctr}%
{!(typeof metricHiddenHere==='function' && metricHiddenHere('roas')) &&
{c.roas}×
}
{(c.imp/1000).toFixed(0)}k
); } /* ---------- Filter dropdown popover ------------------------------ */ function FilterDropdown({ label, options, selected, onChange, icon }){ const [open, setOpen] = useState(false); const ref = useRef(null); useOutsideClick(ref, ()=>setOpen(false)); const toggle = (id) => { onChange(selected.includes(id) ? selected.filter(x=>x!==id) : [...selected, id]); }; return (
setOpen(o=>!o)}> 0} icon={icon}/>
{open && (
)}
); } /* ---------- Main panel ------------------------------------------- */ function CreativePreview({ accountId, platformId }){ const all = useMemo(()=>buildCreatives(accountId, platformId), [accountId, platformId]); /* Filters */ const [platforms, setPlatforms] = useState([]); const [objectives, setObjectives] = useState([]); const [statuses, setStatuses] = useState([]); const [formats, setFormats] = useState([]); const [sort, setSort] = useState('spend_desc'); const [view, setView] = useState('grid'); const [search, setSearch] = useState(''); /* Compare mode */ const [compareMode, setCompareMode] = useState(false); const [selected, setSelected] = useState([]); const [showCompare, setShowCompare] = useState(false); /* Reset filters when account changes */ useEffect(()=>{ setPlatforms([]); setObjectives([]); setStatuses([]); setFormats([]); setSelected([]); setCompareMode(false); }, [accountId]); /* When platform tab changes, auto-scope filters */ const effectivePlatforms = useMemo(()=>{ if (platforms.length > 0) return platforms; if (platformId === 'overview') return []; return [platformId]; }, [platforms, platformId]); const filtered = useMemo(()=>{ let arr = all; if (effectivePlatforms.length) arr = arr.filter(c => effectivePlatforms.includes(c.platform)); if (objectives.length) arr = arr.filter(c => objectives.includes(c.objective)); if (statuses.length) arr = arr.filter(c => statuses.includes(c.status)); if (formats.length) arr = arr.filter(c => formats.includes(c.format)); if (search.trim()){ const q = search.toLowerCase(); arr = arr.filter(c => c.headline.toLowerCase().includes(q) || c.name.toLowerCase().includes(q)); } arr = [...arr]; switch(sort){ case 'spend_desc': arr.sort((a,b)=>b.spend-a.spend); break; case 'spend_asc': arr.sort((a,b)=>a.spend-b.spend); break; case 'roas_desc': arr.sort((a,b)=>b.roas-a.roas); break; case 'ctr_desc': arr.sort((a,b)=>b.ctr-a.ctr); break; case 'imp_desc': arr.sort((a,b)=>b.imp-a.imp); break; default: break; } return arr; }, [all, effectivePlatforms, objectives, statuses, formats, search, sort]); const toggleSelect = (id) => { setSelected(s => s.includes(id) ? s.filter(x=>x!==id) : (s.length>=4 ? s : [...s, id])); }; const selectedCreatives = selected.map(id => all.find(c=>c.id===id)).filter(Boolean); return (
{/* Header bar */}
Creative preview
Every running ad across platforms — {filtered.length} of {all.length} matching
setSearch(e.target.value)} placeholder="Search creatives…" className="w-full h-full pl-7 pr-2 rounded-lg border border-ink-200 dark:border-white/10 bg-white dark:bg-ink-900/40 text-[12px] focus:outline-none focus:border-accent-400"/>
{ setCompareMode(m=>!m); if (compareMode) setSelected([]); }}> {compareMode ? `Comparing ${selected.length}` : 'Compare'} New creative
{/* Filter row */}
p.id!=='overview').map(p=>({id:p.id, label:p.label}))} selected={platforms} onChange={setPlatforms}/>
Sort: {CREATIVE_SORTS.find(s=>s.id===sort).label.split(' — ')[0]} }> {(close)=>(
{creativeSorts().map(s => ( ))}
)}
{(platforms.length||objectives.length||statuses.length||formats.length||search) ? ( ) : ( No filters applied )}
{/* Body */} {filtered.length === 0 ? (
{all.length === 0 ? (<>
No creatives yet
Connect a data source or import data in Admin → Integrations.
) : (<>
No creatives match your filters
Try clearing one of the filters above.
)}
) : view === 'grid' ? (
{filtered.map(c => ( toggleSelect(c.id)}/> ))}
) : (
CreativePlatformObjectiveStatus SpendCTR {!(typeof metricHiddenHere==='function' && metricHiddenHere('roas')) && ROAS} Impr.
{filtered.map(c => )}
)} {/* Compare floating bar */} {compareMode && selected.length > 0 && (
{selectedCreatives.slice(0,4).map(c => ( p.id===c.platform)||PLATFORMS[0]).tone}}> {c.format[0].toUpperCase()} ))}
{selected.length} creative{selected.length===1?'':'s'} selected
{selected.length<2?'select at least 2 to compare':selected.length>=4?'4 max':''}
setShowCompare(true)} className={selected.length<2?'opacity-50 pointer-events-none':''}> Compare →
)} setShowCompare(false)} creatives={selectedCreatives}/>
); } Object.assign(window, { CreativePreview });