Skip to content

Commit 280cfb2

Browse files
committed
campaign ui stuff
1 parent baad3c4 commit 280cfb2

File tree

13 files changed

+1006
-1046
lines changed

13 files changed

+1006
-1046
lines changed

apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx

Lines changed: 17 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
AccordionItem,
4242
AccordionTrigger,
4343
} from "@usesend/ui/src/accordion";
44+
import ScheduleCampaign from "../../schedule-campaign";
4445

4546
const sendSchema = z.object({
4647
confirmation: z.string(),
@@ -63,7 +64,7 @@ export default function EditCampaignPage({
6364
{ campaignId },
6465
{
6566
enabled: !!campaignId,
66-
},
67+
}
6768
);
6869

6970
if (isLoading) {
@@ -98,18 +99,18 @@ function CampaignEditor({
9899
const utils = api.useUtils();
99100

100101
const [json, setJson] = useState<Record<string, any> | undefined>(
101-
campaign.content ? JSON.parse(campaign.content) : undefined,
102+
campaign.content ? JSON.parse(campaign.content) : undefined
102103
);
103104
const [isSaving, setIsSaving] = useState(false);
104105
const [name, setName] = useState(campaign.name);
105106
const [subject, setSubject] = useState(campaign.subject);
106107
const [from, setFrom] = useState(campaign.from);
107108
const [contactBookId, setContactBookId] = useState(campaign.contactBookId);
108109
const [replyTo, setReplyTo] = useState<string | undefined>(
109-
campaign.replyTo[0],
110+
campaign.replyTo[0]
110111
);
111112
const [previewText, setPreviewText] = useState<string | null>(
112-
campaign.previewText,
113+
campaign.previewText
113114
);
114115
const [openSendDialog, setOpenSendDialog] = useState(false);
115116

@@ -135,7 +136,7 @@ function CampaignEditor({
135136

136137
const deboucedUpdateCampaign = useDebouncedCallback(
137138
updateEditorContent,
138-
1000,
139+
1000
139140
);
140141

141142
async function onSendCampaign(values: z.infer<typeof sendSchema>) {
@@ -160,14 +161,14 @@ function CampaignEditor({
160161
onError: (error) => {
161162
toast.error(`Failed to send campaign: ${error.message}`);
162163
},
163-
},
164+
}
164165
);
165166
}
166167

167168
const handleFileChange = async (file: File) => {
168169
if (file.size > IMAGE_SIZE_LIMIT) {
169170
throw new Error(
170-
`File should be less than ${IMAGE_SIZE_LIMIT / 1024 / 1024}MB`,
171+
`File should be less than ${IMAGE_SIZE_LIMIT / 1024 / 1024}MB`
171172
);
172173
}
173174

@@ -194,7 +195,7 @@ function CampaignEditor({
194195
const confirmation = sendForm.watch("confirmation");
195196

196197
const contactBook = contactBooksQuery.data?.find(
197-
(book) => book.id === contactBookId,
198+
(book) => book.id === contactBookId
198199
);
199200

200201
return (
@@ -220,7 +221,7 @@ function CampaignEditor({
220221
toast.error(`${e.message}. Reverting changes.`);
221222
setName(campaign.name);
222223
},
223-
},
224+
}
224225
);
225226
}}
226227
/>
@@ -235,56 +236,8 @@ function CampaignEditor({
235236
? "just now"
236237
: `${formatDistanceToNow(campaign.updatedAt)} ago`}
237238
</div>
238-
<Dialog open={openSendDialog} onOpenChange={setOpenSendDialog}>
239-
<DialogTrigger asChild>
240-
<Button variant="default">Send Campaign</Button>
241-
</DialogTrigger>
242-
<DialogContent>
243-
<DialogHeader>
244-
<DialogTitle>Send Campaign</DialogTitle>
245-
<DialogDescription>
246-
Are you sure you want to send this campaign? This action
247-
cannot be undone.
248-
</DialogDescription>
249-
</DialogHeader>
250-
<div className="py-2">
251-
<Form {...sendForm}>
252-
<form
253-
onSubmit={sendForm.handleSubmit(onSendCampaign)}
254-
className="space-y-4"
255-
>
256-
<FormField
257-
control={sendForm.control}
258-
name="confirmation"
259-
render={({ field }) => (
260-
<FormItem>
261-
<FormLabel>Type 'Send' to confirm</FormLabel>
262-
<FormControl>
263-
<Input {...field} />
264-
</FormControl>
265-
<FormMessage />
266-
</FormItem>
267-
)}
268-
/>
269-
<div className="flex justify-end">
270-
<Button
271-
type="submit"
272-
disabled={
273-
sendCampaignMutation.isPending ||
274-
confirmation?.toLocaleLowerCase() !==
275-
"Send".toLocaleLowerCase()
276-
}
277-
>
278-
{sendCampaignMutation.isPending
279-
? "Sending..."
280-
: "Send"}
281-
</Button>
282-
</div>
283-
</form>
284-
</Form>
285-
</div>
286-
</DialogContent>
287-
</Dialog>
239+
240+
<ScheduleCampaign campaign={campaign} />
288241
</div>
289242
</div>
290243

@@ -315,7 +268,7 @@ function CampaignEditor({
315268
toast.error(`${e.message}. Reverting changes.`);
316269
setSubject(campaign.subject);
317270
},
318-
},
271+
}
319272
);
320273
}}
321274
className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent focus:border-border bg-transparent"
@@ -350,7 +303,7 @@ function CampaignEditor({
350303
toast.error(`${e.message}. Reverting changes.`);
351304
setFrom(campaign.from);
352305
},
353-
},
306+
}
354307
);
355308
}}
356309
/>
@@ -381,7 +334,7 @@ function CampaignEditor({
381334
toast.error(`${e.message}. Reverting changes.`);
382335
setReplyTo(campaign.replyTo[0]);
383336
},
384-
},
337+
}
385338
);
386339
}}
387340
/>
@@ -414,7 +367,7 @@ function CampaignEditor({
414367
toast.error(`${e.message}. Reverting changes.`);
415368
setPreviewText(campaign.previewText ?? "");
416369
},
417-
},
370+
}
418371
);
419372
}}
420373
className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent bg-transparent focus:border-border"
@@ -440,7 +393,7 @@ function CampaignEditor({
440393
onError: () => {
441394
setContactBookId(campaign.contactBookId);
442395
},
443-
},
396+
}
444397
);
445398
setContactBookId(val);
446399
}}

apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { H2 } from "@usesend/ui";
1414
import Spinner from "@usesend/ui/src/spinner";
1515
import { api } from "~/trpc/react";
1616
import { use } from "react";
17+
import { CampaignStatus } from "@prisma/client";
18+
import { formatDistanceToNow } from "date-fns";
19+
import TogglePauseCampaign from "../toggle-pause-campaign";
1720

1821
export default function CampaignDetailsPage({
1922
params,
@@ -22,9 +25,20 @@ export default function CampaignDetailsPage({
2225
}) {
2326
const { campaignId } = use(params);
2427

25-
const { data: campaign, isLoading } = api.campaign.getCampaign.useQuery({
26-
campaignId: campaignId,
27-
});
28+
const { data: campaign, isLoading } = api.campaign.getCampaign.useQuery(
29+
{ campaignId: campaignId },
30+
{
31+
refetchInterval: (query) => {
32+
const c: any = query.state.data;
33+
if (!c) return false;
34+
const dueNow =
35+
c.status === CampaignStatus.SCHEDULED &&
36+
(c.scheduledAt ? new Date(c.scheduledAt) <= new Date() : true);
37+
const shouldPoll = c.status === CampaignStatus.RUNNING || dueNow;
38+
return shouldPoll ? 5000 : false;
39+
},
40+
}
41+
);
2842

2943
if (isLoading) {
3044
return (
@@ -61,6 +75,16 @@ export default function CampaignDetailsPage({
6175
},
6276
];
6377

78+
const total = campaign.total ?? 0;
79+
const processed = (campaign as any).processed ?? 0;
80+
const progressPct = total > 0 ? Math.min(100, (processed / total) * 100) : 0;
81+
82+
const dueNow =
83+
campaign.status === CampaignStatus.SCHEDULED &&
84+
(!!campaign.scheduledAt
85+
? new Date(campaign.scheduledAt) <= new Date()
86+
: true);
87+
6488
return (
6589
<div className="container mx-auto">
6690
<Breadcrumb>
@@ -80,6 +104,48 @@ export default function CampaignDetailsPage({
80104
</BreadcrumbItem>
81105
</BreadcrumbList>
82106
</Breadcrumb>
107+
{/* Header: status + schedule + progress */}
108+
<div className="mt-8 flex items-center justify-between">
109+
<div className="flex items-center gap-4">
110+
<div
111+
className={`text-center min-w-[110px] rounded capitalize py-1 px-3 text-xs ${
112+
campaign.status === CampaignStatus.DRAFT
113+
? "bg-gray/15 text-gray border border-gray/25"
114+
: campaign.status === CampaignStatus.SENT
115+
? "bg-green/15 text-green border border-green/25"
116+
: campaign.status === CampaignStatus.RUNNING
117+
? "bg-blue/15 text-blue border border-blue/25"
118+
: campaign.status === CampaignStatus.PAUSED
119+
? "bg-orange/15 text-orange border border-orange/25"
120+
: "bg-yellow/15 text-yellow border border-yellow/25"
121+
}`}
122+
>
123+
{campaign.status.toLowerCase()}
124+
</div>
125+
{campaign.status === CampaignStatus.SCHEDULED && campaign.scheduledAt ? (
126+
<div className="text-sm text-muted-foreground">
127+
Starts {formatDistanceToNow(new Date(campaign.scheduledAt), { addSuffix: true })}
128+
</div>
129+
) : null}
130+
{campaign.status === CampaignStatus.RUNNING ? (
131+
<div className="flex items-center gap-3">
132+
<div className="text-sm text-muted-foreground">
133+
{processed}/{total} processed
134+
</div>
135+
<div className="w-48 h-2 rounded bg-muted overflow-hidden">
136+
<div
137+
className="h-2 bg-blue rounded"
138+
style={{ width: `${progressPct}%` }}
139+
/>
140+
</div>
141+
</div>
142+
) : null}
143+
</div>
144+
<div className="flex items-center gap-2">
145+
<TogglePauseCampaign campaign={campaign} />
146+
</div>
147+
</div>
148+
83149
<div className="mt-10">
84150
<H2 className="mb-4"> Statistics</H2>
85151
<div className="flex gap-4">

apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { CampaignStatus } from "@prisma/client";
1717
import DeleteCampaign from "./delete-campaign";
1818
import Link from "next/link";
1919
import DuplicateCampaign from "./duplicate-campaign";
20+
import TogglePauseCampaign from "./toggle-pause-campaign";
2021
import {
2122
Select,
2223
SelectTrigger,
@@ -58,6 +59,12 @@ export default function CampaignList() {
5859
>
5960
Scheduled
6061
</SelectItem>
62+
<SelectItem value={CampaignStatus.RUNNING} className=" capitalize">
63+
Running
64+
</SelectItem>
65+
<SelectItem value={CampaignStatus.PAUSED} className=" capitalize">
66+
Paused
67+
</SelectItem>
6168
<SelectItem value={CampaignStatus.SENT} className=" capitalize">
6269
Sent
6370
</SelectItem>
@@ -91,7 +98,8 @@ export default function CampaignList() {
9198
<Link
9299
className="underline underline-offset-4 decoration-dashed text-foreground hover:text-foreground"
93100
href={
94-
campaign.status === CampaignStatus.DRAFT
101+
campaign.status === CampaignStatus.DRAFT ||
102+
campaign.status === CampaignStatus.SCHEDULED
95103
? `/campaigns/${campaign.id}/edit`
96104
: `/campaigns/${campaign.id}`
97105
}
@@ -106,7 +114,11 @@ export default function CampaignList() {
106114
? "bg-gray/15 text-gray border border-gray/25"
107115
: campaign.status === CampaignStatus.SENT
108116
? "bg-green/15 text-green border border-green/25"
109-
: "bg-yellow/15 text-yellow border border-yellow/25"
117+
: campaign.status === CampaignStatus.RUNNING
118+
? "bg-blue/15 text-blue border border-blue/25"
119+
: campaign.status === CampaignStatus.PAUSED
120+
? "bg-orange/15 text-orange border border-orange/25"
121+
: "bg-yellow/15 text-yellow border border-yellow/25"
110122
}`}
111123
>
112124
{campaign.status.toLowerCase()}
@@ -118,7 +130,11 @@ export default function CampaignList() {
118130
})}
119131
</TableCell>
120132
<TableCell>
121-
<div className="flex gap-2">
133+
<div className="flex gap-2 items-center">
134+
{(campaign.status === CampaignStatus.SCHEDULED ||
135+
campaign.status === CampaignStatus.RUNNING) && (
136+
<TogglePauseCampaign campaign={campaign} />
137+
)}
122138
<DuplicateCampaign campaign={campaign} />
123139
<DeleteCampaign campaign={campaign} />
124140
</div>

0 commit comments

Comments
 (0)