"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 (

Failed to load campaign

); } 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`}
Send Campaign Are you sure you want to send this campaign? This action cannot be undone.
( Type 'Send' to confirm )} />
{ setSubject(e.target.value); }} onBlur={() => { if (subject === campaign.subject || !subject) { return; } updateCampaignMutation.mutate( { campaignId: campaign.id, subject, }, { onError: (e) => { toast.error(`${e.message}. Reverting changes.`); setSubject(campaign.subject); }, } ); }} className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent focus:border-border bg-transparent" />
{ 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 } />
); }