"use client";
import { api } from "~/trpc/react";
import { Spinner } from "@unsend/ui/src/spinner";
import { Button } from "@unsend/ui/src/button";
import { Input } from "@unsend/ui/src/input";
import { Editor } from "@unsend/email-editor";
import { useState } from "react";
import { Campaign } from "@prisma/client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@unsend/ui/src/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@unsend/ui/src/dialog";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@unsend/ui/src/form";
import { toast } from "@unsend/ui/src/toaster";
import { useDebouncedCallback } from "use-debounce";
import { formatDistanceToNow } from "date-fns";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@unsend/ui/src/accordion";
const sendSchema = z.object({
confirmation: z.string(),
});
const IMAGE_SIZE_LIMIT = 10 * 1024 * 1024;
export default function EditCampaignPage({
params,
}: {
params: { campaignId: string };
}) {
const {
data: campaign,
isLoading,
error,
} = api.campaign.getCampaign.useQuery(
{ campaignId: params.campaignId },
{
enabled: !!params.campaignId,
}
);
if (isLoading) {
return (
);
}
if (error) {
return (
);
}
if (!campaign) {
return Campaign not found
;
}
return ;
}
function CampaignEditor({
campaign,
}: {
campaign: Campaign & { imageUploadSupported: boolean };
}) {
const contactBooksQuery = api.contacts.getContactBooks.useQuery();
const utils = api.useUtils();
const [json, setJson] = useState | undefined>(
campaign.content ? JSON.parse(campaign.content) : undefined
);
const [isSaving, setIsSaving] = useState(false);
const [name, setName] = useState(campaign.name);
const [subject, setSubject] = useState(campaign.subject);
const [from, setFrom] = useState(campaign.from);
const [contactBookId, setContactBookId] = useState(campaign.contactBookId);
const [replyTo, setReplyTo] = useState(
campaign.replyTo[0]
);
const [previewText, setPreviewText] = useState(
campaign.previewText
);
const [openSendDialog, setOpenSendDialog] = useState(false);
const updateCampaignMutation = api.campaign.updateCampaign.useMutation({
onSuccess: () => {
utils.campaign.getCampaign.invalidate();
setIsSaving(false);
},
});
const sendCampaignMutation = api.campaign.sendCampaign.useMutation();
const getUploadUrl = api.campaign.generateImagePresignedUrl.useMutation();
const sendForm = useForm>({
resolver: zodResolver(sendSchema),
});
function updateEditorContent() {
updateCampaignMutation.mutate({
campaignId: campaign.id,
content: JSON.stringify(json),
});
}
const deboucedUpdateCampaign = useDebouncedCallback(
updateEditorContent,
1000
);
async function onSendCampaign(values: z.infer) {
if (
values.confirmation?.toLocaleLowerCase() !== "Send".toLocaleLowerCase()
) {
sendForm.setError("confirmation", {
message: "Please type 'Send' to confirm",
});
return;
}
sendCampaignMutation.mutate(
{
campaignId: campaign.id,
},
{
onSuccess: () => {
setOpenSendDialog(false);
toast.success(`Campaign sent successfully`);
},
onError: (error) => {
toast.error(`Failed to send campaign: ${error.message}`);
},
}
);
}
const handleFileChange = async (file: File) => {
if (file.size > IMAGE_SIZE_LIMIT) {
throw new Error(
`File should be less than ${IMAGE_SIZE_LIMIT / 1024 / 1024}MB`
);
}
console.log("file type: ", file.type);
const { uploadUrl, imageUrl } = await getUploadUrl.mutateAsync({
name: file.name,
type: file.type,
campaignId: campaign.id,
});
const response = await fetch(uploadUrl, {
method: "PUT",
body: file,
});
if (!response.ok) {
throw new Error("Failed to upload file");
}
return imageUrl;
};
const confirmation = sendForm.watch("confirmation");
const contactBook = contactBooksQuery.data?.find(
(book) => book.id === contactBookId
);
return (
setName(e.target.value)}
className=" border-0 focus:ring-0 focus:outline-none px-0.5 w-[300px]"
onBlur={() => {
if (name === campaign.name || !name) {
return;
}
updateCampaignMutation.mutate(
{
campaignId: campaign.id,
name,
},
{
onError: (e) => {
toast.error(`${e.message}. Reverting changes.`);
setName(campaign.name);
},
}
);
}}
/>
{isSaving ? (
) : (
)}
{formatDistanceToNow(campaign.updatedAt) === "less than a minute"
? "just now"
: `${formatDistanceToNow(campaign.updatedAt)} ago`}
{
setFrom(e.target.value);
}}
className="mt-1 py-1 w-full text-sm outline-none border-b border-transparent focus:border-border bg-transparent"
placeholder="Friendly name"
onBlur={() => {
if (from === campaign.from || !from) {
return;
}
updateCampaignMutation.mutate(
{
campaignId: campaign.id,
from,
},
{
onError: (e) => {
toast.error(`${e.message}. Reverting changes.`);
setFrom(campaign.from);
},
}
);
}}
/>
{
setReplyTo(e.target.value);
}}
className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent bg-transparent focus:border-border"
placeholder="hello@example.com"
onBlur={() => {
if (replyTo === campaign.replyTo[0]) {
return;
}
updateCampaignMutation.mutate(
{
campaignId: campaign.id,
replyTo: replyTo ? [replyTo] : [],
},
{
onError: (e) => {
toast.error(`${e.message}. Reverting changes.`);
setReplyTo(campaign.replyTo[0]);
},
}
);
}}
/>
{
setPreviewText(e.target.value);
}}
onBlur={() => {
if (
previewText === campaign.previewText ||
!previewText
) {
return;
}
updateCampaignMutation.mutate(
{
campaignId: campaign.id,
previewText,
},
{
onError: (e) => {
toast.error(`${e.message}. Reverting changes.`);
setPreviewText(campaign.previewText ?? "");
},
}
);
}}
className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent bg-transparent focus:border-border"
/>
{contactBooksQuery.isLoading ? (
) : (
)}
{
setJson(content.getJSON());
setIsSaving(true);
deboucedUpdateCampaign();
}}
variables={["email", "firstName", "lastName"]}
uploadImage={
campaign.imageUploadSupported ? handleFileChange : undefined
}
/>
);
}