import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { AppLayout } from '../../components/layout/AppLayout';
import { leadGenApi, type AdTemplate } from '../../api/leadGen';
import { useAuth } from '../../contexts/AuthContext';
import {
ArrowLeft,
Check,
ChevronRight,
MapPin,
DollarSign,
Loader2,
AlertCircle,
FileText,
Target,
} from 'lucide-react';
type Step = 'template' | 'config' | 'preview' | 'launch';
export function CampaignBuilderPage() {
const navigate = useNavigate();
const { user } = useAuth();
const companyId = user?.company_id;
const queryClient = useQueryClient();
const [step, setStep] = useState<Step>('template');
const [selectedTemplate, setSelectedTemplate] = useState<AdTemplate | null>(null);
const [verticalFilter, setVerticalFilter] = useState<string>('');
const [config, setConfig] = useState({
city: '',
radius_miles: 25,
daily_budget: 100,
target_cpa: 50,
source_service: 'google_ads' as 'google_ads' | 'facebook_ads',
campaign_name: '',
});
// Fetch templates
const { data: templatesData, isLoading: templatesLoading } = useQuery({
queryKey: ['lead-gen-templates', verticalFilter],
queryFn: () => leadGenApi.getTemplates(verticalFilter || undefined),
});
const templates = templatesData?.data?.templates || [];
// Generate campaign mutation
const generateMutation = useMutation({
mutationFn: (payload: Parameters<typeof leadGenApi.generateCampaign>[1]) =>
leadGenApi.generateCampaign(companyId!, payload),
onSuccess: (res) => {
const ids = res.data.data.campaign_ids;
if (ids.length > 0) {
navigate(`/app/lead-gen/campaigns/${ids[0]}`);
}
},
});
const handleSelectTemplate = (t: AdTemplate) => {
setSelectedTemplate(t);
setStep('config');
// Pre-fill from template
if (t.budget_recommendation) {
setConfig(prev => ({ ...prev, daily_budget: Math.round(t.budget_recommendation! / 30) }));
}
if (t.target_cpa) {
setConfig(prev => ({ ...prev, target_cpa: t.target_cpa! }));
}
setConfig(prev => ({ ...prev, campaign_name: `${t.name} — ${config.city || 'Your City'}` }));
};
const handleLaunch = () => {
if (!selectedTemplate || !companyId) return;
generateMutation.mutate({
template_id: selectedTemplate.id,
source_service: config.source_service,
city: config.city,
radius_miles: config.radius_miles,
daily_budget: config.daily_budget,
target_cpa: config.target_cpa,
campaign_name: config.campaign_name || `${selectedTemplate.name} — ${config.city}`,
});
};
const configValid = config.city.trim() && config.daily_budget > 0 && config.radius_miles > 0;
const structure = selectedTemplate?.structure as {
ad_groups?: { name: string; keywords?: string[]; headlines?: string[]; descriptions?: string[] }[];
} | null;
const steps: { key: Step; label: string }[] = [
{ key: 'template', label: 'Template' },
{ key: 'config', label: 'Config' },
{ key: 'preview', label: 'Preview' },
{ key: 'launch', label: 'Launch' },
];
const currentStepIdx = steps.findIndex(s => s.key === step);
return (
<AppLayout title="Campaign Builder">
<div className="max-w-5xl mx-auto">
{/* Header */}
<div className="flex items-center gap-4 mb-8">
<button
type="button"
onClick={() => navigate('/app/lead-gen')}
className="p-2 hover:bg-surface-100 rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</button>
<div className="flex-1">
<h1 className="text-2xl font-bold text-surface-900">New Campaign</h1>
<p className="text-sm text-surface-500">
Select a template, configure settings, and launch on Google or Facebook Ads.
</p>
</div>
</div>
{/* Step indicator */}
<div className="flex items-center gap-0 mb-8">
{steps.map((s, i) => (
<div key={s.key} className="flex items-center">
<div
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
i <= currentStepIdx
? 'bg-brand-50 text-brand-700'
: 'bg-surface-100 text-surface-400'
}`}
>
<span
className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold ${
i < currentStepIdx
? 'bg-brand-600 text-white'
: i === currentStepIdx
? 'bg-brand-200 text-brand-700'
: 'bg-surface-200 text-surface-500'
}`}
>
{i < currentStepIdx ? <Check className="h-3 w-3" /> : i + 1}
</span>
{s.label}
</div>
{i < steps.length - 1 && (
<ChevronRight className="h-4 w-4 text-surface-300 mx-1" />
)}
</div>
))}
</div>
{/* Step 1: Template Selection */}
{step === 'template' && (
<div>
{/* Vertical filter */}
<div className="mb-6">
<label className="block text-sm font-medium text-surface-700 mb-2">
Filter by vertical
</label>
<select
value={verticalFilter}
onChange={e => setVerticalFilter(e.target.value)}
className="w-full sm:w-64 rounded-lg border border-surface-200 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-100"
>
<option value="">All verticals</option>
<option value="roofing">Roofing</option>
<option value="hvac">HVAC</option>
<option value="plumbing">Plumbing</option>
</select>
</div>
{templatesLoading ? (
<div className="flex justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-brand-500" />
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2">
{templates.map(t => (
<button
key={t.id}
type="button"
onClick={() => handleSelectTemplate(t)}
className="group text-left rounded-xl border-2 border-surface-200 p-5 hover:border-brand-300 hover:shadow-md transition-all"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<FileText className="h-5 w-5 text-brand-500" />
<h3 className="font-semibold text-surface-900">{t.name}</h3>
</div>
<span className="inline-flex items-center rounded-full bg-surface-100 px-2 py-0.5 text-xs font-medium text-surface-600 capitalize">
{t.vertical}
</span>
</div>
{t.description && (
<p className="text-sm text-surface-500 mb-3">{t.description}</p>
)}
<div className="flex items-center gap-4 text-xs text-surface-400">
{t.budget_recommendation && (
<span>~${t.budget_recommendation.toLocaleString()}/mo</span>
)}
{t.target_cpa && <span>Target CPA: ${t.target_cpa}</span>}
</div>
<div className="mt-3 text-sm font-medium text-brand-600 opacity-0 group-hover:opacity-100 transition-opacity">
Select template →
</div>
</button>
))}
</div>
)}
</div>
)}
{/* Step 2: Configuration */}
{step === 'config' && selectedTemplate && (
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2 space-y-6">
{/* Template summary */}
<div className="rounded-xl border border-brand-200 bg-brand-50 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-brand-900">{selectedTemplate.name}</p>
<p className="text-xs text-brand-600 capitalize">{selectedTemplate.vertical}</p>
</div>
<button
type="button"
onClick={() => setStep('template')}
className="text-xs font-medium text-brand-600 hover:text-brand-800"
>
Change
</button>
</div>
</div>
{/* Campaign name */}
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">
Campaign Name
</label>
<input
type="text"
value={config.campaign_name}
onChange={e => setConfig(prev => ({ ...prev, campaign_name: e.target.value }))}
className="w-full rounded-lg border border-surface-200 px-3 py-2 text-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-100"
placeholder="e.g. Emergency Roof Repair — Austin, TX"
/>
</div>
{/* Platform */}
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">
Platform
</label>
<div className="grid grid-cols-2 gap-3">
{(['google_ads', 'facebook_ads'] as const).map(platform => (
<button
key={platform}
type="button"
onClick={() => setConfig(prev => ({ ...prev, source_service: platform }))}
className={`rounded-lg border-2 px-4 py-3 text-sm font-medium transition-all ${
config.source_service === platform
? 'border-brand-500 bg-brand-50 text-brand-700'
: 'border-surface-200 text-surface-600 hover:border-surface-300'
}`}
>
{platform === 'google_ads' ? 'Google Ads' : 'Facebook Ads'}
</button>
))}
</div>
</div>
{/* Location */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">
<MapPin className="h-4 w-4 inline mr-1" />
City
</label>
<input
type="text"
value={config.city}
onChange={e => setConfig(prev => ({ ...prev, city: e.target.value }))}
className="w-full rounded-lg border border-surface-200 px-3 py-2 text-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-100"
placeholder="City name"
/>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">
Radius (miles)
</label>
<input
type="number"
min={5}
max={100}
value={config.radius_miles}
onChange={e => setConfig(prev => ({ ...prev, radius_miles: Number(e.target.value) }))}
className="w-full rounded-lg border border-surface-200 px-3 py-2 text-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-100"
/>
</div>
</div>
{/* Budget */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">
<DollarSign className="h-4 w-4 inline mr-1" />
Daily Budget
</label>
<input
type="number"
min={10}
max={500}
value={config.daily_budget}
onChange={e => setConfig(prev => ({ ...prev, daily_budget: Number(e.target.value) }))}
className="w-full rounded-lg border border-surface-200 px-3 py-2 text-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-100"
/>
<p className="text-xs text-surface-400 mt-1">Max $500/day (safety cap)</p>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">
<Target className="h-4 w-4 inline mr-1" />
Target CPA
</label>
<input
type="number"
min={10}
value={config.target_cpa}
onChange={e => setConfig(prev => ({ ...prev, target_cpa: Number(e.target.value) }))}
className="w-full rounded-lg border border-surface-200 px-3 py-2 text-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-100"
/>
</div>
</div>
<button
type="button"
onClick={() => setStep('preview')}
disabled={!configValid}
className="w-full sm:w-auto px-6 py-2.5 rounded-lg bg-brand-600 text-white font-medium text-sm hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Preview Campaign →
</button>
</div>
{/* Summary sidebar */}
<div className="rounded-xl border border-surface-200 p-5 bg-white">
<h3 className="font-semibold text-surface-900 mb-4">Summary</h3>
<dl className="space-y-3 text-sm">
<div className="flex justify-between">
<dt className="text-surface-500">Daily budget</dt>
<dd className="font-medium">${config.daily_budget}</dd>
</div>
<div className="flex justify-between">
<dt className="text-surface-500">Monthly est.</dt>
<dd className="font-medium">${(config.daily_budget * 30).toLocaleString()}</dd>
</div>
<div className="flex justify-between">
<dt className="text-surface-500">Target CPA</dt>
<dd className="font-medium">${config.target_cpa}</dd>
</div>
<div className="flex justify-between">
<dt className="text-surface-500">Target</dt>
<dd className="font-medium capitalize">{config.city}, {config.radius_miles}mi</dd>
</div>
<div className="flex justify-between">
<dt className="text-surface-500">Platform</dt>
<dd className="font-medium capitalize">
{config.source_service === 'google_ads' ? 'Google Ads' : 'Facebook Ads'}
</dd>
</div>
</dl>
</div>
</div>
)}
{/* Step 3: Preview */}
{step === 'preview' && selectedTemplate && (
<div>
<div className="mb-6">
<button
type="button"
onClick={() => setStep('config')}
className="text-sm font-medium text-surface-500 hover:text-surface-700"
>
← Back to config
</button>
</div>
<div className="rounded-xl border border-surface-200 overflow-hidden">
<div className="border-b border-surface-200 px-6 py-4 bg-surface-50">
<h2 className="font-semibold text-surface-900">
{config.campaign_name || `${selectedTemplate.name} — ${config.city}`}
</h2>
<p className="text-sm text-surface-500">
{config.source_service === 'google_ads' ? 'Google Ads' : 'Facebook Ads'} · {config.city} · ${config.daily_budget}/day
</p>
</div>
{structure?.ad_groups?.map((ag, i) => (
<div key={i} className="border-b border-surface-200 p-6 last:border-b-0">
<h3 className="font-medium text-surface-900 mb-3">
Ad Group {i + 1}: {ag.name}
</h3>
{/* Keywords */}
{ag.keywords && ag.keywords.length > 0 && (
<div className="mb-4">
<p className="text-xs font-medium uppercase tracking-wider text-surface-400 mb-2">
Keywords ({ag.keywords.length})
</p>
<div className="flex flex-wrap gap-2">
{ag.keywords.map((kw: string, j: number) => (
<span
key={j}
className="inline-flex items-center rounded-full bg-surface-100 px-2.5 py-1 text-xs font-medium text-surface-600"
>
{kw.replace(/{city}/g, config.city).replace(/{state}/g, '')}
</span>
))}
</div>
</div>
)}
{/* Headlines */}
{ag.headlines && ag.headlines.length > 0 && (
<div className="mb-3">
<p className="text-xs font-medium uppercase tracking-wider text-surface-400 mb-2">
Headlines ({ag.headlines.length})
</p>
<ul className="space-y-1">
{ag.headlines.map((h: string, j: number) => (
<li key={j} className="text-sm text-surface-700">
{h.replace(/{city}/g, config.city).replace(/{company}/g, user?.company_name || 'Your Company')}
</li>
))}
</ul>
</div>
)}
{/* Descriptions */}
{ag.descriptions && ag.descriptions.length > 0 && (
<div>
<p className="text-xs font-medium uppercase tracking-wider text-surface-400 mb-2">
Descriptions ({ag.descriptions.length})
</p>
<ul className="space-y-1">
{ag.descriptions.map((d: string, j: number) => (
<li key={j} className="text-sm text-surface-600">
{d.replace(/{city}/g, config.city).replace(/{company}/g, user?.company_name || 'Your Company')}
</li>
))}
</ul>
</div>
)}
</div>
))}
</div>
{/* Safety notice */}
<div className="mt-6 flex items-start gap-3 rounded-lg bg-amber-50 border border-amber-200 p-4">
<AlertCircle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-800">Safety caps enforced</p>
<p className="text-xs text-amber-700 mt-1">
Daily budget is capped at $500 and monthly at $5,000. This campaign will spend
~${(config.daily_budget * 30).toLocaleString()}/month based on the daily budget.
</p>
</div>
</div>
<div className="mt-6 flex items-center gap-4">
<button
type="button"
onClick={handleLaunch}
disabled={!configValid || generateMutation.isPending}
className="px-8 py-2.5 rounded-lg bg-brand-600 text-white font-medium text-sm hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
{generateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
{generateMutation.isPending ? 'Launching...' : 'Launch Campaign'}
</button>
{generateMutation.error && (
<p className="text-sm text-red-600">
{generateMutation.error instanceof Error ? generateMutation.error.message : 'Launch failed'}
</p>
)}
</div>
</div>
)}
</div>
</AppLayout>
);
}