feat: Add type to campaign

This commit is contained in:
Dries Augustyns
2026-04-01 18:49:51 +02:00
parent 3343e891bd
commit d24259e8d2
9 changed files with 139 additions and 20 deletions
+45 -4
View File
@@ -29,7 +29,7 @@ import {
StickySaveBar,
} from '@plunk/ui';
import type {Campaign, Segment} from '@plunk/db';
import {CampaignAudienceType, CampaignStatus} from '@plunk/db';
import {CampaignAudienceType, CampaignStatus, TemplateType} from '@plunk/db';
import {CampaignSchemas} from '@plunk/shared';
import {DashboardLayout} from '../../components/DashboardLayout';
import {EmailSettings} from '../../components/EmailSettings';
@@ -216,6 +216,7 @@ export default function CampaignDetailsPage() {
from: editedCampaign.from,
fromName: editedCampaign.fromName || null,
replyTo: editedCampaign.replyTo || null,
type: editedCampaign.type,
audienceType: editedCampaign.audienceType,
segmentId: editedCampaign.segmentId || undefined,
});
@@ -232,6 +233,7 @@ export default function CampaignDetailsPage() {
from: updated.data.from,
fromName: updated.data.fromName || '',
replyTo: updated.data.replyTo || '',
type: updated.data.type,
audienceType: updated.data.audienceType,
segmentId: updated.data.segmentId || undefined,
});
@@ -254,6 +256,7 @@ export default function CampaignDetailsPage() {
from: campaign.data.from,
fromName: campaign.data.fromName || '',
replyTo: campaign.data.replyTo || '',
type: campaign.data.type,
audienceType: campaign.data.audienceType,
segmentId: campaign.data.segmentId || undefined,
});
@@ -274,6 +277,7 @@ export default function CampaignDetailsPage() {
editedCampaign.from !== campaign.data.from ||
(editedCampaign.fromName || '') !== (campaign.data.fromName || '') ||
(editedCampaign.replyTo || '') !== (campaign.data.replyTo || '') ||
editedCampaign.type !== campaign.data.type ||
editedCampaign.audienceType !== campaign.data.audienceType ||
(editedCampaign.segmentId || null) !== (campaign.data.segmentId || null);
@@ -461,6 +465,40 @@ export default function CampaignDetailsPage() {
/>
</div>
<div>
<Label>Campaign Type</Label>
<div className="grid grid-cols-2 gap-3 mt-2">
<button
type="button"
onClick={() => setEditedCampaign({...editedCampaign, type: TemplateType.MARKETING})}
className={`text-left p-3 rounded-lg border-2 transition-colors ${
(editedCampaign.type ?? c.type) === TemplateType.MARKETING
? 'border-neutral-900 bg-neutral-50'
: 'border-neutral-200 hover:border-neutral-300'
}`}
>
<p className="font-medium text-sm text-neutral-900">Marketing</p>
<p className="text-xs text-neutral-500 mt-1">
Subscribed contacts only, includes unsubscribe link.
</p>
</button>
<button
type="button"
onClick={() => setEditedCampaign({...editedCampaign, type: TemplateType.TRANSACTIONAL})}
className={`text-left p-3 rounded-lg border-2 transition-colors ${
(editedCampaign.type ?? c.type) === TemplateType.TRANSACTIONAL
? 'border-neutral-900 bg-neutral-50'
: 'border-neutral-200 hover:border-neutral-300'
}`}
>
<p className="font-medium text-sm text-neutral-900">Transactional</p>
<p className="text-xs text-neutral-500 mt-1">
All contacts regardless of subscription. No unsubscribe footer.
</p>
</button>
</div>
</div>
<div>
<Label htmlFor="subject">Subject Line *</Label>
<Input
@@ -513,8 +551,8 @@ export default function CampaignDetailsPage() {
<SelectContent>
<SelectItemWithDescription
value={CampaignAudienceType.ALL}
title="All Subscribed Contacts"
description="Send to everyone who hasn't unsubscribed"
title={(editedCampaign.type ?? c.type) === TemplateType.TRANSACTIONAL ? 'All Contacts' : 'All Subscribed Contacts'}
description={(editedCampaign.type ?? c.type) === TemplateType.TRANSACTIONAL ? 'Send to all contacts regardless of subscription status' : "Send to everyone who hasn't unsubscribed"}
/>
<SelectItemWithDescription
value={CampaignAudienceType.SEGMENT}
@@ -581,7 +619,10 @@ export default function CampaignDetailsPage() {
<Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
<p className="text-xs text-blue-800">
This count will be recalculated right before sending to ensure accuracy. The final number may
differ if contacts subscribe, unsubscribe, or segment membership changes.
differ if contacts{' '}
{(editedCampaign.type ?? c.type) === TemplateType.TRANSACTIONAL
? 'are added or removed, or segment membership changes.'
: 'subscribe, unsubscribe, or segment membership changes.'}
</p>
</div>
</div>
+64 -8
View File
@@ -15,7 +15,7 @@ import {
Textarea,
} from '@plunk/ui';
import type {Segment, Template} from '@plunk/db';
import {CampaignAudienceType} from '@plunk/db';
import {CampaignAudienceType, TemplateType} from '@plunk/db';
import {NextSeo} from 'next-seo';
import {DashboardLayout} from '../../components/DashboardLayout';
import {EmailSettings} from '../../components/EmailSettings';
@@ -41,6 +41,7 @@ export default function CreateCampaignPage() {
const [from, setFrom] = useState('');
const [fromName, setFromName] = useState('');
const [replyTo, setReplyTo] = useState('');
const [campaignType, setCampaignType] = useState<TemplateType>(TemplateType.MARKETING);
const [audienceType, setAudienceType] = useState<CampaignAudienceType>(CampaignAudienceType.ALL);
const [segmentId, setSegmentId] = useState('');
const [saving, setSaving] = useState(false);
@@ -150,6 +151,7 @@ export default function CreateCampaignPage() {
from,
fromName: fromName || null,
replyTo: replyTo || null,
type: campaignType,
audienceType,
segmentId: audienceType === CampaignAudienceType.SEGMENT ? segmentId : undefined,
audienceFilter: audienceType === CampaignAudienceType.FILTERED ? [] : undefined,
@@ -256,11 +258,54 @@ export default function CreateCampaignPage() {
</CardContent>
</Card>
{/* Email Settings */}
{/* Campaign Type */}
<Card>
<CardHeader>
<StepHeader
stepNumber={2}
title="Campaign Type"
description="Choose how this campaign should be treated"
/>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setCampaignType(TemplateType.MARKETING)}
className={`text-left p-4 rounded-lg border-2 transition-colors ${
campaignType === TemplateType.MARKETING
? 'border-neutral-900 bg-neutral-50'
: 'border-neutral-200 hover:border-neutral-300'
}`}
>
<p className="font-medium text-sm text-neutral-900">Marketing</p>
<p className="text-xs text-neutral-500 mt-1">
Sent to subscribed contacts only. Includes unsubscribe link.
</p>
</button>
<button
type="button"
onClick={() => setCampaignType(TemplateType.TRANSACTIONAL)}
className={`text-left p-4 rounded-lg border-2 transition-colors ${
campaignType === TemplateType.TRANSACTIONAL
? 'border-neutral-900 bg-neutral-50'
: 'border-neutral-200 hover:border-neutral-300'
}`}
>
<p className="font-medium text-sm text-neutral-900">Transactional</p>
<p className="text-xs text-neutral-500 mt-1">
Sent to all contacts regardless of subscription status. No unsubscribe footer.
</p>
</button>
</div>
</CardContent>
</Card>
{/* Email Settings */}
<Card>
<CardHeader>
<StepHeader
stepNumber={3}
title="Email Settings"
description="Configure sender information and subject"
/>
@@ -294,7 +339,7 @@ export default function CreateCampaignPage() {
{/* Email Content */}
<Card className="overflow-visible">
<CardHeader>
<StepHeader stepNumber={3} title="Email Content" description="Design your email message" />
<StepHeader stepNumber={4} title="Email Content" description="Design your email message" />
</CardHeader>
<CardContent>
<div className="space-y-2">
@@ -309,7 +354,7 @@ export default function CreateCampaignPage() {
{/* Audience Selection */}
<Card>
<CardHeader>
<StepHeader stepNumber={4} title="Audience" description="Choose who will receive this campaign" />
<StepHeader stepNumber={5} title="Audience" description="Choose who will receive this campaign" />
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
@@ -327,8 +372,8 @@ export default function CreateCampaignPage() {
<SelectContent>
<SelectItemWithDescription
value={CampaignAudienceType.ALL}
title="All Subscribed Contacts"
description="Send to everyone who hasn't unsubscribed"
title={campaignType === TemplateType.TRANSACTIONAL ? 'All Contacts' : 'All Subscribed Contacts'}
description={campaignType === TemplateType.TRANSACTIONAL ? 'Send to all contacts regardless of subscription status' : "Send to everyone who hasn't unsubscribed"}
/>
<SelectItemWithDescription
value={CampaignAudienceType.SEGMENT}
@@ -388,9 +433,13 @@ export default function CreateCampaignPage() {
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-start gap-3">
<Users className="h-5 w-5 text-blue-600 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-900">All subscribed contacts</p>
<p className="text-sm font-medium text-blue-900">
{campaignType === TemplateType.TRANSACTIONAL ? 'All contacts' : 'All subscribed contacts'}
</p>
<p className="text-xs text-blue-700 mt-1">
This campaign will be sent to all contacts who haven&#39;t unsubscribed
{campaignType === TemplateType.TRANSACTIONAL
? 'This campaign will be sent to all contacts regardless of subscription status'
: "This campaign will be sent to all contacts who haven't unsubscribed"}
</p>
</div>
</div>
@@ -438,6 +487,13 @@ export default function CreateCampaignPage() {
</div>
)}
<div className="flex justify-between py-2 border-b border-neutral-100">
<span className="text-neutral-500">Type</span>
<span className="font-medium">
{campaignType === TemplateType.MARKETING ? 'Marketing' : 'Transactional'}
</span>
</div>
<div className="flex justify-between py-2 border-b border-neutral-100">
<span className="text-neutral-500">Audience</span>
<span className="font-medium">