@@ -208,7 +208,7 @@ export function BillingLimits({projectId, hasSubscription, billingEnabled}: Bill
{!isEditing && limitsData && (
{/* For free tier, show total usage across all categories */}
- {!hasSubscription ? (
+ {tier !== 'paid' ? (
{
if (availableFields.length > 0) {
setAvailableVariables(availableFields);
diff --git a/apps/web/src/components/EmailSettings.tsx b/apps/web/src/components/EmailSettings.tsx
index 9d1f68d..a7a3588 100644
--- a/apps/web/src/components/EmailSettings.tsx
+++ b/apps/web/src/components/EmailSettings.tsx
@@ -11,14 +11,9 @@ interface EmailSettingsProps {
fromPlaceholder?: string;
fromNamePlaceholder?: string;
replyToPlaceholder?: string;
- showFromNameHelpText?: boolean;
layout?: 'vertical' | 'grid';
}
-/**
- * Reusable email settings component for from, fromName, and replyTo fields
- * Used in campaign and template forms
- */
export function EmailSettings({
from,
fromName,
@@ -29,7 +24,6 @@ export function EmailSettings({
fromPlaceholder = 'hello',
fromNamePlaceholder = 'Your Company',
replyToPlaceholder,
- showFromNameHelpText = false,
layout = 'grid',
}: EmailSettingsProps) {
// Use from email's local part as the reply-to placeholder if not provided
@@ -63,11 +57,9 @@ export function EmailSettings({
onChange={e => onFromNameChange(e.target.value)}
placeholder={fromNamePlaceholder}
/>
- {showFromNameHelpText && (
-
- The sender name that appears in the recipient's inbox. Defaults to your project name if not set.
-
- )}
+
+ The sender name that appears in the recipient's inbox. Defaults to your project name if not set.
+
diff --git a/apps/web/src/lib/contexts/ActiveProjectProvider.tsx b/apps/web/src/lib/contexts/ActiveProjectProvider.tsx
index 9f9c3fb..5be260c 100644
--- a/apps/web/src/lib/contexts/ActiveProjectProvider.tsx
+++ b/apps/web/src/lib/contexts/ActiveProjectProvider.tsx
@@ -1,5 +1,5 @@
import type {Project} from '@plunk/db';
-import {createContext, type ReactNode, useContext, useEffect, useState} from 'react';
+import {createContext, type ReactNode, use, useEffect, useState} from 'react';
import {useSWRConfig} from 'swr';
import {useProjects} from '../hooks/useProject';
@@ -74,7 +74,7 @@ export function ActiveProjectProvider({children}: {children: ReactNode}) {
}
export function useActiveProject() {
- const context = useContext(ActiveProjectContext);
+ const context = use(ActiveProjectContext);
if (context === undefined) {
throw new Error('useActiveProject must be used within an ActiveProjectProvider');
}
diff --git a/apps/web/src/pages/campaigns/[id].tsx b/apps/web/src/pages/campaigns/[id].tsx
index fb0a382..adf65fd 100644
--- a/apps/web/src/pages/campaigns/[id].tsx
+++ b/apps/web/src/pages/campaigns/[id].tsx
@@ -106,14 +106,18 @@ export default function CampaignDetailsPage() {
);
const [editedCampaign, setEditedCampaign] = useState
>({});
- const [isScheduleDialogOpen, setIsScheduleDialogOpen] = useState(false);
const [scheduledDateTime, setScheduledDateTime] = useState('');
- const [isTestEmailDialogOpen, setIsTestEmailDialogOpen] = useState(false);
const [testEmailAddress, setTestEmailAddress] = useState('');
- const [sendingTestEmail, setSendingTestEmail] = useState(false);
- const [showCancelDialog, setShowCancelDialog] = useState(false);
- const [showSendDialog, setShowSendDialog] = useState(false);
- const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+
+ type CampaignDialog =
+ | {type: 'none'}
+ | {type: 'schedule'}
+ | {type: 'testEmail'; sending: boolean}
+ | {type: 'send'}
+ | {type: 'cancel'}
+ | {type: 'delete'};
+
+ const [dialog, setDialog] = useState({type: 'none'});
// Automatically initialize edit fields when campaign is loaded and is a draft
const isEditMode = campaign?.data.status === CampaignStatus.DRAFT;
@@ -172,7 +176,7 @@ export default function CampaignDetailsPage() {
// Show confirmation with user's local time
const localTimeString = formatFullDateTime(scheduledDate);
toast.success(`Campaign scheduled for ${localTimeString}`);
- setIsScheduleDialogOpen(false);
+ setDialog({type: 'none'});
setScheduledDateTime('');
void mutate();
} catch (error) {
@@ -186,7 +190,7 @@ export default function CampaignDetailsPage() {
return;
}
- setSendingTestEmail(true);
+ setDialog({type: 'testEmail', sending: true});
try {
await network.fetch<{success: boolean; message: string}>('POST', `/campaigns/${id}/test`, {
@@ -194,12 +198,12 @@ export default function CampaignDetailsPage() {
} as any);
toast.success(`Test email sent to ${testEmailAddress}`);
- setIsTestEmailDialogOpen(false);
+ setDialog({type: 'none'});
setTestEmailAddress('');
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to send test email');
} finally {
- setSendingTestEmail(false);
+ setDialog(d => (d.type === 'testEmail' ? {type: 'testEmail', sending: false} : d));
}
};
@@ -370,7 +374,7 @@ export default function CampaignDetailsPage() {
- setIsTestEmailDialogOpen(true)} className="py-3 cursor-pointer">
+ setDialog({type: 'testEmail', sending: false})} className="py-3 cursor-pointer">
@@ -406,7 +410,7 @@ export default function CampaignDetailsPage() {
- setShowSendDialog(true)} className="py-3 cursor-pointer">
+ setDialog({type: 'send'})} className="py-3 cursor-pointer">
@@ -417,7 +421,7 @@ export default function CampaignDetailsPage() {
- setIsScheduleDialogOpen(true)} className="py-3 cursor-pointer">
+ setDialog({type: 'schedule'})} className="py-3 cursor-pointer">
@@ -531,7 +535,6 @@ export default function CampaignDetailsPage() {
onFromNameChange={value => setEditedCampaign({...editedCampaign, fromName: value})}
onReplyToChange={value => setEditedCampaign({...editedCampaign, replyTo: value})}
fromNamePlaceholder={activeProject?.name || 'Your Company'}
- showFromNameHelpText
layout="vertical"
/>
@@ -658,7 +661,7 @@ export default function CampaignDetailsPage() {
{/* Test Email Dialog */}
-
- setShowResetDialog(true)} className="shrink-0">
+ setDialog({type: 'reset'})} className="shrink-0">
Reset Data
@@ -614,7 +614,7 @@ export default function Settings() {