import { zodResolver } from "@hookform/resolvers/zod"; import { CampaignSchemas } from "@plunk/shared"; import type { Campaign } from "@prisma/client"; import { Ring } from "@uiball/loaders"; import dayjs from "dayjs"; import { AnimatePresence, motion } from "framer-motion"; import { Search, Users2, XIcon } from "lucide-react"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import { type FieldError, useForm } from "react-hook-form"; import { toast } from "sonner"; import { Alert, Card, Dropdown, Editor, FullscreenLoader, Input, MultiselectDropdown } from "../../components"; import { Dashboard } from "../../layouts"; import { useCampaigns } from "../../lib/hooks/campaigns"; import { useContacts } from "../../lib/hooks/contacts"; import { useEventsWithoutTriggers } from "../../lib/hooks/events"; import { useActiveProject } from "../../lib/hooks/projects"; import { network } from "../../lib/network"; interface CampaignValues { subject: string; body: string; email?: string; from?: string; recipients: string[]; style: "PLUNK" | "HTML"; } const templates = { blank: { subject: "", body: "", }, }; /** * */ export default function Index() { const router = useRouter(); const project = useActiveProject(); const { mutate } = useCampaigns(); const { data: contacts } = useContacts(0); const { data: events } = useEventsWithoutTriggers(); const [query, setQuery] = useState<{ events?: string[]; last?: "day" | "week" | "month"; data?: string; value?: string; notevents?: string[]; notlast?: "day" | "week" | "month"; }>({}); const [advancedSelector, setSelector] = useState(false); const { register, handleSubmit, formState: { errors }, setValue, watch, setError, clearErrors, } = useForm({ resolver: zodResolver(CampaignSchemas.create), defaultValues: { recipients: [], ...templates.blank, style: "PLUNK", }, }); useEffect(() => { watch((value, { name, type }) => { if (name === "email") { if (value.email && project?.email && !value.email.endsWith(project.email.split("@")[1])) { setError("email", { type: "manual", message: `The sender address must end with @${project.email?.split("@")[1]}`, }); } else { clearErrors("email"); } } }); }, [watch, project, setError, clearErrors]); if (!project || !events) { return ; } const selectQuery = () => { if (!contacts) { return; } let filteredContacts = contacts.contacts; if (query.events && query.events.length > 0) { query.events.map((e) => { filteredContacts = filteredContacts.filter((c) => c.triggers.some((t) => t.eventId === e)); }); } if (query.last) { filteredContacts = filteredContacts.filter((c) => { if (c.triggers.length === 0) { return false; } const lastTrigger = c.triggers.sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1)); if (lastTrigger.length === 0) { return false; } return dayjs(lastTrigger[0].createdAt).isAfter(dayjs().subtract(1, query.last)); }); } if (query.notevents && query.notevents.length > 0 && query.notlast) { query.notevents.map((e) => { filteredContacts = filteredContacts.filter((c) => { if (c.triggers.length === 0) { return true; } const lastTrigger = c.triggers.filter((t) => t.eventId === e).sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1)); if (lastTrigger.length === 0) { return true; } return dayjs(lastTrigger[0].createdAt).isAfter(dayjs().subtract(1, query.last)); }); }); } else if (query.notevents && query.notevents.length > 0) { query.notevents.map((e) => { filteredContacts = filteredContacts.filter((c) => c.triggers.every((t) => t.eventId !== e)); }); } else if (query.notlast) { filteredContacts = filteredContacts.filter((c) => { if (c.triggers.length === 0) { return true; } const lastTrigger = c.triggers.sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1)); if (lastTrigger.length === 0) { return true; } return !dayjs(lastTrigger[0].createdAt).isAfter(dayjs().subtract(1, query.notlast)); }); } if (query.data) { filteredContacts = filteredContacts.filter((c) => { if (!c.data) { return false; } return JSON.parse(c.data)[query.data as string]; }); } if (query.data && query.value) { filteredContacts = filteredContacts.filter((c) => { if (!c.data) { return false; } return Array.isArray(JSON.parse(c.data)[query.data as string]) ? JSON.parse(c.data)[query.data as string].includes(query.value) : JSON.parse(c.data)[query.data as string] === query.value; }); } setValue( "recipients", filteredContacts.map((c) => c.id), ); }; const create = async (data: CampaignValues) => { toast.promise( network.mock( project.secret, "POST", "/v1/campaigns", data.recipients.length === contacts?.contacts.filter((c) => c.subscribed).length ? { ...data, recipients: ["all"] } : { ...data, }, ), { loading: "Creating new campaign", success: () => { void mutate(); return "Created new campaign"; }, error: "Could not create new campaign!", }, ); await router.push("/campaigns"); }; return ( <>
{project.verified && ( )} {project.verified && ( )}
{contacts ? ( <>
setValue("recipients", c)} values={contacts.contacts .filter((c) => c.subscribed) .map((c) => { return { name: c.email, value: c.id }; })} selectedValues={watch("recipients")} /> {(errors.recipients as FieldError | undefined)?.message && ( {(errors.recipients as FieldError | undefined)?.message} )}
{advancedSelector && (
setQuery( e.length > 0 ? { ...query, events: e } : { ...query, events: undefined, last: undefined, }, ) } values={[ ...events .filter((e) => !query.notevents?.includes(e.id)) .sort((a, b) => { if (!a.templateId && !a.campaignId) { return -1; } if (!b.templateId && !b.campaignId) { return 1; } if (a.name.includes("delivered") && !b.name.includes("delivered")) { return -1; } return 0; }) .map((e) => { return { name: e.name, value: e.id, tag: e.templateId ?? e.campaignId ? (e.name.includes("opened") ? "On Open" : "On Delivery") : undefined, }; }), ]} selectedValues={query.events ?? []} />
{query.events && query.events.length > 0 && ( <> setQuery({ ...query, last: (e as "" | "day" | "week" | "month") === "" ? undefined : (e as "day" | "week" | "month"), }) } values={[ { name: "Anytime", value: "" }, { name: "In the last day", value: "day" }, { name: "In the last week", value: "week" }, { name: "In the last month", value: "month" }, ]} selectedValue={query.last ?? ""} /> )}
{ setQuery( e.length > 0 ? { ...query, notevents: e } : { ...query, notevents: undefined, notlast: undefined, }, ); }} values={[ ...events .filter((e) => !query.events?.includes(e.id)) .sort((a, b) => { if (!a.templateId && !a.campaignId) { return -1; } if (!b.templateId && !b.campaignId) { return 1; } if (a.name.includes("delivered") && !b.name.includes("delivered")) { return -1; } return 0; }) .map((e) => { return { name: e.name, value: e.id, tag: e.templateId ?? e.campaignId ? (e.name.includes("opened") ? "On Open" : "On Delivery") : undefined, }; }), ]} selectedValues={query.notevents ?? []} />
{query.notevents && query.notevents.length > 0 && ( <> setQuery({ ...query, notlast: (e as "" | "day" | "week" | "month") === "" ? undefined : (e as "day" | "week" | "month"), }) } values={[ { name: "Anytime", value: "" }, { name: "In the last day", value: "day" }, { name: "In the last week", value: "week" }, { name: "In the last month", value: "month" }, ]} selectedValue={query.notlast ?? ""} /> )}
setQuery({ ...query, data: e === "" ? undefined : e, }) } values={[ { name: "Any parameter", value: "" }, ...new Set( contacts.contacts .filter((c) => c.data) .map((c) => { return Object.keys(JSON.parse(c.data ?? "{}")); }) .reduce((acc, val) => acc.concat(val), []), ), ].map((k) => (typeof k === "string" ? { name: k, value: k } : k))} selectedValue={query.data ?? ""} />
{query.data && ( <> setQuery({ ...query, value: e === "" ? undefined : e, }) } values={[ { name: "Any value", value: "" }, ...new Set( contacts.contacts .filter((c) => c.data && JSON.parse(c.data)[query.data ?? ""]) .map((c) => { return JSON.parse(c.data ?? "{}")[query.data ?? ""]; }) .reduce((acc, val) => acc.concat(val), []), ), ].map((k) => typeof k === "string" ? { name: k, value: k, } : (k as { name: string; value: string; }), )} selectedValue={query.value ?? ""} /> )}
{ e.preventDefault(); selectQuery(); }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.9 }} className={ "ml-auto flex items-center justify-center gap-x-0.5 rounded bg-neutral-800 px-8 py-2 text-center text-sm font-medium text-white" } > Select contacts
)}
) : ( <>

Hang on!

We're still loading your contacts. This might take up to a minute. You can already start writing your campaign in the editor below.

)} {watch("recipients").length >= 10 && ( Your campaign will be sent out in batches of 80 recipients each. It will be delivered to all contacts{" "} {dayjs().to(dayjs().add(Math.ceil(watch("recipients").length / 80), "minutes"))} )}
{ setValue("body", value); setValue("style", type); }} modeSwitcher /> {errors.body?.message && ( {errors.body.message} )}
{ e.preventDefault(); return router.push("/campaigns"); }} className={ "flex w-full justify-center rounded border border-neutral-300 bg-white px-6 py-2 text-base font-medium text-neutral-800 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm" } > Cancel Create
); }