1048 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ── 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 = `
<div class="campaign-card-header">
<div>
<span class="campaign-card-number">#${i + 1}</span>
<span class="campaign-card-title">${escapeHtml(c.campaign_name || '')}</span>
</div>
<button class="campaign-toggle ${c.selected ? 'active' : ''}" onclick="toggleCampaign(${i})" title="Toggle campaign"></button>
</div>
<div class="campaign-card-body">
<div class="campaign-micro-niche">${escapeHtml(c.micro_niche || '')}</div>
<div class="campaign-image-area" id="campaignImage${i}">
${hasImage
? `<img src="${c.image_url}" alt="Ad image">`
: `<div class="campaign-image-placeholder">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<span>No image yet</span>
</div>`
}
</div>
<div class="campaign-ad-copy">${escapeHtml(adCopy.body || '')}</div>
<div class="campaign-targeting">
<strong>🎯 Targeting:</strong> Ages ${targeting.age_min || '?'}-${targeting.age_max || '?'} · ${targeting.gender || 'All'}<br>
<strong>Interests:</strong> ${escapeHtml(targeting.interests || 'N/A')}<br>
<strong>Behaviors:</strong> ${escapeHtml(targeting.behaviors || 'N/A')}
</div>
<div class="campaign-landing">📄 ${escapeHtml(c.landing_page_angle || '')}</div>
<div class="campaign-card-actions">
<button class="btn-generate" onclick="generateCampaignImage(${i})" ${hasImage ? 'style="opacity:0.6"' : ''}>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
${hasImage ? 'Regenerate' : 'Generate'} Image
</button>
</div>
</div>
`;
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 = `<div class="campaign-image-placeholder"><div class="ai-spinner"></div><span>Generating...</span></div>`;
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 = `<img src="${data.url}" alt="Ad image">`;
// Update gallery view too
const galleryImg = document.querySelector(`#galleryCard${index} .gallery-fb-image`);
if (galleryImg) {
galleryImg.innerHTML = `<img src="${data.url}" alt="Ad image">`;
}
setStatus('Ready', 'ready');
showToast(`Image generated for campaign #${index + 1}`, 'success');
} catch (e) {
imageArea.innerHTML = `<div class="campaign-image-placeholder"><span style="color:var(--red)">Failed</span></div>`;
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 = `
<div class="gallery-phone-frame">
<div class="gallery-fb-post">
<div class="gallery-fb-header">
<div class="gallery-fb-avatar"></div>
<div>
<div class="gallery-fb-page">${escapeHtml(c.campaign_name || 'Campaign')}</div>
<div class="gallery-fb-sponsored">Sponsored · 🌐</div>
</div>
</div>
<div class="gallery-fb-body" contenteditable="true" data-campaign="${i}" data-field="body" onblur="updateCampaignField(${i}, 'body', this.textContent)">${escapeHtml(adCopy.body || '')}</div>
<div class="gallery-fb-image">
${c.image_url
? `<img src="${c.image_url}" alt="Ad">`
: `<div class="campaign-image-placeholder" style="height:100%;display:flex;align-items:center;justify-content:center"><span>No image</span></div>`
}
</div>
<div class="gallery-fb-link-bar">
<div>
<div class="gallery-fb-headline" contenteditable="true" onblur="updateCampaignField(${i}, 'headline', this.textContent)">${escapeHtml(adCopy.headline || '')}</div>
<div class="gallery-fb-description" contenteditable="true" onblur="updateCampaignField(${i}, 'description', this.textContent)">${escapeHtml(adCopy.description || '')}</div>
</div>
<button class="gallery-fb-cta">${ctaText}</button>
</div>
</div>
</div>
<div class="gallery-card-controls">
<span class="gallery-niche-label">${escapeHtml(c.micro_niche || '')}</span>
<button class="campaign-toggle ${c.selected ? 'active' : ''}" onclick="toggleCampaign(${i})"></button>
</div>
`;
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 = '<table><thead><tr>';
activeCols.forEach(col => { html += `<th>${escapeHtml(col)}</th>`; });
html += '</tr></thead><tbody>';
data.rows.forEach(row => {
html += '<tr>';
activeCols.forEach(col => {
html += `<td title="${escapeHtml(row[col] || '')}">${escapeHtml(row[col] || '—')}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
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 = `
<div class="card-header" onclick="toggleSection('ad-${index}')">
<div class="flex items-center gap-2">
<div class="section-icon ad-icon"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg></div>
<h2 class="card-title">Ad Creative <span class="ad-number">#${num}</span></h2>
</div>
<div class="flex items-center gap-2">
<button class="btn-delete-ad" onclick="event.stopPropagation(); removeAd(this);">✕ Remove</button>
<svg class="chevron w-5 h-5 text-gray-400" id="chevron-ad-${index}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</div>
</div>
<div class="card-body" id="body-ad-${index}">
<div class="field-grid">
<div class="field-group col-span-2"><label class="field-label">Ad Name <span class="req">*</span></label><input type="text" class="field-input" data-field="ad_name" placeholder="e.g. Ad ${num}" oninput="updatePreview()"></div>
<div class="field-group"><label class="field-label">Status</label><select class="field-input" data-field="ad_status"><option value="PAUSED">Paused</option><option value="ACTIVE">Active</option></select></div>
<div class="field-group"><label class="field-label">Call to Action</label><select class="field-input" data-field="call_to_action" onchange="updatePreview()"><option value="">— None —</option><option value="LEARN_MORE">Learn More</option><option value="SHOP_NOW">Shop Now</option><option value="SIGN_UP">Sign Up</option><option value="DOWNLOAD">Download</option><option value="GET_QUOTE">Get Quote</option><option value="CONTACT_US">Contact Us</option><option value="BOOK_NOW">Book Now</option><option value="APPLY_NOW">Apply Now</option></select></div>
<div class="field-group col-span-2"><label class="field-label">Body Text</label><textarea class="field-input field-textarea" data-field="body" placeholder="Primary text..." rows="3" maxlength="2200" oninput="updatePreview(); updateCharCount(this)"></textarea><span class="char-count">0 / 2,200</span></div>
<div class="field-group"><label class="field-label">Headline</label><input type="text" class="field-input" data-field="title" placeholder="Headline" maxlength="255" oninput="updatePreview()"></div>
<div class="field-group"><label class="field-label">Description</label><input type="text" class="field-input" data-field="caption" placeholder="Description" maxlength="255" oninput="updatePreview()"></div>
<div class="field-group col-span-2"><label class="field-label">Link URL</label><input type="url" class="field-input" data-field="link" placeholder="https://..." oninput="updatePreview()"></div>
</div>
<div class="ai-gen-section">
<div class="ai-gen-header"><span class="text-sm font-semibold text-amber-400">✨ AI Image Generation</span></div>
<div class="ai-gen-body">
<div class="flex gap-2">
<input type="text" class="field-input flex-1 ai-prompt-input" data-ai-prompt placeholder="Describe the ad image...">
<button class="btn-generate" onclick="generateImage(this)"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>Generate</button>
</div>
<div class="generated-images" data-generated-images></div>
</div>
</div>
</div>
`;
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 = `<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg> Generating...`;
const placeholder = document.createElement('div');
placeholder.className = 'generating-placeholder';
placeholder.innerHTML = '<svg class="w-5 h-5 text-gray-500 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>';
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 = `<img src="${data.url}" alt="${prompt}" loading="lazy">`;
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 = `<img src="${url}" alt="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 = '<table><thead><tr>';
activeCols.forEach(col => { html += `<th>${escapeHtml(col)}</th>`; });
html += '</tr></thead><tbody>';
data.rows.forEach(row => {
html += '<tr>';
activeCols.forEach(col => { html += `<td title="${escapeHtml(row[col] || '')}">${escapeHtml(row[col] || '—')}</td>`; });
html += '</tr>';
});
html += '</tbody></table>';
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 = `<span>${icons[type] || ''}</span><span>${escapeHtml(message)}</span>`;
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();
});