diff --git a/apps/web/src/components/CampaignSelectionDialog.tsx b/apps/web/src/components/CampaignSelectionDialog.tsx new file mode 100644 index 0000000..e482c80 --- /dev/null +++ b/apps/web/src/components/CampaignSelectionDialog.tsx @@ -0,0 +1,424 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Checkbox, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@plunk/ui'; +import type {Campaign} from '@plunk/db'; +import {CampaignStatus} from '@plunk/db'; +import {ArrowLeft, Calendar, Mail, Users} from 'lucide-react'; +import {useState} from 'react'; +import useSWR from 'swr'; + +interface PaginatedCampaigns { + campaigns: Campaign[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +interface CampaignSelectionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSelectCampaign: (campaign: Campaign, selectedFields: SelectedFields) => void; +} + +interface SelectedFields { + subject: boolean; + body: boolean; + from: boolean; + fromName: boolean; + replyTo: boolean; + audience: boolean; +} + +export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}: CampaignSelectionDialogProps) { + const [page, setPage] = useState(1); + const [statusFilter, setStatusFilter] = useState('ALL'); + const [step, setStep] = useState<'select' | 'configure'>('select'); + const [selectedCampaign, setSelectedCampaign] = useState(null); + const [selectedFields, setSelectedFields] = useState({ + subject: true, + body: true, + from: true, + fromName: true, + replyTo: true, + audience: true, + }); + + const {data, isLoading} = useSWR( + open + ? `/campaigns?page=${page}&pageSize=10${statusFilter !== 'ALL' ? `&status=${statusFilter}` : ''}` + : null, + {revalidateOnFocus: false}, + ); + + const handleCampaignClick = (campaign: Campaign) => { + setSelectedCampaign(campaign); + setStep('configure'); + }; + + const handleBack = () => { + setStep('select'); + setSelectedCampaign(null); + }; + + const handleConfirm = () => { + if (selectedCampaign) { + onSelectCampaign(selectedCampaign, selectedFields); + onOpenChange(false); + // Reset state + setPage(1); + setStatusFilter('ALL'); + setStep('select'); + setSelectedCampaign(null); + setSelectedFields({ + subject: true, + body: true, + from: true, + fromName: true, + replyTo: true, + audience: true, + }); + } + }; + + const toggleField = (field: keyof SelectedFields) => { + setSelectedFields(prev => ({...prev, [field]: !prev[field]})); + }; + + const getStatusBadge = (status: CampaignStatus) => { + const variants: Record< + CampaignStatus, + {variant: 'default' | 'secondary' | 'destructive' | 'outline'; label: string; className?: string} + > = { + DRAFT: {variant: 'secondary', label: 'Draft', className: 'bg-neutral-100 text-neutral-700'}, + SCHEDULED: {variant: 'default', label: 'Scheduled', className: 'bg-blue-100 text-blue-700'}, + SENDING: {variant: 'default', label: 'Sending', className: 'bg-purple-100 text-purple-700'}, + SENT: {variant: 'default', label: 'Sent', className: 'bg-green-100 text-green-700'}, + CANCELLED: {variant: 'destructive', label: 'Cancelled', className: 'bg-red-100 text-red-700'}, + }; + + const config = variants[status]; + return ( + + {config.label} + + ); + }; + + const getAudienceLabel = (campaign: Campaign) => { + if (campaign.audienceType === 'ALL') { + return 'All Contacts'; + } else if (campaign.audienceType === 'SEGMENT') { + return `Segment (${campaign.totalRecipients.toLocaleString()} contacts)`; + } else { + return `Filtered (${campaign.totalRecipients.toLocaleString()} contacts)`; + } + }; + + return ( + + + + + {step === 'configure' && ( + + )} + {step === 'select' ? 'Select a Campaign' : 'Choose What to Copy'} + + + {step === 'select' + ? 'Choose a previous campaign to use as a starting point' + : 'Select which parts of the campaign you want to use'} + + + + {step === 'select' ? ( +
+ {/* Status Filter */} +
+ +
+ + {/* Campaigns List */} +
+ {isLoading && ( +
+
+ + + + +

Loading campaigns...

+
+
+ )} + + {!isLoading && data?.campaigns.length === 0 && ( +
+ +

No campaigns found

+

+ {statusFilter !== 'ALL' ? 'Try adjusting your filter' : 'Create your first campaign to get started'} +

+
+ )} + + {data?.campaigns.map(campaign => ( + handleCampaignClick(campaign)} + > + +
+
+
+ {campaign.name} + {getStatusBadge(campaign.status)} +
+ {campaign.description && ( + {campaign.description} + )} +
+
+
+ +
+
+ + Subject: + {campaign.subject} +
+
+ + Audience: + {getAudienceLabel(campaign)} +
+ {campaign.scheduledFor && ( +
+ + Scheduled: + + {new Date(campaign.scheduledFor).toLocaleDateString()} + +
+ )} +
+
+
+ ))} +
+ + {/* Pagination */} + {data && data.totalPages > 1 && ( +
+

+ Page {page} of {data.totalPages} +

+
+ + +
+
+ )} +
+ ) : ( +
+ {/* Scrollable Content */} +
+ {/* Campaign Preview */} + {selectedCampaign && ( + + +
+
+
+ {selectedCampaign.name} + {getStatusBadge(selectedCampaign.status)} +
+ {selectedCampaign.description && ( + {selectedCampaign.description} + )} +
+
+
+
+ )} + + {/* Field Selection */} +
+
toggleField('subject')} + > + toggleField('subject')} + /> +
+ + {selectedCampaign?.subject && ( +

{selectedCampaign.subject}

+ )} +
+
+ +
toggleField('body')} + > + toggleField('body')} /> +
+ +

The full email content and design

+
+
+ +
toggleField('from')} + > + toggleField('from')} /> +
+ + {selectedCampaign?.from && ( +

{selectedCampaign.from}

+ )} +
+
+ +
toggleField('fromName')} + > + toggleField('fromName')} + /> +
+ + {selectedCampaign?.fromName && ( +

{selectedCampaign.fromName}

+ )} +
+
+ +
toggleField('replyTo')} + > + toggleField('replyTo')} + /> +
+ + {selectedCampaign?.replyTo && ( +

{selectedCampaign.replyTo}

+ )} +
+
+ +
toggleField('audience')} + > + toggleField('audience')} + /> +
+ + {selectedCampaign && ( +

{getAudienceLabel(selectedCampaign)}

+ )} +
+
+
+
+ + {/* Fixed Action Buttons */} +
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/web/src/components/TemplateSelectionDialog.tsx b/apps/web/src/components/TemplateSelectionDialog.tsx new file mode 100644 index 0000000..cc2b08e --- /dev/null +++ b/apps/web/src/components/TemplateSelectionDialog.tsx @@ -0,0 +1,427 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Checkbox, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + Input, + Label, +} from '@plunk/ui'; +import type {Template} from '@plunk/db'; +import {network} from '../lib/network'; +import {ArrowLeft, FileText, Search} from 'lucide-react'; +import {useState} from 'react'; +import useSWR from 'swr'; + +interface PaginatedTemplates { + templates: Template[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +interface TemplateSelectionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSelectTemplate: (template: Template, selectedFields: SelectedFields) => void; +} + +interface SelectedFields { + subject: boolean; + body: boolean; + from: boolean; + fromName: boolean; + replyTo: boolean; +} + +export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}: TemplateSelectionDialogProps) { + const [search, setSearch] = useState(''); + const [searchInput, setSearchInput] = useState(''); + const [page, setPage] = useState(1); + const [typeFilter, setTypeFilter] = useState<'ALL' | 'TRANSACTIONAL' | 'MARKETING'>('ALL'); + const [step, setStep] = useState<'select' | 'configure'>('select'); + const [selectedTemplate, setSelectedTemplate] = useState