import { Badge, Button, Card, CardContent, ConfirmDialog, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@plunk/ui'; import type {Campaign, Template} from '@plunk/db'; import {CampaignStatus} from '@plunk/db'; import type {PaginatedResponse} from '@plunk/types'; import {EmptyState} from '@plunk/ui'; import {DashboardLayout} from '../../components/DashboardLayout'; import {TemplateSelectionDialog} from '../../components/TemplateSelectionDialog'; import {CampaignSelectionDialog} from '../../components/CampaignSelectionDialog'; import {network} from '../../lib/network'; import {formatRelativeTime} from '../../lib/dateUtils'; import {Ban, Calendar, ChevronDown, Copy, Edit, FileText, Mail, Plus, RefreshCw, Trash2} from 'lucide-react'; import {NextSeo} from 'next-seo'; import Link from 'next/link'; import {useRouter} from 'next/router'; import {useState} from 'react'; import {toast} from 'sonner'; import useSWR from 'swr'; import dayjs from 'dayjs'; export default function CampaignsPage() { const router = useRouter(); const [page, setPage] = useState(1); const [statusFilter, setStatusFilter] = useState('ALL'); const [showCancelDialog, setShowCancelDialog] = useState(false); const [campaignToCancel, setCampaignToCancel] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [campaignToDelete, setCampaignToDelete] = useState(null); const [showTemplateDialog, setShowTemplateDialog] = useState(false); const [showCampaignDialog, setShowCampaignDialog] = useState(false); const {data, mutate, isLoading} = useSWR>( `/campaigns?page=${page}&pageSize=20${statusFilter !== 'ALL' ? `&status=${statusFilter}` : ''}`, {revalidateOnFocus: false}, ); const getStatusBadge = (status: CampaignStatus) => { const config: Record = { DRAFT: {label: 'Draft', variant: 'neutral'}, SCHEDULED: {label: 'Scheduled', variant: 'default'}, SENDING: {label: 'Sending', variant: 'default'}, SENT: {label: 'Sent', variant: 'success'}, CANCELLED: {label: 'Cancelled', variant: 'neutral'}, }; const {label, variant} = config[status]; return {label}; }; const handleCancel = async () => { if (!campaignToCancel) return; try { await network.fetch('POST', `/campaigns/${campaignToCancel}/cancel`); toast.success('Campaign cancelled successfully'); void mutate(); } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to cancel campaign'); } finally { setCampaignToCancel(null); } }; const handleDuplicate = async (campaignId: string) => { try { await network.fetch('POST', `/campaigns/${campaignId}/duplicate`); toast.success('Campaign duplicated successfully'); void mutate(); } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to duplicate campaign'); } }; const handleDelete = async () => { if (!campaignToDelete) return; try { await network.fetch('DELETE', `/campaigns/${campaignToDelete}`); toast.success('Campaign deleted successfully'); void mutate(); } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to delete campaign'); } finally { setCampaignToDelete(null); } }; const handleSelectTemplate = ( template: Template, selectedFields: { subject: boolean; body: boolean; from: boolean; fromName: boolean; replyTo: boolean; }, ) => { // Navigate to create page with template data as query params const query: Record = { name: `${template.name}`, }; // Only include templateId if body is selected (needed to fetch body content) if (selectedFields.body) { query.templateId = template.id; } // Add selected fields to query params if (selectedFields.subject) { query.subject = template.subject; } if (selectedFields.from) { query.from = template.from; } if (selectedFields.fromName && template.fromName) { query.fromName = template.fromName; } if (selectedFields.replyTo && template.replyTo) { query.replyTo = template.replyTo; } void router.push({ pathname: '/campaigns/create', query, }); }; const handleSelectCampaign = ( campaign: Campaign, selectedFields: { subject: boolean; body: boolean; from: boolean; fromName: boolean; replyTo: boolean; audience: boolean; }, ) => { // Navigate to create page with campaign data as query params const query: Record = { name: `${campaign.name}`, }; // Only include campaignId if body is selected (needed to fetch body content) if (selectedFields.body) { query.campaignId = campaign.id; } // Add selected fields to query params if (selectedFields.subject) { query.subject = campaign.subject; } if (selectedFields.from) { query.from = campaign.from; } if (selectedFields.fromName && campaign.fromName) { query.fromName = campaign.fromName; } if (selectedFields.replyTo && campaign.replyTo) { query.replyTo = campaign.replyTo; } if (selectedFields.audience) { query.audienceType = campaign.audienceType; if (campaign.segmentId) { query.segmentId = campaign.segmentId; } } void router.push({ pathname: '/campaigns/create', query, }); }; return ( <>
{/* Header */}

Campaigns

Send one-time email broadcasts to your contacts. {data?.total ? `${data.total} total campaigns` : ''}

Empty Campaign Start from scratch with a blank canvas
setShowTemplateDialog(true)} className="py-3 cursor-pointer">
From Template Use an existing template as a starting point
setShowCampaignDialog(true)} className="py-3 cursor-pointer">
From Previous Campaign Copy content and settings from an existing campaign
{/* Filters */}
{/* Campaigns List */}
{isLoading && ( Loading campaigns... )} {!isLoading && data?.data.length === 0 && (
Empty Campaign Start from scratch with a blank canvas
setShowTemplateDialog(true)} className="py-3 cursor-pointer">
From Template Use an existing template as a starting point
setShowCampaignDialog(true)} className="py-3 cursor-pointer">
From Previous Campaign Copy content and settings from an existing campaign
) : ( ) } />
)} {data?.data.map(campaign => { const openRate = campaign.sentCount > 0 ? (campaign.openedCount / campaign.sentCount) * 100 : 0; const clickRate = campaign.sentCount > 0 ? (campaign.clickedCount / campaign.sentCount) * 100 : 0; const deliveryPct = campaign.totalRecipients > 0 ? (campaign.sentCount / campaign.totalRecipients) * 100 : 0; return (

{campaign.name}

{getStatusBadge(campaign.status)}
{campaign.status === 'DRAFT' && ( <> {campaign.totalRecipients.toLocaleString()} estimated recipients )} {campaign.status === 'SCHEDULED' && ( <> {campaign.totalRecipients.toLocaleString()} recipients {campaign.scheduledFor && ( <> Sending {dayjs(campaign.scheduledFor).format('MMM D, YYYY [at] h:mm A')} )} )} {campaign.status === 'SENDING' && ( <> {deliveryPct.toFixed(0)}% delivered {openRate.toFixed(1)}% opens )} {campaign.status === 'SENT' && ( <> {campaign.sentCount.toLocaleString()} sent {openRate.toFixed(1)}% opens {clickRate > 0 && ( <> {clickRate.toFixed(1)}% clicks )} )} {campaign.status === 'CANCELLED' && ( {campaign.totalRecipients.toLocaleString()} recipients )}
Updated {formatRelativeTime(campaign.updatedAt)}
{dayjs(campaign.updatedAt).format('DD MMMM YYYY, hh:mm')}
{campaign.status === 'DRAFT' && ( )} {(campaign.status === 'SCHEDULED' || campaign.status === 'SENDING') && ( )}
); })}
{/* Pagination */} {data && data.totalPages > 1 && (
Page {page} of {data.totalPages}
)}
); }