import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { apiClient } from "../lib/apiClient"; import { Plus, Search, Megaphone, Play, Pause, Square, Trash2, ArrowUpRight, Sliders, Clock, Globe, CheckCircle2, MailCheck } from "lucide-react"; import { useState } from "react"; import { Link } from "react-router-dom"; import StatusBadge from "../components/StatusBadge"; import ConfirmModal from "../components/ConfirmModal"; import FormDrawer from "../components/FormDrawer"; import toast from "react-hot-toast"; export default function Campaigns() { const queryClient = useQueryClient(); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("ALL"); const [isCreateOpen, setIsCreateOpen] = useState(false); const [selectedForDelete, setSelectedForDelete] = useState(null); const { data: campaigns, isLoading } = useQuery({ queryKey: ["campaigns"], queryFn: async () => { const res = await apiClient.get("/campaigns"); return res.data; }, }); const updateStatusMutation = useMutation({ mutationFn: async ({ id, status }: { id: string; status: "START" | "PAUSE" | "STOP" }) => { const res = await apiClient.post(`/campaigns/${id}/status`, { status }); return res.data; }, onSuccess: (_, variables) => { const statusLabel = variables.status === "START" ? "started" : variables.status === "PAUSE" ? "paused" : "stopped"; toast.success(`Campaign ${statusLabel}`); queryClient.invalidateQueries({ queryKey: ["campaigns"] }); }, onError: (err: any) => { toast.error(err.response?.data?.error || "Failed to update campaign status"); } }); const deleteMutation = useMutation({ mutationFn: async (id: string) => { const res = await apiClient.delete(`/campaigns/${id}`); return res.data; }, onSuccess: () => { toast.success("Campaign removed"); setSelectedForDelete(null); queryClient.invalidateQueries({ queryKey: ["campaigns"] }); }, onError: (err: any) => { toast.error(err.response?.data?.error || "Failed to delete campaign"); } }); const filteredCampaigns = (campaigns || []).filter((c: any) => { const matchesSearch = c.name?.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = statusFilter === "ALL" || c.status === statusFilter; return matchesSearch && matchesStatus; }); return (
{/* Header */}

Outreach Campaigns

Automated multi-step cold email sequences with A/B variant testing.

{/* Filter and Search Bar */}
setSearchTerm(e.target.value)} className="w-full bg-black/40 border border-white/5 rounded-lg py-2 pl-9 pr-4 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 shadow-[inset_0_1px_1px_rgba(246,248,242,0.02)] transition-all" />
{["ALL", "ACTIVE", "PAUSED", "STOPPED"].map((status) => ( ))}
{/* Campaigns Table */}
{isLoading ? (

Loading campaign records...

) : filteredCampaigns.length > 0 ? (
{filteredCampaigns.map((camp: any) => ( ))}
Campaign Name Status Leads Sent Opens Replies Bounces Actions
{camp.name}
Schedule: {camp.start_schedule || "09:00"} - {camp.end_schedule || "17:00"} ({camp.timezone || "America/New_York"})
{camp.lead_count?.toLocaleString() || 0} {camp.sent_count?.toLocaleString() || 0} {camp.open_count?.toLocaleString() || 0} {camp.reply_count?.toLocaleString() || 0} {camp.bounce_count || 0}
{camp.status === "ACTIVE" ? ( ) : ( )}
) : (

No campaigns found

Create your first campaign to begin dispatching automated cold outreach sequences.

)}
{/* Delete Campaign Confirmation */} setSelectedForDelete(null)} onConfirm={() => selectedForDelete && deleteMutation.mutate(selectedForDelete)} title="Delete Campaign" description="Are you sure you want to permanently delete this campaign and all associated sequences? This action cannot be undone." confirmText="Delete Campaign" isDestructive={true} isLoading={deleteMutation.isPending} /> {/* Full Multi-Step Create Campaign Drawer */} setIsCreateOpen(false)} title="Launch New Campaign" subtitle="Configure schedule parameters, tracking domain, and initial sequence step." >
{ e.preventDefault(); const form = e.target as HTMLFormElement; const data = { name: (form.elements.namedItem("name") as HTMLInputElement).value, timezone: (form.elements.namedItem("timezone") as HTMLSelectElement).value, start_schedule: (form.elements.namedItem("start_schedule") as HTMLInputElement).value, end_schedule: (form.elements.namedItem("end_schedule") as HTMLInputElement).value, track_settings: (form.elements.namedItem("track_settings") as HTMLSelectElement).value, send_as_plain_text: (form.elements.namedItem("send_as_plain_text") as HTMLInputElement).checked, follow_up_percentage: Number((form.elements.namedItem("follow_up_percentage") as HTMLInputElement).value), unsubscribe_text: (form.elements.namedItem("unsubscribe_text") as HTMLInputElement).value, stop_lead_settings: (form.elements.namedItem("stop_lead_settings") as HTMLSelectElement).value, initial_subject: (form.elements.namedItem("subject") as HTMLInputElement).value, initial_body: (form.elements.namedItem("body") as HTMLTextAreaElement).value, }; try { await apiClient.post("/campaigns", data); toast.success("Campaign launched successfully"); setIsCreateOpen(false); queryClient.invalidateQueries({ queryKey: ["campaigns"] }); } catch (err: any) { toast.error(err.response?.data?.error || "Failed to create campaign"); } }} className="space-y-5 text-sm" >
Send as Plain Text Improves deliverability in strict enterprise inboxes
Follow-up Distribution % 85%

Step 1 Sequence Setup