/* ── TheNicheQuiz.com – Frontend Logic ─────────────────────────────────── */ let adCount = 1; let previewAdIndex = 0; let lastCSVData = null; let lastExportCSVData = null; // Quiz state let quizState = { step: 1, industry: '', subNiche: '', microNiche: '' }; // Parallel campaigns state let parallelCampaigns = []; let galleryMode = false; // ── Save Campaign Set ─────────────────────────────────────────────────────── async function saveCampaignSet() { if (parallelCampaigns.length === 0) { showToast('No campaigns to save', 'error'); return; } const name = quizState.microNiche || quizState.subNiche || quizState.industry || 'Untitled Campaign'; try { const res = await fetch('/api/save-campaign', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name + ' — ' + new Date().toLocaleDateString(), industry: quizState.industry, sub_niche: quizState.subNiche, micro_niche: quizState.microNiche, campaign_data: { campaigns: parallelCampaigns } }) }); const data = await res.json(); if (data.error) throw new Error(data.error); showToast('Campaigns saved! View them in My Campaigns.', 'success'); } catch (e) { showToast('Failed to save: ' + e.message, 'error'); } } // ── Particle Background ───────────────────────────────────────────────────── (function initParticles() { const canvas = document.getElementById('particleCanvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); let particles = []; const PARTICLE_COUNT = 60; function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } resize(); window.addEventListener('resize', resize); class Particle { constructor() { this.reset(); } reset() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.radius = Math.random() * 1.5 + 0.5; this.alpha = Math.random() * 0.5 + 0.1; const colors = ['245, 158, 11', '59, 130, 246', '168, 85, 247']; this.color = colors[Math.floor(Math.random() * colors.length)]; } update() { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = `rgba(${this.color}, ${this.alpha})`; ctx.fill(); } } for (let i = 0; i < PARTICLE_COUNT; i++) particles.push(new Particle()); function drawConnections() { for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 150) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.strokeStyle = `rgba(255, 255, 255, ${0.03 * (1 - dist / 150)})`; ctx.lineWidth = 0.5; ctx.stroke(); } } } } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { p.update(); p.draw(); }); drawConnections(); requestAnimationFrame(animate); } animate(); })(); // ═══════════════════════════════════════════════════════════════════════════ // NICHE QUIZ // ═══════════════════════════════════════════════════════════════════════════ function updateStepIndicator() { const fill = document.getElementById('stepLineFill'); for (let i = 1; i <= 3; i++) { const dot = document.getElementById(`stepDot${i}`); dot.classList.remove('active', 'completed'); if (i < quizState.step) dot.classList.add('completed'); else if (i === quizState.step) dot.classList.add('active'); } fill.style.width = ((quizState.step - 1) / 2 * 100) + '%'; } function goToStep(step) { quizState.step = step; document.getElementById('quizStep1').classList.add('hidden'); document.getElementById('quizStep2').classList.add('hidden'); document.getElementById('quizStep3').classList.add('hidden'); document.getElementById('quizComplete').classList.add('hidden'); document.getElementById(`quizStep${step}`).classList.remove('hidden'); updateStepIndicator(); } function selectIndustry(btn) { document.querySelectorAll('.industry-card').forEach(c => c.classList.remove('selected')); btn.classList.add('selected'); document.getElementById('customIndustryRow').classList.add('hidden'); const industry = btn.dataset.industry; quizState.industry = industry; setTimeout(() => { goToStep(2); loadSubNiches(industry); }, 300); } function selectIndustryOther(btn) { document.querySelectorAll('.industry-card').forEach(c => c.classList.remove('selected')); btn.classList.add('selected'); document.getElementById('customIndustryRow').classList.remove('hidden'); document.getElementById('customIndustryInput').focus(); } function confirmCustomIndustry() { const val = document.getElementById('customIndustryInput').value.trim(); if (!val) { showToast('Please enter your industry', 'error'); return; } quizState.industry = val; goToStep(2); loadSubNiches(val); } async function loadSubNiches(industry) { const grid = document.getElementById('subNicheGrid'); const loading = document.getElementById('nicheLoading2'); document.getElementById('selectedIndustryLabel').textContent = industry; grid.innerHTML = ''; loading.classList.remove('hidden'); setStatus('Finding sub-niches...', 'loading'); try { const res = await fetch('/api/suggest-niches', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ industry }) }); const data = await res.json(); loading.classList.add('hidden'); setStatus('Ready', 'ready'); if (data.error) { showToast(data.error, 'error'); return; } data.niches.forEach(niche => { const card = document.createElement('button'); card.className = 'niche-card'; card.textContent = niche; card.onclick = () => selectSubNiche(niche, card); grid.appendChild(card); }); } catch (e) { loading.classList.add('hidden'); setStatus('Ready', 'ready'); showToast('Failed to load sub-niches', 'error'); } } function selectSubNiche(niche, card) { document.querySelectorAll('#subNicheGrid .niche-card').forEach(c => c.classList.remove('selected')); if (card) card.classList.add('selected'); quizState.subNiche = niche; setTimeout(() => { goToStep(3); loadMicroNiches(quizState.industry, niche); }, 300); } function confirmCustomSubNiche() { const val = document.getElementById('customSubNicheInput').value.trim(); if (!val) { showToast('Please enter your sub-niche', 'error'); return; } selectSubNiche(val, null); } async function loadMicroNiches(industry, subNiche) { const grid = document.getElementById('microNicheGrid'); const loading = document.getElementById('nicheLoading3'); document.getElementById('breadcrumb1').textContent = industry; document.getElementById('breadcrumb2').textContent = subNiche; grid.innerHTML = ''; loading.classList.remove('hidden'); setStatus('Finding micro-niches...', 'loading'); try { const res = await fetch('/api/suggest-niches', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ industry, sub_niche: subNiche }) }); const data = await res.json(); loading.classList.add('hidden'); setStatus('Ready', 'ready'); if (data.error) { showToast(data.error, 'error'); return; } data.niches.forEach(niche => { const card = document.createElement('button'); card.className = 'niche-card'; card.textContent = niche; card.onclick = () => selectMicroNiche(niche, card); grid.appendChild(card); }); } catch (e) { loading.classList.add('hidden'); setStatus('Ready', 'ready'); showToast('Failed to load micro-niches', 'error'); } } function selectMicroNiche(niche, card) { document.querySelectorAll('#microNicheGrid .niche-card').forEach(c => c.classList.remove('selected')); if (card) card.classList.add('selected'); quizState.microNiche = niche; setTimeout(() => showQuizComplete(), 300); } function confirmCustomMicroNiche() { const val = document.getElementById('customMicroNicheInput').value.trim(); if (!val) { showToast('Please enter your micro-niche', 'error'); return; } quizState.microNiche = val; showQuizComplete(); } function showQuizComplete() { quizState.step = 4; document.getElementById('quizStep1').classList.add('hidden'); document.getElementById('quizStep2').classList.add('hidden'); document.getElementById('quizStep3').classList.add('hidden'); document.getElementById('quizComplete').classList.remove('hidden'); document.getElementById('finalIndustry').textContent = quizState.industry; document.getElementById('finalSubNiche').textContent = quizState.subNiche; document.getElementById('finalMicroNiche').textContent = quizState.microNiche; // Update step indicator to all completed for (let i = 1; i <= 3; i++) { document.getElementById(`stepDot${i}`).classList.remove('active'); document.getElementById(`stepDot${i}`).classList.add('completed'); } document.getElementById('stepLineFill').style.width = '100%'; showToast('Niche locked in!', 'success'); } function skipToManual() { document.getElementById('quizSection').classList.add('hidden'); document.getElementById('manualBuilder').classList.remove('hidden'); // Pre-fill with quiz data if available const nameInput = document.querySelector('#campaignSection [data-field="campaign_name"]'); const adsetInput = document.querySelector('#adsetSection [data-field="adset_name"]'); const bodyInput = document.querySelector('.ad-card [data-field="body"]'); const titleInput = document.querySelector('.ad-card [data-field="title"]'); if (quizState.microNiche) { if (nameInput) nameInput.value = quizState.microNiche + ' Campaign'; if (adsetInput) adsetInput.value = quizState.microNiche + ' - Core Audience'; if (titleInput) titleInput.value = quizState.microNiche; updatePreview(); } } // ═══════════════════════════════════════════════════════════════════════════ // PARALLEL CAMPAIGNS // ═══════════════════════════════════════════════════════════════════════════ async function generateParallelCampaigns() { document.getElementById('quizSection').classList.add('hidden'); document.getElementById('campaignsSection').classList.remove('hidden'); document.getElementById('generationLoading').classList.remove('hidden'); document.getElementById('campaignCardsGrid').innerHTML = ''; document.getElementById('campaignsNicheLabel').textContent = quizState.microNiche; setStatus('Generating campaigns...', 'loading'); try { const res = await fetch('/api/generate-parallel-campaigns', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ industry: quizState.industry, sub_niche: quizState.subNiche, micro_niche: quizState.microNiche }) }); const data = await res.json(); document.getElementById('generationLoading').classList.add('hidden'); if (data.error) { showToast('Campaign generation failed: ' + data.error, 'error'); setStatus('Ready', 'ready'); return; } parallelCampaigns = data.campaigns; renderCampaignCards(); setStatus('Ready', 'ready'); showToast(`Generated ${parallelCampaigns.length} campaigns!`, 'success'); } catch (e) { document.getElementById('generationLoading').classList.add('hidden'); setStatus('Ready', 'ready'); showToast('Failed to generate campaigns', 'error'); } } const CTA_MAP = { 'LEARN_MORE': 'Learn More', 'SHOP_NOW': 'Shop Now', 'SIGN_UP': 'Sign Up', 'DOWNLOAD': 'Download', 'GET_QUOTE': 'Get Quote', 'CONTACT_US': 'Contact Us', 'BOOK_NOW': 'Book Now', 'APPLY_NOW': 'Apply Now', 'NO_BUTTON': '' }; function renderCampaignCards() { const grid = document.getElementById('campaignCardsGrid'); grid.innerHTML = ''; parallelCampaigns.forEach((c, i) => { const card = document.createElement('div'); card.className = `campaign-card ${c.selected ? '' : 'deselected'}`; card.id = `campaignCard${i}`; const targeting = c.targeting || {}; const adCopy = c.ad_copy || {}; const hasImage = !!c.image_url; card.innerHTML = `
#${i + 1} ${escapeHtml(c.campaign_name || '')}
${escapeHtml(c.micro_niche || '')}
${hasImage ? `Ad image` : `
No image yet
` }
${escapeHtml(adCopy.body || '')}
🎯 Targeting: Ages ${targeting.age_min || '?'}-${targeting.age_max || '?'} · ${targeting.gender || 'All'}
Interests: ${escapeHtml(targeting.interests || 'N/A')}
Behaviors: ${escapeHtml(targeting.behaviors || 'N/A')}
📄 ${escapeHtml(c.landing_page_angle || '')}
`; grid.appendChild(card); }); } function toggleCampaign(index) { parallelCampaigns[index].selected = !parallelCampaigns[index].selected; const card = document.getElementById(`campaignCard${index}`); const toggle = card.querySelector('.campaign-toggle'); if (parallelCampaigns[index].selected) { card.classList.remove('deselected'); toggle.classList.add('active'); } else { card.classList.add('deselected'); toggle.classList.remove('active'); } // Also update gallery view if visible const galleryCard = document.getElementById(`galleryCard${index}`); if (galleryCard) { if (parallelCampaigns[index].selected) { galleryCard.classList.remove('deselected'); } else { galleryCard.classList.add('deselected'); } } } async function generateCampaignImage(index) { const campaign = parallelCampaigns[index]; const imageArea = document.getElementById(`campaignImage${index}`); const card = document.getElementById(`campaignCard${index}`); const btn = card.querySelector('.btn-generate'); if (!campaign.image_prompt) { showToast('No image prompt for this campaign', 'error'); return; } btn.disabled = true; btn.classList.add('generating'); imageArea.innerHTML = `
Generating...
`; setStatus(`Generating image ${index + 1}...`, 'loading'); try { const res = await fetch('/api/generate-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: campaign.image_prompt }) }); const data = await res.json(); if (data.error) throw new Error(data.error); campaign.image_url = data.url; imageArea.innerHTML = `Ad image`; // Update gallery view too const galleryImg = document.querySelector(`#galleryCard${index} .gallery-fb-image`); if (galleryImg) { galleryImg.innerHTML = `Ad image`; } setStatus('Ready', 'ready'); showToast(`Image generated for campaign #${index + 1}`, 'success'); } catch (e) { imageArea.innerHTML = `
Failed
`; setStatus('Ready', 'ready'); showToast(`Image failed: ${e.message}`, 'error'); } btn.disabled = false; btn.classList.remove('generating'); } async function generateAllImages() { const selected = parallelCampaigns.map((c, i) => ({ campaign: c, index: i })).filter(x => x.campaign.selected); if (selected.length === 0) { showToast('No campaigns selected', 'error'); return; } const progressBar = document.getElementById('imageProgress'); const fill = document.getElementById('progressBarFill'); const text = document.getElementById('progressText'); const btn = document.getElementById('generateAllBtn'); progressBar.classList.remove('hidden'); btn.disabled = true; let completed = 0; text.textContent = `Generating images: 0/${selected.length}`; fill.style.width = '0%'; for (const item of selected) { if (item.campaign.image_url) { completed++; text.textContent = `Generating images: ${completed}/${selected.length}`; fill.style.width = (completed / selected.length * 100) + '%'; continue; } await generateCampaignImage(item.index); completed++; text.textContent = `Generating images: ${completed}/${selected.length}`; fill.style.width = (completed / selected.length * 100) + '%'; } btn.disabled = false; text.textContent = `All ${completed} images generated!`; showToast('All images generated!', 'success'); setTimeout(() => progressBar.classList.add('hidden'), 3000); } // ═══════════════════════════════════════════════════════════════════════════ // GALLERY VIEW // ═══════════════════════════════════════════════════════════════════════════ function toggleGalleryView() { galleryMode = !galleryMode; const grid = document.getElementById('campaignCardsGrid'); const gallery = document.getElementById('galleryView'); const label = document.getElementById('galleryToggleLabel'); if (galleryMode) { grid.classList.add('hidden'); gallery.classList.remove('hidden'); label.textContent = 'Card View'; renderGallery(); } else { grid.classList.remove('hidden'); gallery.classList.add('hidden'); label.textContent = 'Gallery View'; } } function renderGallery() { const gallery = document.getElementById('galleryView'); gallery.innerHTML = ''; parallelCampaigns.forEach((c, i) => { const adCopy = c.ad_copy || {}; const ctaText = CTA_MAP[c.cta] || 'Learn More'; const card = document.createElement('div'); card.className = `gallery-ad-card ${c.selected ? '' : 'deselected'}`; card.id = `galleryCard${i}`; card.innerHTML = ` `; gallery.appendChild(card); }); } function updateCampaignField(index, field, value) { if (!parallelCampaigns[index].ad_copy) parallelCampaigns[index].ad_copy = {}; parallelCampaigns[index].ad_copy[field] = value; } // ═══════════════════════════════════════════════════════════════════════════ // CSV EXPORT (for parallel campaigns) // ═══════════════════════════════════════════════════════════════════════════ async function exportSelectedCampaigns() { // Check if we have parallel campaigns if (parallelCampaigns.length > 0) { exportParallelCampaignsCSV(); return; } // Fallback: original manual flow generateCSV(); } async function exportParallelCampaignsCSV() { const selected = parallelCampaigns.filter(c => c.selected); if (selected.length === 0) { showToast('No campaigns selected for export', 'error'); return; } // Build CSV rows const ads = selected.map(c => { const targeting = c.targeting || {}; const adCopy = c.ad_copy || {}; return { campaign_name: c.campaign_name || '', campaign_objective: 'OUTCOME_LEADS', buying_type: 'AUCTION', campaign_status: 'PAUSED', campaign_budget_optimization: 'FALSE', adset_name: c.adset_name || '', adset_status: 'PAUSED', targeting_age_min: String(targeting.age_min || 18), targeting_age_max: String(targeting.age_max || 65), targeting_genders: targeting.gender || 'All', targeting_interests: targeting.interests || '', targeting_behaviors: targeting.behaviors || '', targeting_geo_locations: 'US', ad_name: (c.campaign_name || 'Ad') + ' - Creative 1', ad_status: 'PAUSED', body: adCopy.body || '', title: adCopy.headline || '', caption: adCopy.description || '', call_to_action: c.cta || 'LEARN_MORE', image_file: c.image_url || '' }; }); setStatus('Generating CSV...', 'loading'); try { const res = await fetch('/api/generate-csv', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ads }) }); const data = await res.json(); if (data.error) throw new Error(data.error); lastExportCSVData = data; // Show CSV preview section const section = document.getElementById('csvPreviewSection'); section.classList.remove('hidden'); document.getElementById('csvExportRowCount').textContent = `${data.rows.length} campaign(s) · ${data.columns.length} columns`; // Build table const activeCols = data.columns.filter(col => data.rows.some(row => row[col] && row[col].trim()) ); let html = ''; activeCols.forEach(col => { html += ``; }); html += ''; data.rows.forEach(row => { html += ''; activeCols.forEach(col => { html += ``; }); html += ''; }); html += '
${escapeHtml(col)}
${escapeHtml(row[col] || '—')}
'; document.getElementById('csvExportTableWrap').innerHTML = html; section.scrollIntoView({ behavior: 'smooth' }); setStatus('Ready', 'ready'); showToast(`CSV generated — ${data.rows.length} row(s)`, 'success'); } catch (e) { setStatus('Ready', 'ready'); showToast('CSV generation failed: ' + e.message, 'error'); } } function downloadExportCSV() { if (!lastExportCSVData) return; const blob = new Blob([lastExportCSVData.csv_content], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = lastExportCSVData.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(`Downloaded ${lastExportCSVData.filename}`, 'success'); } // ═══════════════════════════════════════════════════════════════════════════ // ORIGINAL MANUAL BUILDER FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════ function toggleSection(id) { const body = document.getElementById(`body-${id}`); const chevron = document.getElementById(`chevron-${id}`); if (body) body.classList.toggle('collapsed'); if (chevron) chevron.classList.toggle('rotated'); } function toggleSubSection(id) { const body = document.getElementById(`body-${id}`); const chevron = document.getElementById(`chevron-${id}`); if (body) body.classList.toggle('collapsed'); if (chevron) chevron.classList.toggle('rotated'); } function toggleCBO(value) { const group = document.getElementById('cboBudgetGroup'); if (group) group.style.display = value === 'TRUE' ? '' : 'none'; } function switchTab(tab) { document.querySelectorAll('.preview-tab').forEach(t => t.classList.remove('active')); document.querySelector(`[data-tab="${tab}"]`).classList.add('active'); const previewPanel = document.getElementById('previewPanel'); const csvPanel = document.getElementById('csvPanel'); if (tab === 'preview') { previewPanel.classList.remove('hidden'); csvPanel.classList.add('hidden'); } else { previewPanel.classList.add('hidden'); csvPanel.classList.remove('hidden'); } } function updateCharCount(textarea) { const counter = textarea.parentElement.querySelector('.char-count'); if (counter) { const len = textarea.value.length; const max = textarea.maxLength || 2200; counter.textContent = `${len.toLocaleString()} / ${max.toLocaleString()}`; counter.style.color = len > max * 0.9 ? '#ef4444' : ''; } } function updatePreview() { const adCard = document.querySelectorAll('.ad-card')[previewAdIndex]; if (!adCard) return; const body = getFieldValue(adCard, 'body') || 'Your ad text will appear here.'; document.getElementById('previewBody').textContent = body; const title = getFieldValue(adCard, 'title') || 'Your Headline Here'; document.getElementById('previewHeadline').textContent = title; const caption = getFieldValue(adCard, 'caption') || 'Your description text'; document.getElementById('previewDescription').textContent = caption; const link = getFieldValue(adCard, 'link') || getFieldValue(adCard, 'display_link') || ''; let displayDomain = 'example.com'; if (link) { try { displayDomain = new URL(link).hostname; } catch { displayDomain = link.replace(/https?:\/\//, '').split('/')[0] || 'example.com'; } } document.getElementById('previewDisplayLink').textContent = getFieldValue(adCard, 'display_link') || displayDomain; const cta = getFieldValue(adCard, 'call_to_action') || 'LEARN_MORE'; const ctaBtn = document.getElementById('previewCTA'); const ctaText = CTA_MAP[cta] || 'Learn More'; ctaBtn.textContent = ctaText; ctaBtn.style.display = ctaText ? '' : 'none'; const campaignName = document.querySelector('[data-field="campaign_name"]'); if (campaignName && campaignName.value) { document.getElementById('previewPageName').textContent = campaignName.value; } } function getFieldValue(container, fieldName) { const el = container.querySelector(`[data-field="${fieldName}"]`); return el ? el.value : ''; } function addAd() { const container = document.getElementById('adsContainer'); const index = adCount++; const num = document.querySelectorAll('.ad-card').length + 1; const section = document.createElement('section'); section.className = 'form-card ad-card'; section.dataset.adIndex = index; section.innerHTML = `

Ad Creative #${num}

0 / 2,200
✨ AI Image Generation
`; container.appendChild(section); section.scrollIntoView({ behavior: 'smooth', block: 'center' }); showToast('Ad creative added', 'success'); } function removeAd(btn) { const card = btn.closest('.ad-card'); if (document.querySelectorAll('.ad-card').length <= 1) { showToast('You need at least one ad', 'error'); return; } card.remove(); document.querySelectorAll('.ad-card .ad-number').forEach((num, i) => num.textContent = `#${i + 1}`); previewAdIndex = Math.min(previewAdIndex, document.querySelectorAll('.ad-card').length - 1); updatePreview(); showToast('Ad removed', 'info'); } // ── AI Image Generation (Manual) ──────────────────────────────────────────── async function generateImage(btn) { const adCard = btn.closest('.ad-card') || btn.closest('.form-card'); const promptInput = adCard.querySelector('[data-ai-prompt]'); const container = adCard.querySelector('[data-generated-images]'); const prompt = promptInput.value.trim(); if (!prompt) { showToast('Please enter an image prompt', 'error'); promptInput.focus(); return; } btn.disabled = true; btn.classList.add('generating'); const origText = btn.innerHTML; btn.innerHTML = ` Generating...`; const placeholder = document.createElement('div'); placeholder.className = 'generating-placeholder'; placeholder.innerHTML = ''; container.appendChild(placeholder); setStatus('Generating image...', 'loading'); try { const res = await fetch('/api/generate-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }) }); const data = await res.json(); if (data.error) throw new Error(data.error); placeholder.remove(); const thumb = document.createElement('div'); thumb.className = 'generated-image-thumb'; thumb.innerHTML = `${prompt}`; thumb.onclick = () => selectImage(thumb, data.url, adCard); container.appendChild(thumb); if (container.querySelectorAll('.generated-image-thumb').length === 1) { selectImage(thumb, data.url, adCard); } setStatus('Ready', 'ready'); showToast('Image generated successfully', 'success'); } catch (err) { placeholder.remove(); setStatus('Ready', 'ready'); showToast(`Image generation failed: ${err.message}`, 'error'); } btn.disabled = false; btn.classList.remove('generating'); btn.innerHTML = origText; } function selectImage(thumb, url, adCard) { thumb.parentElement.querySelectorAll('.generated-image-thumb').forEach(t => t.classList.remove('selected')); thumb.classList.add('selected'); document.getElementById('previewImage').innerHTML = `Ad Image`; const imageFileInput = adCard.querySelector('[data-field="image_file"]'); if (imageFileInput) imageFileInput.value = url; } // ── CSV Generation (Manual) ───────────────────────────────────────────────── async function generateCSV() { const ads = collectFormData(); if (ads.length === 0) { showToast('No ad data to export', 'error'); return; } for (let i = 0; i < ads.length; i++) { if (!ads[i].campaign_name) { showToast('Campaign Name is required', 'error'); return; } if (!ads[i].adset_name) { showToast('Ad Set Name is required', 'error'); return; } if (!ads[i].ad_name) { showToast(`Ad #${i + 1}: Ad Name is required`, 'error'); return; } } setStatus('Generating CSV...', 'loading'); try { const res = await fetch('/api/generate-csv', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ads }) }); const data = await res.json(); if (data.error) throw new Error(data.error); lastCSVData = data; renderCSVPreview(data); switchTab('csv'); setStatus('Ready', 'ready'); showToast(`CSV generated — ${data.rows.length} row(s)`, 'success'); } catch (err) { setStatus('Ready', 'ready'); showToast(`CSV generation failed: ${err.message}`, 'error'); } } function collectFormData() { const ads = []; const adCards = document.querySelectorAll('.ad-card'); const sharedData = {}; const campaignSection = document.getElementById('campaignSection'); const adsetSection = document.getElementById('adsetSection'); if (campaignSection) campaignSection.querySelectorAll('[data-field]').forEach(el => { sharedData[el.dataset.field] = el.value; }); if (adsetSection) adsetSection.querySelectorAll('[data-field]').forEach(el => { sharedData[el.dataset.field] = el.value; }); adCards.forEach(card => { const adData = { ...sharedData }; card.querySelectorAll('[data-field]').forEach(el => { adData[el.dataset.field] = el.value; }); ads.push(adData); }); return ads; } function renderCSVPreview(data) { document.getElementById('csvEmpty').classList.add('hidden'); document.getElementById('csvContent').classList.remove('hidden'); document.getElementById('csvRowCount').textContent = `${data.rows.length} row(s) · ${data.columns.length} columns`; const activeCols = data.columns.filter(col => data.rows.some(row => row[col] && row[col].trim())); let html = ''; activeCols.forEach(col => { html += ``; }); html += ''; data.rows.forEach(row => { html += ''; activeCols.forEach(col => { html += ``; }); html += ''; }); html += '
${escapeHtml(col)}
${escapeHtml(row[col] || '—')}
'; document.getElementById('csvTableWrap').innerHTML = html; } function downloadCSV() { if (!lastCSVData) return; const blob = new Blob([lastCSVData.csv_content], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = lastCSVData.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(`Downloaded ${lastCSVData.filename}`, 'success'); } // ── Status & Toast ────────────────────────────────────────────────────────── function setStatus(text, type) { const badge = document.getElementById('statusBadge'); if (!badge) return; badge.textContent = text; badge.className = 'status-badge'; if (type === 'loading') badge.classList.add('loading'); if (type === 'error') badge.classList.add('error'); } function showToast(message, type = 'info') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: '✓', error: '✕', info: 'ℹ' }; toast.innerHTML = `${icons[type] || 'ℹ'}${escapeHtml(message)}`; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, 4000); } function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // ── CSS Animation Helper ──────────────────────────────────────────────────── const style = document.createElement('style'); style.textContent = `@keyframes spin { to { transform: rotate(360deg); } } .animate-spin { animation: spin 1s linear infinite; }`; document.head.appendChild(style); // ── Init ──────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { updateStepIndicator(); });