diff --git a/package.json b/package.json index 2210725..7e4dbf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plunk", - "version": "1.2.1", + "version": "1.3.0", "private": true, "license": "agpl-3.0", "workspaces": { diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index dac9241..9abfbc0 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -31,6 +31,7 @@ "@uiball/loaders": "^1.3.1", "classnames": "^2.5.1", "dayjs": "^1.11.12", + "dompurify": "^3.2.6", "framer-motion": "^11.3.7", "jotai": "2.9.0", "lucide-react": "^0.408.0", diff --git a/packages/dashboard/src/pages/contacts/[id].tsx b/packages/dashboard/src/pages/contacts/[id].tsx index a6084d4..f5a9f3d 100644 --- a/packages/dashboard/src/pages/contacts/[id].tsx +++ b/packages/dashboard/src/pages/contacts/[id].tsx @@ -1,606 +1,673 @@ // @ts-nocheck // React Hook Form messes up our types, ignore the entire file -import { zodResolver } from "@hookform/resolvers/zod"; -import { ContactSchemas, EventSchemas, type UtilitySchemas } from "@plunk/shared"; -import type { Contact, Email, Template } from "@prisma/client"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {ContactSchemas, EventSchemas, type UtilitySchemas} from "@plunk/shared"; +import type {Contact, Email, Template} from "@prisma/client"; import dayjs from "dayjs"; -import { motion } from "framer-motion"; -import { Save } from "lucide-react"; -import { useRouter } from "next/router"; -import React, { useEffect, useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -import { Card, Empty, FullscreenLoader, Input, Modal, Toggle } from "../../components"; -import { Dashboard } from "../../layouts"; -import { useContact } from "../../lib/hooks/contacts"; -import { useActiveProject } from "../../lib/hooks/projects"; -import { network } from "../../lib/network"; +import {motion} from "framer-motion"; +import {Save} from "lucide-react"; +import {useRouter} from "next/router"; +import React, {useEffect, useState} from "react"; +import {useFieldArray, useForm} from "react-hook-form"; +import {toast} from "sonner"; +import {z} from "zod"; +import {Badge, Card, Empty, FullscreenLoader, Input, Modal, Toggle} from "../../components"; +import {Dashboard} from "../../layouts"; +import {useContact} from "../../lib/hooks/contacts"; +import {useActiveProject} from "../../lib/hooks/projects"; +import {network} from "../../lib/network"; +import DOMPurify from "dompurify"; interface ContactValues { - email: string; - data: string | null; - subscribed: boolean; + email: string; + data: string | null; + subscribed: boolean; } interface EventValues { - event: string; + event: string; } /** * */ export default function Index() { - const router = useRouter(); + const router = useRouter(); + const [selectedEmail, setSelectedEmail] = useState(null); + if (!router.isReady) { + return ; + } - if (!router.isReady) { - return ; - } + const [eventModal, setEventModal] = useState(false); - const [eventModal, setEventModal] = useState(false); + const project = useActiveProject(); + const {data: contact, mutate} = useContact({ + id: router.query.id as string, + }); - const project = useActiveProject(); - const { data: contact, mutate } = useContact({ - id: router.query.id as string, - }); + const {handleSubmit, watch, setValue, reset} = useForm({ + resolver: zodResolver(ContactSchemas.manage), + }); - const { handleSubmit, watch, setValue, reset } = useForm({ - resolver: zodResolver(ContactSchemas.manage), - }); + const { + register: dataRegister, + control, + getValues: getDataValues, + reset: dataReset, + } = useForm({ + defaultValues: { + data: Object.entries(JSON.parse(contact?.data ? contact.data : "{}")).map(([key]) => ({ + value: {key}, + })), + }, + resolver: zodResolver( + z.object({ + data: z + .array( + z.object({ + value: z.object({key: z.string(), value: z.string()}), + }), + ) + .min(0), + }), + ), + }); - const { - register: dataRegister, - control, - getValues: getDataValues, - reset: dataReset, - } = useForm({ - defaultValues: { - data: Object.entries(JSON.parse(contact?.data ? contact.data : "{}")).map(([key]) => ({ - value: { key }, - })), - }, - resolver: zodResolver( - z.object({ - data: z - .array( - z.object({ - value: z.object({ key: z.string(), value: z.string() }), - }), - ) - .min(0), - }), - ), - }); + const {fields, append: fieldAppend, remove: fieldRemove} = useFieldArray({control, name: "data"}); - const { fields, append: fieldAppend, remove: fieldRemove } = useFieldArray({ control, name: "data" }); + const { + register: eventRegister, + handleSubmit: eventHandleSubmit, + formState: {errors: eventErrors}, + reset: eventReset, + } = useForm({ + resolver: zodResolver(EventSchemas.post.pick({event: true})), + }); - const { - register: eventRegister, - handleSubmit: eventHandleSubmit, - formState: { errors: eventErrors }, - reset: eventReset, - } = useForm({ - resolver: zodResolver(EventSchemas.post.pick({ event: true })), - }); + useEffect(() => { + if (!contact) { + return; + } - useEffect(() => { - if (!contact) { - return; - } + reset({ + email: contact.email, + subscribed: contact.subscribed, + }); + dataReset({ + data: Object.entries(JSON.parse(contact.data ? contact.data : "{}")).map(([key, value]) => ({ + value: {key, value}, + })), + }); + }, [dataReset, reset, contact]); - reset({ - email: contact.email, - subscribed: contact.subscribed, - }); - dataReset({ - data: Object.entries(JSON.parse(contact.data ? contact.data : "{}")).map(([key, value]) => ({ - value: { key, value }, - })), - }); - }, [dataReset, reset, contact]); + if (!contact) { + return ; + } - if (!contact) { - return ; - } + const create = (data: EventValues) => { + toast.promise( + network.mock(project.secret, "POST", "/v1", { + ...data, + email: contact.email, + }), + { + loading: "Creating new event", + success: () => { + void mutate(); + eventReset(); + return "Created new event"; + }, + error: "Could not create new event!", + }, + ); - const create = (data: EventValues) => { - toast.promise( - network.mock(project.secret, "POST", "/v1", { - ...data, - email: contact.email, - }), - { - loading: "Creating new event", - success: () => { - void mutate(); - eventReset(); - return "Created new event"; - }, - error: "Could not create new event!", - }, - ); + setEventModal(false); + }; - setEventModal(false); - }; + const update = (data: ContactValues) => { + const entries = getDataValues().data.map(({value}) => [value.key, value.value]); + let dataObject = {}; - const update = (data: ContactValues) => { - const entries = getDataValues().data.map(({ value }) => [value.key, value.value]); - let dataObject = {}; + entries.forEach(([key, value]) => { + Object.assign(dataObject, {[key]: value}); + }); - entries.forEach(([key, value]) => { - Object.assign(dataObject, { [key]: value }); - }); + dataObject = Object.fromEntries(Object.entries(dataObject).filter(([, value]) => value !== "")); - dataObject = Object.fromEntries(Object.entries(dataObject).filter(([, value]) => value !== "")); + toast.promise( + network.mock(project.secret, "PUT", "/v1/contacts", { + ...data, + data: dataObject, + }), + { + loading: "Saving your changes", + success: () => { + void mutate(); + return "Saved your changes"; + }, + error: "Could not save your changes!", + }, + ); + }; - toast.promise( - network.mock(project.secret, "PUT", "/v1/contacts", { - ...data, - data: dataObject, - }), - { - loading: "Saving your changes", - success: () => { - void mutate(); - return "Saved your changes"; - }, - error: "Could not save your changes!", - }, - ); - }; + const remove = async (e: { preventDefault: () => void }) => { + e.preventDefault(); + toast.promise( + network.mock(project.secret, "DELETE", "/v1/contacts", { + id: contact.id, + }), + { + loading: "Deleting contact", + success: "Deleted contact", + error: "Could not delete contact!", + }, + ); - const remove = async (e: { preventDefault: () => void }) => { - e.preventDefault(); - toast.promise( - network.mock(project.secret, "DELETE", "/v1/contacts", { - id: contact.id, - }), - { - loading: "Deleting contact", - success: "Deleted contact", - error: "Could not delete contact!", - }, - ); + await router.push("/contacts"); + }; - await router.push("/contacts"); - }; - - return ( - <> - setEventModal(!eventModal)} - onAction={eventHandleSubmit(create)} - type={"info"} - action={"Trigger"} - title={"Trigger event"} - description={`Trigger an event for ${contact.email}`} - icon={ - <> - - - - } - > - - - - - - - - } - > -
-
- - {contact.email[0].toUpperCase()} + return ( + <> + setEventModal(!eventModal)} + onAction={eventHandleSubmit(create)} + type={"info"} + action={"Trigger"} + title={"Trigger event"} + description={`Trigger an event for ${contact.email}`} + icon={ + <> + + + + } + > + + + + + + + + } + > + +
+ + {contact.email[0].toUpperCase()} -

- {contact.email[0].toUpperCase()} - {contact.email.slice(1)} -

-
+

+ {contact.email[0].toUpperCase()} + {contact.email.slice(1)} +

+
-
-
- - -
+
+
+ + +
- {fields.length > 0 ? ( - fields.map((field, index) => { - return ( - <> -
-
-
- - -
+ {fields.length > 0 ? ( + fields.map((field, index) => { + return ( + <> +
+
+
+ + +
-
- - -
- -
-
- - ); - }) - ) : ( -

No fields added

- )} -
+
+ + +
+ +
+
+ + ); + }) + ) : ( +

No fields added

+ )} +
-
- setValue("subscribed", !watch("subscribed"))} - /> -
+
+ setValue("subscribed", !watch("subscribed"))} + /> +
-
- - - Save - -
-
-
- - {contact.triggers.length > 0 || contact.emails.length > 0 ? ( -
-
    - {[...contact.triggers, ...contact.emails] - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) - .map((t, index) => { - if (t.messageId) { - const email = t as Email; +
    + + + Save + +
    + + + + {contact.triggers.length > 0 || contact.emails.length > 0 ? ( +
    +
      + {[...contact.triggers, ...contact.emails] + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .map((t, index) => { + if (t.messageId) { + const email = t as Email; + const expanded = selectedEmail?.id === email.id; - return ( -
    • -
      - {contact.triggers.length + contact.emails.length - 1 !== index && ( -
    • + {contact.triggers.length + contact.emails.length - 1 !== index && ( +
    • + ) + } + + if (t.action) { + return ( +
    • +
      + {contact.triggers.length + contact.emails.length - 1 !== index && ( +
      -
    • - ); - } +
    +
    +
    +

    {t.action.name} triggered

    +
    +
    + +
    +
    +
+ + + ); + } - if (t.action) { - return ( -
  • -
    - {contact.triggers.length + contact.emails.length - 1 !== index && ( -
    -
  • - ); - } - - if (t.event) { - return ( -
  • -
    - {contact.triggers.length + contact.emails.length - 1 !== index && ( -
  • +
    + {contact.triggers.length + contact.emails.length - 1 !== index && ( +
    -
  • - ); - } - })} - - - ) : ( - - )} -
    -
    - - ); + +
    +
    +

    + {t.event.templateId || t.event.campaignId + ? `${t.event.name.charAt(0).toUpperCase()}${t.event.name + .replaceAll("-", " ") + .slice(1) + .replace(/(delivered|opened)$/, "")}` + : t.event.name}{" "} + {t.event.templateId + ? t.event.name.endsWith("delivered") + ? "delivered" + : "opened" + : t.event.campaignId + ? t.event.name.endsWith("delivered") + ? "delivered" + : "opened" + : "triggered"} +

    +
    +
    + +
    +
    + + + + ); + } + })} + + + ) : ( + + )} + + + + ); } diff --git a/yarn.lock b/yarn.lock index 4621824..1dc2cf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3041,6 +3041,7 @@ __metadata: autoprefixer: "npm:^10.4.19" classnames: "npm:^2.5.1" dayjs: "npm:^1.11.12" + dompurify: "npm:^3.2.6" framer-motion: "npm:^11.3.7" jotai: "npm:2.9.0" lucide-react: "npm:^0.408.0" @@ -4777,6 +4778,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c + languageName: node + linkType: hard + "@types/unist@npm:^2": version: 2.0.11 resolution: "@types/unist@npm:2.0.11" @@ -6565,6 +6573,18 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:^3.2.6": + version: 3.2.6 + resolution: "dompurify@npm:3.2.6" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10c0/c8f8e5b0879a0d93c84a2e5e78649a47d0c057ed0f7850ca3d573d2cca64b84fb1ff85bd4b20980ade69c4e5b80ae73011340f1c2ff375c7ef98bb8268e1d13a + languageName: node + linkType: hard + "domutils@npm:^2.4.2": version: 2.8.0 resolution: "domutils@npm:2.8.0"