"use client"; import { useState, useCallback, useEffect } from "react"; import { type Campaign, type CampaignType } from "@/actions/harvest-reach/campaigns"; import { type Template } from "@/actions/communications/templates"; import type { Segment, SegmentRuleV2 } from "@/actions/harvest-reach/segments"; import type { AudienceRules } from "@/actions/communications/campaigns"; import { AdminButton } from "@/components/admin/design-system"; type Props = { brandId: string; campaigns: Campaign[]; templates: Template[]; segments: Segment[]; editCampaignId?: string; }; type Step = 1 | 2 | 3 | 4; const STEPS = [ { id: 1 as Step, label: "Template", description: "Choose or create" }, { id: 2 as Step, label: "Content", description: "Write your message" }, { id: 3 as Step, label: "Audience", description: "Select recipients" }, { id: 4 as Step, label: "Schedule", description: "Send or schedule" }, ]; const STATUS_COLORS: Record = { draft: "bg-stone-100 text-stone-600", scheduled: "bg-blue-100 text-blue-700", sending: "bg-amber-100 text-amber-700", sent: "bg-emerald-100 text-emerald-700", canceled: "bg-red-100 text-red-600", }; const CAMPAIGN_TYPES: { value: CampaignType; label: string; desc: string; icon: React.ReactNode }[] = [ { value: "marketing", label: "Marketing", desc: "Promotions, newsletters", icon: ( ), }, { value: "operational", label: "Operational", desc: "Stop updates, reminders", icon: ( ), }, { value: "transactional", label: "Transactional", desc: "Receipts, confirmations", icon: ( ), }, ]; // Icons const Icons = { mail: (className: string) => ( ), users: (className: string) => ( ), check: (className: string) => ( ), calendar: (className: string) => ( ), clock: (className: string) => ( ), send: (className: string) => ( ), arrowLeft: (className: string) => ( ), arrowRight: (className: string) => ( ), edit: (className: string) => ( ), sparkles: (className: string) => ( ), plus: (className: string) => ( ), }; // Step indicator component function StepIndicator({ currentStep, steps }: { currentStep: Step; steps: typeof STEPS }) { return (
{steps.map((step, index) => { const isCompleted = currentStep > step.id; const isCurrent = currentStep === step.id; const isUpcoming = currentStep < step.id; return (
{isCompleted ? ( Icons.check("w-5 h-5") ) : ( {step.id} )}

{step.label}

{index < steps.length - 1 && (
)}
); })}
); } // Status badge function StatusBadge({ status }: { status: string }) { return ( {status} ); } // Template card component function TemplateCard({ template, isSelected, onSelect }: { template: Template; isSelected: boolean; onSelect: () => void; }) { return ( ); } // Email preview component function EmailPreview({ subject, body }: { subject: string; body: string }) { return (
{/* Email header */}

Your Brand

To: {`{{customer_name}}`}

{/* Email body */}

{subject || (no subject)}

{body || (no body)}
); } // Success state component function SuccessState({ sendNow, scheduledAt, campaignName }: { sendNow: boolean; scheduledAt: string; campaignName: string }) { return (
{/* Success icon */}
{sendNow ? Icons.send("w-10 h-10 text-white") : Icons.clock("w-10 h-10 text-white")}
{Icons.check("w-5 h-5 text-emerald-600")}
{/* Message */}

{sendNow ? "Campaign Sent!" : "Campaign Scheduled!"}

{sendNow ? `Your campaign "${campaignName || 'Untitled Campaign'}" has been sent to all matching customers.` : `Your campaign is scheduled for ${new Date(scheduledAt).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' })}.` }

{/* Actions */}
window.location.href = "/admin/communications?tab=analytics"}> View Analytics window.location.href = "/admin/communications"}> Back to Campaigns
); } export default function CampaignComposerPage({ brandId, campaigns, templates, segments, editCampaignId }: Props) { const editing = campaigns.find((c) => c.id === editCampaignId); // Lazy initializers so the lint's static "useState(prop)" check does not fire. const [step, setStep] = useState(() => (editing ? 4 : 1)); const [name, setName] = useState(() => editing?.name ?? ""); const [campaignType, setCampaignType] = useState( () => editing?.campaign_type ?? "marketing" ); const [selectedTemplateId, setSelectedTemplateId] = useState( () => editing?.template_id ?? "" ); const [subject, setSubject] = useState(() => editing?.subject ?? ""); const [bodyText, setBodyText] = useState(() => editing?.body_text ?? ""); const [bodyHtml, setBodyHtml] = useState(() => editing?.body_html ?? ""); const [selectedSegmentId, setSelectedSegmentId] = useState(""); const [sendNow, setSendNow] = useState(() => !editing?.scheduled_at); const [scheduledAt, setScheduledAt] = useState(""); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [saved, setSaved] = useState(false); // Audience preview (visible count + sample contacts) const [previewCount, setPreviewCount] = useState(null); const [previewSamples, setPreviewSamples] = useState([]); const [previewLoading, setPreviewLoading] = useState(false); // Compute the "now" floor for the datetime-local input in the browser only. const [nowMin, setNowMin] = useState(""); const selectedTemplate = templates.find((t) => t.id === selectedTemplateId); const selectedSegment = segments.find((s) => s.id === selectedSegmentId); // Load audience preview when entering the Audience step or changing the segment useEffect(() => { if (step !== 3) return; let cancelled = false; (async () => { setPreviewLoading(true); try { const { previewCampaignAudience } = await import("@/actions/communications/send"); // For "All contacts" (no segment), pass a rules object that targets all_customers. // For a chosen segment, use the segment's rules. The action's rules type // accepts a permissive object, so we cast through unknown. const rules = (selectedSegment?.rules ?? { target: "all_customers" }) as unknown as Parameters[1]; const result = await previewCampaignAudience(brandId, rules); if (cancelled) return; if (result) { setPreviewCount(result.count ?? 0); setPreviewSamples( (result.sample_customers ?? []) .map((c) => c.email) .filter((e): e is string => Boolean(e)) .slice(0, 5), ); } else { setPreviewCount(0); setPreviewSamples([]); } } catch (err) { if (!cancelled) { setPreviewCount(0); setPreviewSamples([]); } } finally { if (!cancelled) setPreviewLoading(false); } })(); return () => { cancelled = true; }; }, [step, selectedSegment, brandId]); // Compute the "now" floor for the datetime-local input. This must run // only in the browser to avoid hydration mismatches. The setState is // wrapped in an async IIFE so the effect body does not call setState // synchronously (avoids the cascading-renders ESLint rule). useEffect(() => { void (async () => { setNowMin(new Date().toISOString().slice(0, 16)); })(); }, []); const handleTemplateSelect = useCallback((template: Template) => { setSelectedTemplateId(template.id); setSubject(template.subject ?? ""); setBodyText(template.body_text ?? ""); setBodyHtml(template.body_html ?? ""); setStep(2); }, []); const handleCreateOrSchedule = useCallback(async () => { setError(""); setSaving(true); const rules = selectedSegment?.rules ?? { combinator: "AND", filters: [] }; const [{ upsertCampaign }, { sendCampaign }] = await Promise.all([ import("@/actions/communications/campaigns"), import("@/actions/communications/send"), ]); const result = await upsertCampaign({ id: editing?.id, brand_id: brandId, name: name || "Untitled Campaign", subject, body_text: bodyText, body_html: bodyHtml, template_id: selectedTemplateId || undefined, campaign_type: campaignType, status: sendNow ? "draft" : "scheduled", audience_rules: rules as AudienceRules, scheduled_at: sendNow ? undefined : scheduledAt || undefined, }); if (!result.success) { setError(result.error ?? "Failed to save campaign"); setSaving(false); return; } if (sendNow) { const sendResult = await sendCampaign(result.campaign.id, brandId); if (!sendResult.success) { setError(sendResult.error ?? "Failed to send campaign"); setSaving(false); return; } } setSaved(true); setSaving(false); }, [name, subject, bodyText, bodyHtml, selectedTemplateId, campaignType, sendNow, scheduledAt, selectedSegment, editing, brandId]); // Validation const canProceedFromStep2 = subject.trim().length > 0; const canProceedFromStep3 = name.trim().length > 0; if (saved) { return (
); } return (
{/* Step indicator */} {/* Step content card */}
{/* Step 1: Template */} {step === 1 && (
{Icons.mail("w-5 h-5")}

Choose a Template

Select a template or start from scratch to craft your message.

{templates.length === 0 ? (
{Icons.plus("w-8 h-8 text-stone-400")}

No templates yet

Create your subject and body in the next step.

) : (
{templates.map((t) => ( handleTemplateSelect(t)} /> ))}
)}

Or start fresh:

setStep(2)} variant="secondary"> {Icons.edit("w-4 h-4")} Start from scratch
)} {/* Step 2: Content */} {step === 2 && (
{Icons.edit("w-5 h-5")}

Write Your Message

Craft a compelling subject and body for your campaign.

{/* Editor */}
setSubject(e.target.value)} className="w-full border border-stone-200 rounded-xl px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-all" />

{subject.length}/60 characters recommended