Files
marketing-app/client/src/pages/Campaigns.jsx
2026-02-10 21:03:36 +03:00

655 lines
31 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.
import { useState, useEffect, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import { Plus, Search, TrendingUp, DollarSign, Eye, MousePointer, Target, BarChart3 } from 'lucide-react'
import { format } from 'date-fns'
import { AppContext } from '../App'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../i18n/LanguageContext'
import { api, PLATFORMS } from '../utils/api'
import { PlatformIcons } from '../components/PlatformIcon'
import StatusBadge from '../components/StatusBadge'
import BrandBadge from '../components/BrandBadge'
import Modal from '../components/Modal'
import BudgetBar from '../components/BudgetBar'
import InteractiveTimeline from '../components/InteractiveTimeline'
const EMPTY_CAMPAIGN = {
name: '', description: '', brand_id: '', status: 'planning',
start_date: '', end_date: '', budget: '', goals: '', platforms: [],
budget_spent: '', revenue: '', impressions: '', clicks: '', conversions: '', cost_per_click: '', notes: '',
}
function ROIBadge({ revenue, spent }) {
if (!spent || spent <= 0) return null
const roi = ((revenue - spent) / spent * 100).toFixed(0)
const color = roi >= 0 ? 'text-emerald-600 bg-emerald-50' : 'text-red-600 bg-red-50'
return (
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${color}`}>
ROI {roi}%
</span>
)
}
function MetricCard({ icon: Icon, label, value, color = 'text-text-primary' }) {
return (
<div className="bg-surface-secondary rounded-lg p-3 text-center">
<Icon className={`w-4 h-4 mx-auto mb-1 ${color}`} />
<div className={`text-sm font-bold ${color}`}>{value || '—'}</div>
<div className="text-[10px] text-text-tertiary">{label}</div>
</div>
)
}
export default function Campaigns() {
const { brands, getBrandName } = useContext(AppContext)
const { lang } = useLanguage()
const { permissions } = useAuth()
const navigate = useNavigate()
const [campaigns, setCampaigns] = useState([])
const [loading, setLoading] = useState(true)
const [showModal, setShowModal] = useState(false)
const [editingCampaign, setEditingCampaign] = useState(null)
const [formData, setFormData] = useState(EMPTY_CAMPAIGN)
const [filters, setFilters] = useState({ brand: '', status: '' })
const [activeTab, setActiveTab] = useState('details') // details | performance
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
useEffect(() => { loadCampaigns() }, [])
const loadCampaigns = async () => {
try {
const res = await api.get('/campaigns')
setCampaigns(res.data || res || [])
} catch (err) {
console.error('Failed to load campaigns:', err)
} finally {
setLoading(false)
}
}
const handleSave = async () => {
try {
const data = {
name: formData.name,
description: formData.description,
brand_id: formData.brand_id ? Number(formData.brand_id) : null,
status: formData.status,
start_date: formData.start_date,
end_date: formData.end_date,
budget: formData.budget ? Number(formData.budget) : null,
goals: formData.goals,
platforms: formData.platforms || [],
budget_spent: formData.budget_spent ? Number(formData.budget_spent) : 0,
revenue: formData.revenue ? Number(formData.revenue) : 0,
impressions: formData.impressions ? Number(formData.impressions) : 0,
clicks: formData.clicks ? Number(formData.clicks) : 0,
conversions: formData.conversions ? Number(formData.conversions) : 0,
cost_per_click: formData.cost_per_click ? Number(formData.cost_per_click) : 0,
notes: formData.notes || '',
}
if (editingCampaign) {
await api.patch(`/campaigns/${editingCampaign.id || editingCampaign._id}`, data)
} else {
await api.post('/campaigns', data)
}
setShowModal(false)
setEditingCampaign(null)
setFormData(EMPTY_CAMPAIGN)
loadCampaigns()
} catch (err) {
console.error('Save failed:', err)
}
}
const openEdit = (campaign) => {
setEditingCampaign(campaign)
setFormData({
name: campaign.name || '',
description: campaign.description || '',
brand_id: campaign.brandId || campaign.brand_id || '',
status: campaign.status || 'planning',
start_date: campaign.startDate ? new Date(campaign.startDate).toISOString().slice(0, 10) : '',
end_date: campaign.endDate ? new Date(campaign.endDate).toISOString().slice(0, 10) : '',
budget: campaign.budget || '',
goals: campaign.goals || '',
platforms: campaign.platforms || [],
budget_spent: campaign.budgetSpent || campaign.budget_spent || '',
revenue: campaign.revenue || '',
impressions: campaign.impressions || '',
clicks: campaign.clicks || '',
conversions: campaign.conversions || '',
cost_per_click: campaign.costPerClick || campaign.cost_per_click || '',
notes: campaign.notes || '',
})
setActiveTab('details')
setShowModal(true)
}
const openNew = () => {
setEditingCampaign(null)
setFormData(EMPTY_CAMPAIGN)
setActiveTab('details')
setShowModal(true)
}
const filtered = campaigns.filter(c => {
if (filters.brand && String(c.brandId || c.brand_id) !== filters.brand) return false
if (filters.status && c.status !== filters.status) return false
return true
})
// Aggregate stats
const totalBudget = filtered.reduce((sum, c) => sum + (c.budget || 0), 0)
const totalSpent = filtered.reduce((sum, c) => sum + (c.budgetSpent || c.budget_spent || 0), 0)
const totalImpressions = filtered.reduce((sum, c) => sum + (c.impressions || 0), 0)
const totalClicks = filtered.reduce((sum, c) => sum + (c.clicks || 0), 0)
const totalConversions = filtered.reduce((sum, c) => sum + (c.conversions || 0), 0)
const totalRevenue = filtered.reduce((sum, c) => sum + (c.revenue || 0), 0)
if (loading) {
return (
<div className="space-y-4 animate-pulse">
<div className="h-12 bg-surface-tertiary rounded-xl"></div>
<div className="h-[400px] bg-surface-tertiary rounded-xl"></div>
</div>
)
}
return (
<div className="space-y-6 animate-fade-in">
{/* Toolbar */}
<div className="flex flex-wrap items-center gap-3">
<select
value={filters.brand}
onChange={e => setFilters(f => ({ ...f, brand: e.target.value }))}
className="text-sm border border-border rounded-lg px-3 py-2 bg-white text-text-secondary focus:outline-none"
>
<option value="">All Brands</option>
{brands.map(b => <option key={b.id || b._id} value={b.id || b._id}>{lang === 'ar' && b.name_ar ? b.name_ar : b.name}</option>)}
</select>
<select
value={filters.status}
onChange={e => setFilters(f => ({ ...f, status: e.target.value }))}
className="text-sm border border-border rounded-lg px-3 py-2 bg-white text-text-secondary focus:outline-none"
>
<option value="">All Statuses</option>
<option value="planning">Planning</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
{permissions?.canCreateCampaigns && (
<button
onClick={openNew}
className="flex items-center gap-2 px-4 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light shadow-sm ml-auto"
>
<Plus className="w-4 h-4" />
New Campaign
</button>
)}
</div>
{/* Summary Cards */}
{(totalBudget > 0 || totalSpent > 0) && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
<div className="bg-white rounded-xl border border-border p-4">
<div className="flex items-center gap-2 mb-1">
<DollarSign className="w-4 h-4 text-blue-500" />
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Budget</span>
</div>
<div className="text-lg font-bold text-text-primary">{totalBudget.toLocaleString()}</div>
<div className="text-[10px] text-text-tertiary">SAR total</div>
</div>
<div className="bg-white rounded-xl border border-border p-4">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-4 h-4 text-amber-500" />
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Spent</span>
</div>
<div className="text-lg font-bold text-text-primary">{totalSpent.toLocaleString()}</div>
<div className="text-[10px] text-text-tertiary">SAR spent</div>
</div>
<div className="bg-white rounded-xl border border-border p-4">
<div className="flex items-center gap-2 mb-1">
<Eye className="w-4 h-4 text-purple-500" />
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Impressions</span>
</div>
<div className="text-lg font-bold text-text-primary">{totalImpressions.toLocaleString()}</div>
</div>
<div className="bg-white rounded-xl border border-border p-4">
<div className="flex items-center gap-2 mb-1">
<MousePointer className="w-4 h-4 text-green-500" />
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Clicks</span>
</div>
<div className="text-lg font-bold text-text-primary">{totalClicks.toLocaleString()}</div>
</div>
<div className="bg-white rounded-xl border border-border p-4">
<div className="flex items-center gap-2 mb-1">
<Target className="w-4 h-4 text-red-500" />
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Conversions</span>
</div>
<div className="text-lg font-bold text-text-primary">{totalConversions.toLocaleString()}</div>
</div>
<div className="bg-white rounded-xl border border-border p-4">
<div className="flex items-center gap-2 mb-1">
<BarChart3 className="w-4 h-4 text-emerald-500" />
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Revenue</span>
</div>
<div className="text-lg font-bold text-text-primary">{totalRevenue.toLocaleString()}</div>
<div className="text-[10px] text-text-tertiary">SAR</div>
</div>
</div>
)}
{/* Timeline */}
<InteractiveTimeline
items={filtered}
mapItem={(campaign) => ({
id: campaign._id || campaign.id,
label: campaign.name,
description: campaign.description,
startDate: campaign.startDate || campaign.start_date || campaign.createdAt,
endDate: campaign.endDate || campaign.end_date,
status: campaign.status,
assigneeName: campaign.brandName || campaign.brand_name,
tags: campaign.platforms || [],
})}
onDateChange={async (campaignId, { startDate, endDate }) => {
try {
await api.patch(`/campaigns/${campaignId}`, { start_date: startDate, end_date: endDate })
} catch (err) {
console.error('Timeline date update failed:', err)
} finally {
loadCampaigns()
}
}}
onItemClick={(campaign) => {
navigate(`/campaigns/${campaign._id || campaign.id}`)
}}
/>
{/* Campaign list */}
<div className="bg-white rounded-xl border border-border overflow-hidden">
<div className="px-5 py-4 border-b border-border">
<h3 className="font-semibold text-text-primary">All Campaigns</h3>
</div>
<div className="divide-y divide-border-light">
{filtered.length === 0 ? (
<div className="py-12 text-center text-sm text-text-tertiary">
No campaigns found
</div>
) : (
filtered.map(campaign => {
const spent = campaign.budgetSpent || campaign.budget_spent || 0
const budget = campaign.budget || 0
return (
<div
key={campaign.id || campaign._id}
onClick={() => navigate(`/campaigns/${campaign.id || campaign._id}`)}
className="relative px-5 py-4 hover:bg-surface-secondary cursor-pointer transition-colors"
>
<div className="flex items-center gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="text-sm font-semibold text-text-primary">{campaign.name}</h4>
{(campaign.brand_id || campaign.brandName) && <BrandBadge brand={getBrandName(campaign.brand_id) || campaign.brandName} />}
<ROIBadge revenue={campaign.revenue || 0} spent={spent} />
</div>
{campaign.description && (
<p className="text-xs text-text-secondary line-clamp-1">{campaign.description}</p>
)}
<div className="flex items-center gap-3 mt-1.5">
{budget > 0 && (
<div className="w-32">
<BudgetBar budget={budget} spent={spent} />
</div>
)}
{(campaign.impressions > 0 || campaign.clicks > 0) && (
<div className="flex items-center gap-3 text-[10px] text-text-tertiary">
{campaign.impressions > 0 && <span>👁 {campaign.impressions.toLocaleString()}</span>}
{campaign.clicks > 0 && <span>🖱 {campaign.clicks.toLocaleString()}</span>}
{campaign.conversions > 0 && <span>🎯 {campaign.conversions.toLocaleString()}</span>}
</div>
)}
</div>
</div>
<div className="text-right shrink-0">
<StatusBadge status={campaign.status} size="xs" />
<div className="text-xs text-text-tertiary mt-1">
{campaign.startDate && campaign.endDate ? (
<>
{format(new Date(campaign.startDate), 'MMM d')} {format(new Date(campaign.endDate), 'MMM d, yyyy')}
</>
) : '—'}
</div>
</div>
</div>
{campaign.platforms && campaign.platforms.length > 0 && (
<div className="flex justify-end mt-2">
<PlatformIcons platforms={campaign.platforms} size={16} />
</div>
)}
</div>
)
})
)}
</div>
</div>
{/* Create/Edit Modal */}
<Modal
isOpen={showModal}
onClose={() => { setShowModal(false); setEditingCampaign(null) }}
title={editingCampaign ? 'Edit Campaign' : 'Create Campaign'}
size="lg"
>
<div className="space-y-4">
{/* Tabs */}
{editingCampaign && (
<div className="flex gap-1 p-1 bg-surface-tertiary rounded-lg">
<button
onClick={() => setActiveTab('details')}
className={`flex-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
activeTab === 'details' ? 'bg-white text-text-primary shadow-sm' : 'text-text-tertiary hover:text-text-secondary'
}`}
>
Details
</button>
<button
onClick={() => setActiveTab('performance')}
className={`flex-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
activeTab === 'performance' ? 'bg-white text-text-primary shadow-sm' : 'text-text-tertiary hover:text-text-secondary'
}`}
>
Performance & ROI
</button>
</div>
)}
{activeTab === 'details' ? (
<>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Name *</label>
<input
type="text"
value={formData.name}
onChange={e => setFormData(f => ({ ...f, name: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Campaign name"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Description</label>
<textarea
value={formData.description}
onChange={e => setFormData(f => ({ ...f, description: e.target.value }))}
rows={3}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary resize-none"
placeholder="Campaign description..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Brand</label>
<select
value={formData.brand_id}
onChange={e => setFormData(f => ({ ...f, brand_id: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
>
<option value="">Select brand</option>
{brands.map(b => <option key={b.id || b._id} value={b.id || b._id}>{b.icon} {lang === 'ar' && b.name_ar ? b.name_ar : b.name}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Status</label>
<select
value={formData.status}
onChange={e => setFormData(f => ({ ...f, status: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
>
<option value="planning">Planning</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
{/* Platforms multi-select */}
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Platforms</label>
<div className="flex flex-wrap gap-2 p-2 border border-border rounded-lg bg-white min-h-[42px]">
{Object.entries(PLATFORMS).map(([k, v]) => {
const checked = (formData.platforms || []).includes(k)
return (
<label
key={k}
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-full cursor-pointer border transition-colors ${
checked
? 'bg-brand-primary/10 border-brand-primary/30 text-brand-primary font-medium'
: 'bg-surface-tertiary border-transparent text-text-secondary hover:bg-surface-tertiary/80'
}`}
>
<input
type="checkbox"
checked={checked}
onChange={() => {
setFormData(f => ({
...f,
platforms: checked
? f.platforms.filter(p => p !== k)
: [...(f.platforms || []), k]
}))
}}
className="sr-only"
/>
{v.label}
</label>
)
})}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Start Date *</label>
<input
type="date"
value={formData.start_date}
onChange={e => setFormData(f => ({ ...f, start_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">End Date *</label>
<input
type="date"
value={formData.end_date}
onChange={e => setFormData(f => ({ ...f, end_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">
Budget (SAR)
{!permissions?.canSetBudget && <span className="text-[10px] text-text-tertiary ml-1">(Superadmin only)</span>}
</label>
<input
type="number"
value={formData.budget}
onChange={e => setFormData(f => ({ ...f, budget: e.target.value }))}
disabled={!permissions?.canSetBudget}
className={`w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary ${!permissions?.canSetBudget ? 'bg-surface-tertiary text-text-tertiary cursor-not-allowed' : ''}`}
placeholder="e.g., 50000"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Goals</label>
<input
type="text"
value={formData.goals}
onChange={e => setFormData(f => ({ ...f, goals: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Campaign goals"
/>
</div>
</div>
</>
) : (
/* Performance & ROI Tab */
<>
{/* Live metrics summary */}
{(formData.budget_spent || formData.impressions || formData.clicks) && (
<div className="grid grid-cols-4 gap-2 mb-2">
<MetricCard icon={DollarSign} label="Spent" value={formData.budget_spent ? `${Number(formData.budget_spent).toLocaleString()} SAR` : null} color="text-amber-600" />
<MetricCard icon={Eye} label="Impressions" value={formData.impressions ? Number(formData.impressions).toLocaleString() : null} color="text-purple-600" />
<MetricCard icon={MousePointer} label="Clicks" value={formData.clicks ? Number(formData.clicks).toLocaleString() : null} color="text-blue-600" />
<MetricCard icon={Target} label="Conversions" value={formData.conversions ? Number(formData.conversions).toLocaleString() : null} color="text-emerald-600" />
</div>
)}
{formData.budget && formData.budget_spent && (
<div className="p-3 bg-surface-secondary rounded-lg">
<BudgetBar budget={Number(formData.budget)} spent={Number(formData.budget_spent)} />
<div className="flex items-center gap-2 mt-2">
<ROIBadge revenue={Number(formData.revenue) || 0} spent={Number(formData.budget_spent) || 0} />
{formData.clicks > 0 && formData.budget_spent > 0 && (
<span className="text-[10px] text-text-tertiary">
CPC: {(Number(formData.budget_spent) / Number(formData.clicks)).toFixed(2)} SAR
</span>
)}
{formData.impressions > 0 && formData.clicks > 0 && (
<span className="text-[10px] text-text-tertiary">
CTR: {(Number(formData.clicks) / Number(formData.impressions) * 100).toFixed(2)}%
</span>
)}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Budget Spent (SAR)</label>
<input
type="number"
value={formData.budget_spent}
onChange={e => setFormData(f => ({ ...f, budget_spent: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Amount spent so far"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Revenue (SAR)</label>
<input
type="number"
value={formData.revenue}
onChange={e => setFormData(f => ({ ...f, revenue: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Revenue generated"
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Impressions</label>
<input
type="number"
value={formData.impressions}
onChange={e => setFormData(f => ({ ...f, impressions: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Total impressions"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Clicks</label>
<input
type="number"
value={formData.clicks}
onChange={e => setFormData(f => ({ ...f, clicks: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Total clicks"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Conversions</label>
<input
type="number"
value={formData.conversions}
onChange={e => setFormData(f => ({ ...f, conversions: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Conversions (visits, tickets...)"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Notes</label>
<textarea
value={formData.notes}
onChange={e => setFormData(f => ({ ...f, notes: e.target.value }))}
rows={3}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary resize-none"
placeholder="Performance notes, observations, what's working..."
/>
</div>
</>
)}
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
{editingCampaign && permissions?.canDeleteCampaigns && (
<button
onClick={() => setShowDeleteConfirm(true)}
className="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg mr-auto"
>
Delete
</button>
)}
<button
onClick={() => { setShowModal(false); setEditingCampaign(null) }}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={!formData.name || !formData.start_date || !formData.end_date}
className="px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
>
{editingCampaign ? 'Save Changes' : 'Create Campaign'}
</button>
</div>
</div>
</Modal>
{/* Delete Confirmation */}
<Modal
isOpen={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
title="Delete Campaign?"
isConfirm
danger
confirmText="Delete Campaign"
onConfirm={async () => {
if (editingCampaign) {
await api.delete(`/campaigns/${editingCampaign.id || editingCampaign._id}`)
setShowModal(false)
setEditingCampaign(null)
loadCampaigns()
}
}}
>
Are you sure you want to delete this campaign? All associated posts and tracks will also be deleted. This action cannot be undone.
</Modal>
</div>
)
}