Merge pull request #118 from nklmantey/replace-variables-aws-region
Replace variables aws region
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
.env
|
||||
.yarn/
|
||||
.next/
|
||||
.github/
|
||||
dist/
|
||||
|
||||
+894
File diff suppressed because one or more lines are too long
@@ -1 +1,3 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.3.0.cjs
|
||||
|
||||
+7
-2
@@ -8,11 +8,16 @@ Support can be asked in the `#contributions` channel of the [Plunk Discord serve
|
||||
|
||||
- Docker needs to be [installed](https://docs.docker.com/engine/install/) on your system.
|
||||
|
||||
### 2. Set your environment variables
|
||||
### 2. Install dependencies
|
||||
|
||||
- Run `yarn install` to install the dependencies.
|
||||
|
||||
### 3. Set your environment variables
|
||||
|
||||
- Copy the `.env.example` files in the `api`, `dashboard` and `prisma` folder to `.env` in their respective folders.
|
||||
- Set AWS credentials in the `api` `.env` file.
|
||||
|
||||
### 3. Start resources
|
||||
### 4. Start resources
|
||||
|
||||
- Run `yarn services:up` to start a local database and a local redis server.
|
||||
- Run `yarn migrate` to apply the migrations to the database.
|
||||
|
||||
Regular → Executable
+21
-5
@@ -7,10 +7,26 @@ if [ -z "${API_URI}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find and replace baked values with real values for the API_URI
|
||||
find /app/packages/dashboard/public /app/packages/dashboard/.next -type f -name "*.js" |
|
||||
while read file; do
|
||||
sed -i "s|PLUNK_API_URI|${API_URI}|g" "$file"
|
||||
if [ -z "${AWS_REGION}" ]; then
|
||||
echo "AWS_REGION is not set. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Process each directory that might contain JS files
|
||||
for dir in "/app/packages/dashboard/public" "/app/packages/dashboard/.next"; do
|
||||
if [ -d "$dir" ]; then
|
||||
# Find all JS files and process them
|
||||
find "$dir" -type f -name "*.js" -o -name "*.mjs" | while read -r file; do
|
||||
if [ -f "$file" ]; then
|
||||
# Replace environment variables
|
||||
sed -i "s|PLUNK_API_URI|${API_URI}|g" "$file"
|
||||
sed -i "s|PLUNK_AWS_REGION|${AWS_REGION}|g" "$file"
|
||||
echo "Processed: $file"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "Warning: Directory $dir does not exist, skipping..."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Environment Variables Baked."
|
||||
echo "Environment Variables Baked."
|
||||
|
||||
+7
-4
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "plunk",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.9",
|
||||
"private": true,
|
||||
"license": "agpl-3.0",
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"]
|
||||
"packages": [
|
||||
"packages/*"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=6.14.x",
|
||||
"yarn": "1.22.x",
|
||||
"yarn": "4.3.x",
|
||||
"node": ">=18.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -34,5 +36,6 @@
|
||||
"generate": "prisma generate",
|
||||
"services:up": "docker compose -f docker-compose.dev.yml up -d",
|
||||
"services:down": "docker compose -f docker-compose.dev.yml down"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@4.3.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ENV
|
||||
JWT_SECRET=mysupersecretJWTsecret
|
||||
REDIS_URL=redis://127.0.0.1:56379
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:55432/postgres
|
||||
DISABLE_SIGNUPS=false
|
||||
|
||||
# AWS
|
||||
|
||||
@@ -4,12 +4,22 @@ import { API_URI } from "./constants";
|
||||
|
||||
export const task = cron.schedule("* * * * *", () => {
|
||||
signale.info("Running scheduled tasks");
|
||||
void fetch(`${API_URI}/tasks`, {
|
||||
method: "POST",
|
||||
});
|
||||
try {
|
||||
void fetch(`${API_URI}/tasks`, {
|
||||
method: "POST",
|
||||
});
|
||||
} catch (e) {
|
||||
signale.error("Failed to run scheduled tasks. Please check the error below");
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
signale.info("Updating verified identities");
|
||||
void fetch(`${API_URI}/identities/update`, {
|
||||
method: "POST",
|
||||
});
|
||||
try {
|
||||
void fetch(`${API_URI}/identities/update`, {
|
||||
method: "POST",
|
||||
});
|
||||
} catch (e) {
|
||||
signale.error("Failed to update verified identities. Please check the error below");
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,6 +41,9 @@ export class Tasks {
|
||||
let subject = "";
|
||||
let body = "";
|
||||
|
||||
let email = "";
|
||||
let name = "";
|
||||
|
||||
if (action) {
|
||||
const { template, notevents } = action;
|
||||
|
||||
@@ -52,6 +55,9 @@ export class Tasks {
|
||||
}
|
||||
}
|
||||
|
||||
email = project.verified && project.email ? template.email ?? project.email : "no-reply@useplunk.dev";
|
||||
name = template.from ?? project.from ?? project.name;
|
||||
|
||||
({ subject, body } = EmailService.format({
|
||||
subject: template.subject,
|
||||
body: template.body,
|
||||
@@ -62,6 +68,9 @@ export class Tasks {
|
||||
},
|
||||
}));
|
||||
} else if (campaign) {
|
||||
email = project.verified && project.email ? campaign.email ?? project.email : "no-reply@useplunk.dev";
|
||||
name = campaign.from ?? project.from ?? project.name;
|
||||
|
||||
({ subject, body } = EmailService.format({
|
||||
subject: campaign.subject,
|
||||
body: campaign.body,
|
||||
@@ -75,8 +84,8 @@ export class Tasks {
|
||||
|
||||
const { messageId } = await EmailService.send({
|
||||
from: {
|
||||
name: project.from ?? project.name,
|
||||
email: project.verified && project.email ? project.email : "no-reply@useplunk.dev",
|
||||
name,
|
||||
email,
|
||||
},
|
||||
to: [contact.email],
|
||||
content: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CampaignSchemas, UtilitySchemas } from "@plunk/shared";
|
||||
import dayjs from "dayjs";
|
||||
import type { Request, Response } from "express";
|
||||
import { prisma } from "../../database/prisma";
|
||||
import { HttpException, NotFound } from "../../exceptions";
|
||||
import { HttpException, NotAllowed, NotFound } from "../../exceptions";
|
||||
import { type IJwt, type ISecret, isAuthenticated, isValidSecretKey } from "../../middleware/auth";
|
||||
import { CampaignService } from "../../services/CampaignService";
|
||||
import { ContactService } from "../../services/ContactService";
|
||||
@@ -160,6 +160,8 @@ export class Campaigns {
|
||||
subject: campaign.subject,
|
||||
body: campaign.body,
|
||||
style: campaign.style,
|
||||
email: campaign.email,
|
||||
from: campaign.from,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -180,7 +182,15 @@ export class Campaigns {
|
||||
throw new NotFound("project");
|
||||
}
|
||||
|
||||
let { subject, body, recipients, style } = CampaignSchemas.create.parse(req.body);
|
||||
let { subject, body, recipients, style, email, from } = CampaignSchemas.create.parse(req.body);
|
||||
|
||||
if (email && !project.verified) {
|
||||
throw new NotAllowed("You need to attach a domain to your project to customize the sender address");
|
||||
}
|
||||
|
||||
if (email && email.split("@")[1] !== project.email?.split("@")[1]) {
|
||||
throw new NotAllowed("The sender address must be the same domain as the project's email address");
|
||||
}
|
||||
|
||||
if (recipients.length === 1 && recipients[0] === "all") {
|
||||
const projectContacts = await prisma.contact.findMany({
|
||||
@@ -197,6 +207,8 @@ export class Campaigns {
|
||||
subject,
|
||||
body,
|
||||
style,
|
||||
from: from === "" ? null : from,
|
||||
email: email === "" ? null : email,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -253,7 +265,15 @@ export class Campaigns {
|
||||
throw new NotFound("project");
|
||||
}
|
||||
|
||||
let { id, subject, body, recipients, style } = CampaignSchemas.update.parse(req.body);
|
||||
let { id, subject, body, recipients, style, email, from } = CampaignSchemas.update.parse(req.body);
|
||||
|
||||
if (email && !project.verified) {
|
||||
throw new NotAllowed("You need to attach a domain to your project to customize the sender address");
|
||||
}
|
||||
|
||||
if (email && email.split("@")[1] !== project.email?.split("@")[1]) {
|
||||
throw new NotAllowed("The sender address must be the same domain as the project's email address");
|
||||
}
|
||||
|
||||
if (recipients.length === 1 && recipients[0] === "all") {
|
||||
const projectContacts = await prisma.contact.findMany({
|
||||
@@ -276,6 +296,8 @@ export class Campaigns {
|
||||
subject,
|
||||
body,
|
||||
style,
|
||||
from: from === "" ? null : from,
|
||||
email: email === "" ? null : email,
|
||||
},
|
||||
include: {
|
||||
recipients: { select: { id: true } },
|
||||
|
||||
@@ -60,6 +60,8 @@ export class Templates {
|
||||
body: template.body,
|
||||
type: template.type,
|
||||
style: template.style,
|
||||
email: template.email,
|
||||
from: template.from,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -101,7 +103,15 @@ export class Templates {
|
||||
throw new NotFound("project");
|
||||
}
|
||||
|
||||
const { subject, body, type, style } = TemplateSchemas.create.parse(req.body);
|
||||
const { subject, body, type, style, email, from } = TemplateSchemas.create.parse(req.body);
|
||||
|
||||
if (email && !project.verified) {
|
||||
throw new NotAllowed("You need to attach a domain to your project to customize the sender address");
|
||||
}
|
||||
|
||||
if (email && email.split("@")[1] !== project.email?.split("@")[1]) {
|
||||
throw new NotAllowed("The sender address must be the same domain as the project's email address");
|
||||
}
|
||||
|
||||
const template = await prisma.template.create({
|
||||
data: {
|
||||
@@ -110,6 +120,8 @@ export class Templates {
|
||||
body,
|
||||
type,
|
||||
style,
|
||||
from: from === "" ? null : from,
|
||||
email: email === "" ? null : email,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -151,7 +163,7 @@ export class Templates {
|
||||
throw new NotFound("project");
|
||||
}
|
||||
|
||||
const { id, subject, body, type, style } = TemplateSchemas.update.parse(req.body);
|
||||
const { id, subject, body, type, style, email, from } = TemplateSchemas.update.parse(req.body);
|
||||
|
||||
let template = await TemplateService.id(id);
|
||||
|
||||
@@ -159,9 +171,24 @@ export class Templates {
|
||||
throw new NotFound("template");
|
||||
}
|
||||
|
||||
if (email && !project.verified) {
|
||||
throw new NotAllowed("You need to attach a domain to your project to customize the sender address");
|
||||
}
|
||||
|
||||
if (email && email.split("@")[1] !== project.email?.split("@")[1]) {
|
||||
throw new NotAllowed("The sender address must be the same domain as the project's email address");
|
||||
}
|
||||
|
||||
template = await prisma.template.update({
|
||||
where: { id },
|
||||
data: { subject, body, type, style },
|
||||
data: {
|
||||
subject,
|
||||
body,
|
||||
type,
|
||||
style,
|
||||
from: from === "" ? null : from,
|
||||
email: email === "" ? null : email,
|
||||
},
|
||||
include: {
|
||||
actions: true,
|
||||
},
|
||||
|
||||
@@ -78,15 +78,12 @@ export class V1 {
|
||||
redis.del(Keys.Contact.id(contact.id));
|
||||
redis.del(Keys.Contact.email(project.id, contact.email));
|
||||
} else {
|
||||
if (subscribed && contact.subscribed !== subscribed) {
|
||||
contact = await prisma.contact.update({
|
||||
where: { id: contact.id },
|
||||
data: { subscribed },
|
||||
});
|
||||
|
||||
if (subscribed !== null && contact.subscribed !== subscribed) {
|
||||
contact = await prisma.contact.update({where: {id: contact.id}, data: {subscribed}});
|
||||
|
||||
redis.del(Keys.Contact.id(contact.id));
|
||||
redis.del(Keys.Contact.email(project.id, contact.email));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
|
||||
@@ -120,8 +120,8 @@ export class ActionService {
|
||||
|
||||
const { messageId } = await EmailService.send({
|
||||
from: {
|
||||
name: project.from ?? project.name,
|
||||
email: project.verified && project.email ? project.email : "no-reply@useplunk.dev",
|
||||
name: action.template.from ?? project.from ?? project.name,
|
||||
email: project.verified && project.email ? action.template.email ?? project.email : "no-reply@useplunk.dev",
|
||||
},
|
||||
to: [contact.email],
|
||||
content: {
|
||||
|
||||
@@ -107,7 +107,7 @@ ${
|
||||
<hr style="border: none; border-top: 1px solid #eaeaea; width: 100%; margin-top: 12px; margin-bottom: 12px;">
|
||||
<p style="font-size: 12px; line-height: 24px; margin: 16px 0; text-align: center; color: rgb(64, 64, 64);">
|
||||
You received this email because you agreed to receive emails from ${project.name}. If you no longer wish to receive emails like this, please
|
||||
<a href="https://${APP_URI}/unsubscribe/${contact.id}">update your preferences</a>.
|
||||
<a href="${APP_URI.startsWith("https://") ? APP_URI : `https://${APP_URI}`}/unsubscribe/${contact.id}">update your preferences</a>.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -472,7 +472,7 @@ ${
|
||||
<mj-divider border-width="2px" border-color="#f5f5f5"></mj-divider>
|
||||
<mj-text align="center">
|
||||
<p style="color: #a3a3a3; text-decoration: none; font-size: 12px; line-height: 1.7142857;">
|
||||
You received this email because you agreed to receive emails from ${project.name}. If you no longer wish to receive emails like this, please <a style="text-decoration: underline" href="${APP_URI}/unsubscribe/${contact.id}" target="_blank">update your preferences</a>.
|
||||
You received this email because you agreed to receive emails from ${project.name}. If you no longer wish to receive emails like this, please <a style="text-decoration: underline" href="${APP_URI.startsWith("https://") ? APP_URI : `https://${APP_URI}`}/unsubscribe/${contact.id}" target="_blank">update your preferences</a>.
|
||||
</p>
|
||||
</mj-text>
|
||||
`
|
||||
|
||||
@@ -32,6 +32,8 @@ import { network } from "../../lib/network";
|
||||
interface CampaignValues {
|
||||
subject: string;
|
||||
body: string;
|
||||
email?: string;
|
||||
from?: string;
|
||||
recipients: string[];
|
||||
style: "PLUNK" | "HTML";
|
||||
}
|
||||
@@ -71,6 +73,8 @@ export default function Index() {
|
||||
watch,
|
||||
reset,
|
||||
setValue,
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<CampaignValues>({
|
||||
resolver: zodResolver(CampaignSchemas.update),
|
||||
defaultValues: { recipients: [], body: undefined },
|
||||
@@ -87,6 +91,21 @@ export default function Index() {
|
||||
});
|
||||
}, [reset, campaign]);
|
||||
|
||||
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 || !campaign || !events || (watch("body") as string | undefined) === undefined) {
|
||||
return <FullscreenLoader />;
|
||||
}
|
||||
@@ -404,13 +423,35 @@ export default function Index() {
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit(update)} className="space-6 grid gap-6 sm:grid-cols-6">
|
||||
<Input
|
||||
className={"sm:col-span-6"}
|
||||
label={"Subject"}
|
||||
placeholder={`Welcome to ${project.name}!`}
|
||||
register={register("subject")}
|
||||
error={errors.subject}
|
||||
/>
|
||||
<div className={"sm:col-span-6 grid sm:grid-cols-6 gap-6"}>
|
||||
<Input
|
||||
className={"sm:col-span-6"}
|
||||
label={"Subject"}
|
||||
placeholder={`Welcome to ${project.name}!`}
|
||||
register={register("subject")}
|
||||
error={errors.subject}
|
||||
/>
|
||||
|
||||
{project.verified && (
|
||||
<Input
|
||||
className={"sm:col-span-3"}
|
||||
label={"Sender Email"}
|
||||
placeholder={`${project.email}`}
|
||||
register={register("email")}
|
||||
error={errors.email}
|
||||
/>
|
||||
)}
|
||||
|
||||
{project.verified && (
|
||||
<Input
|
||||
className={"sm:col-span-3"}
|
||||
label={"Sender Name"}
|
||||
placeholder={`${project.from ?? project.name}`}
|
||||
register={register("from")}
|
||||
error={errors.from}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{contacts ? (
|
||||
<>
|
||||
|
||||
@@ -6,7 +6,7 @@ import dayjs from "dayjs";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Search, Users2, XIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
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";
|
||||
@@ -20,6 +20,8 @@ import { network } from "../../lib/network";
|
||||
interface CampaignValues {
|
||||
subject: string;
|
||||
body: string;
|
||||
email?: string;
|
||||
from?: string;
|
||||
recipients: string[];
|
||||
style: "PLUNK" | "HTML";
|
||||
}
|
||||
@@ -58,6 +60,8 @@ export default function Index() {
|
||||
formState: { errors },
|
||||
setValue,
|
||||
watch,
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<CampaignValues>({
|
||||
resolver: zodResolver(CampaignSchemas.create),
|
||||
defaultValues: {
|
||||
@@ -67,6 +71,21 @@ export default function Index() {
|
||||
},
|
||||
});
|
||||
|
||||
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 <FullscreenLoader />;
|
||||
}
|
||||
@@ -194,13 +213,36 @@ export default function Index() {
|
||||
<Dashboard>
|
||||
<Card title={"Create a new campaign"}>
|
||||
<form onSubmit={handleSubmit(create)} className="space-6 grid gap-6 sm:grid-cols-6">
|
||||
<Input
|
||||
className={"sm:col-span-6"}
|
||||
label={"Subject"}
|
||||
placeholder={`Welcome to ${project.name}!`}
|
||||
register={register("subject")}
|
||||
error={errors.subject}
|
||||
/>
|
||||
<div className={"sm:col-span-6 grid sm:grid-cols-6 gap-6"}>
|
||||
<Input
|
||||
className={"sm:col-span-6"}
|
||||
label={"Subject"}
|
||||
placeholder={`Welcome to ${project.name}!`}
|
||||
register={register("subject")}
|
||||
error={errors.subject}
|
||||
/>
|
||||
|
||||
{project.verified && (
|
||||
<Input
|
||||
className={"sm:col-span-3"}
|
||||
label={"Sender Email"}
|
||||
placeholder={`${project.email}`}
|
||||
register={register("email")}
|
||||
error={errors.email}
|
||||
/>
|
||||
)}
|
||||
|
||||
{project.verified && (
|
||||
<Input
|
||||
className={"sm:col-span-3"}
|
||||
label={"Sender Name"}
|
||||
placeholder={`${project.from ?? project.name}`}
|
||||
register={register("from")}
|
||||
error={errors.from}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{contacts ? (
|
||||
<>
|
||||
<div className={"sm:col-span-3"}>
|
||||
|
||||
@@ -12,305 +12,308 @@ import { useActiveProject, useActiveProjectVerifiedIdentity, useProjects } from
|
||||
import { network } from "../../lib/network";
|
||||
|
||||
interface EmailValues {
|
||||
email: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface FromValues {
|
||||
from: string;
|
||||
from: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function Index() {
|
||||
const activeProject = useActiveProject();
|
||||
const { mutate: projectsMutate } = useProjects();
|
||||
const { data: identity, mutate: identityMutate } = useActiveProjectVerifiedIdentity();
|
||||
const activeProject = useActiveProject();
|
||||
const { mutate: projectsMutate } = useProjects();
|
||||
const { data: identity, mutate: identityMutate } = useActiveProjectVerifiedIdentity();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<EmailValues>({
|
||||
resolver: zodResolver(IdentitySchemas.create.omit({ id: true })),
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<EmailValues>({
|
||||
resolver: zodResolver(IdentitySchemas.create.omit({ id: true })),
|
||||
});
|
||||
|
||||
const {
|
||||
register: registerUpdate,
|
||||
handleSubmit: handleSubmitUpdate,
|
||||
formState: { errors: errorsUpdate },
|
||||
reset,
|
||||
} = useForm<FromValues>({
|
||||
resolver: zodResolver(IdentitySchemas.update.omit({ id: true })),
|
||||
});
|
||||
const {
|
||||
register: registerUpdate,
|
||||
handleSubmit: handleSubmitUpdate,
|
||||
formState: { errors: errorsUpdate },
|
||||
reset,
|
||||
} = useForm<FromValues>({
|
||||
resolver: zodResolver(IdentitySchemas.update.omit({ id: true })),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
reset({ from: activeProject.from ?? undefined });
|
||||
}, [reset, activeProject]);
|
||||
reset({ from: activeProject.from ?? undefined });
|
||||
}, [reset, activeProject]);
|
||||
|
||||
if (!activeProject || !identity) {
|
||||
return <FullscreenLoader />;
|
||||
}
|
||||
if (!activeProject || !identity) {
|
||||
return <FullscreenLoader />;
|
||||
}
|
||||
|
||||
const create = async (data: EmailValues) => {
|
||||
toast.promise(
|
||||
network.fetch<
|
||||
{
|
||||
success: true;
|
||||
tokens: string[];
|
||||
},
|
||||
typeof IdentitySchemas.create
|
||||
>("POST", "/identities/create", {
|
||||
id: activeProject.id,
|
||||
...data,
|
||||
}),
|
||||
{
|
||||
loading: "Adding your domain",
|
||||
success: (res) => {
|
||||
void identityMutate({ tokens: res.tokens }, { revalidate: false });
|
||||
void projectsMutate();
|
||||
const create = async (data: EmailValues) => {
|
||||
toast.promise(
|
||||
network.fetch<
|
||||
{
|
||||
success: true;
|
||||
tokens: string[];
|
||||
},
|
||||
typeof IdentitySchemas.create
|
||||
>("POST", "/identities/create", {
|
||||
id: activeProject.id,
|
||||
...data,
|
||||
}),
|
||||
{
|
||||
loading: "Adding your domain",
|
||||
success: (res) => {
|
||||
void identityMutate({ tokens: res.tokens }, { revalidate: false });
|
||||
void projectsMutate();
|
||||
|
||||
return "Added your domain";
|
||||
},
|
||||
error: "Could not add domain",
|
||||
},
|
||||
);
|
||||
};
|
||||
return "Added your domain";
|
||||
},
|
||||
error: "Could not add domain",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const update = async (data: FromValues) => {
|
||||
toast.promise(
|
||||
network.fetch<
|
||||
{
|
||||
success: true;
|
||||
},
|
||||
typeof IdentitySchemas.update
|
||||
>("PUT", "/projects/update/identity", {
|
||||
id: activeProject.id,
|
||||
...data,
|
||||
}),
|
||||
{
|
||||
loading: "Updating your sender name",
|
||||
success: "Updated your sender name",
|
||||
error: "Could not update sender name",
|
||||
},
|
||||
);
|
||||
const update = async (data: FromValues) => {
|
||||
toast.promise(
|
||||
network.fetch<
|
||||
{
|
||||
success: true;
|
||||
},
|
||||
typeof IdentitySchemas.update
|
||||
>("PUT", "/projects/update/identity", {
|
||||
id: activeProject.id,
|
||||
...data,
|
||||
}),
|
||||
{
|
||||
loading: "Updating your sender name",
|
||||
success: "Updated your sender name",
|
||||
error: "Could not update sender name",
|
||||
},
|
||||
);
|
||||
|
||||
await identityMutate();
|
||||
await projectsMutate();
|
||||
};
|
||||
await identityMutate();
|
||||
await projectsMutate();
|
||||
};
|
||||
|
||||
const unlink = async () => {
|
||||
toast.promise(
|
||||
network.fetch<
|
||||
{
|
||||
success: true;
|
||||
},
|
||||
typeof UtilitySchemas.id
|
||||
>("POST", "/identities/reset", {
|
||||
id: activeProject.id,
|
||||
}),
|
||||
{
|
||||
loading: "Unlinking your domain",
|
||||
success: "Unlinked your domain",
|
||||
error: "Could not unlink domain",
|
||||
},
|
||||
);
|
||||
const unlink = async () => {
|
||||
toast.promise(
|
||||
network.fetch<
|
||||
{
|
||||
success: true;
|
||||
},
|
||||
typeof UtilitySchemas.id
|
||||
>("POST", "/identities/reset", {
|
||||
id: activeProject.id,
|
||||
}),
|
||||
{
|
||||
loading: "Unlinking your domain",
|
||||
success: "Unlinked your domain",
|
||||
error: "Could not unlink domain",
|
||||
},
|
||||
);
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dashboard>
|
||||
<SettingTabs />
|
||||
const domain = activeProject.email?.split("@")[1] ?? "";
|
||||
const subdomain = domain.split(".").length > 2 ? domain.split(".")[0] : "";
|
||||
|
||||
<Card
|
||||
title={"Domain"}
|
||||
description={"By sending emails from your own domain you build up domain authority and trust."}
|
||||
actions={
|
||||
activeProject.email && (
|
||||
<>
|
||||
<button
|
||||
onClick={unlink}
|
||||
className={
|
||||
"flex items-center gap-x-2 rounded bg-red-600 px-8 py-2 text-center text-sm font-medium text-white transition ease-in-out hover:bg-red-700"
|
||||
}
|
||||
>
|
||||
<Unlink strokeWidth={1.5} size={18} />
|
||||
Unlink domain
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
{activeProject.email && !activeProject.verified ? (
|
||||
<>
|
||||
<Alert type={"warning"} title={"Waiting for DNS verification"}>
|
||||
Please add the following records to {activeProject.email.split("@")[1]} to verify {activeProject.email}, this
|
||||
may take up to 15 minutes to register. <br />
|
||||
In the meantime you can already start sending emails, we will automatically switch to your domain once it is
|
||||
verified.
|
||||
</Alert>
|
||||
return (
|
||||
<>
|
||||
<Dashboard>
|
||||
<SettingTabs />
|
||||
|
||||
<div className="mt-6">
|
||||
<Table
|
||||
values={[
|
||||
{
|
||||
Type: <Badge type={"info"}>TXT</Badge>,
|
||||
Key: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText("plunk");
|
||||
toast.success("Copied key to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>plunk</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
Value: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText("v=spf1 include:amazonses.com ~all");
|
||||
toast.success("Copied value to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>v=spf1 include:amazonses.com ~all</p> <Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: <Badge type={"info"}>MX</Badge>,
|
||||
Key: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText("plunk");
|
||||
toast.success("Copied key to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>plunk</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
Value: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(`10 feedback-smtp.${AWS_REGION}.amazonses.com`);
|
||||
toast.success("Copied value to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>10 feedback-smtp.{AWS_REGION}.amazonses.com</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...identity.tokens.map((token) => {
|
||||
return {
|
||||
Type: <Badge type={"info"}>CNAME</Badge>,
|
||||
Key: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(`${token}._domainkey`);
|
||||
toast.success("Copied key to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>{token}._domainkey</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
Value: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(`${token}.dkim.amazonses.com`);
|
||||
toast.success("Copied value to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>{token}.dkim.amazonses.com</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : activeProject.email && activeProject.verified ? (
|
||||
<>
|
||||
<Alert type={"success"} title={"Domain verified"}>
|
||||
You have confirmed {activeProject.email} as your domain. Any emails sent by Plunk will now use this address.
|
||||
</Alert>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(create)} className="space-y-6">
|
||||
<Input register={register("email")} error={errors.email} placeholder={"hello@example.com"} label={"Email"} />
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={
|
||||
"ml-auto flex items-center gap-x-0.5 rounded bg-neutral-800 px-8 py-2 text-center text-sm font-medium text-white"
|
||||
}
|
||||
>
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M12 5.75V18.25"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M18.25 12L5.75 12"
|
||||
/>
|
||||
</svg>
|
||||
Verify domain
|
||||
</motion.button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
<Card
|
||||
title={"Domain"}
|
||||
description={"By sending emails from your own domain you build up domain authority and trust."}
|
||||
actions={
|
||||
activeProject.email && (
|
||||
<>
|
||||
<button
|
||||
onClick={unlink}
|
||||
className={
|
||||
"flex items-center gap-x-2 rounded bg-red-600 px-8 py-2 text-center text-sm font-medium text-white transition ease-in-out hover:bg-red-700"
|
||||
}
|
||||
>
|
||||
<Unlink strokeWidth={1.5} size={18} />
|
||||
Unlink domain
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
{activeProject.email && !activeProject.verified ? (
|
||||
<>
|
||||
<Alert type={"warning"} title={"Waiting for DNS verification"}>
|
||||
Please add the following records to {activeProject.email.split("@")[1]} to verify {activeProject.email}, this
|
||||
may take up to 15 minutes to register. <br />
|
||||
In the meantime you can already start sending emails, we will automatically switch to your domain once it is
|
||||
verified.
|
||||
</Alert>
|
||||
|
||||
<Card
|
||||
title={"Sender name"}
|
||||
description={
|
||||
"The name that will be used when sending emails from Plunk. Your project name will be used by default"
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmitUpdate(update)} className="space-y-6">
|
||||
<Input
|
||||
register={registerUpdate("from")}
|
||||
placeholder={activeProject.name}
|
||||
label={"Name"}
|
||||
error={errorsUpdate.from}
|
||||
/>
|
||||
<div className="mt-6">
|
||||
<Table
|
||||
values={[
|
||||
{
|
||||
Type: <Badge type={"info"}>TXT</Badge>,
|
||||
Key: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText("plunk");
|
||||
toast.success("Copied key to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>plunk</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
Value: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText("v=spf1 include:amazonses.com ~all");
|
||||
toast.success("Copied value to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>v=spf1 include:amazonses.com ~all</p> <Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: <Badge type={"info"}>MX</Badge>,
|
||||
Key: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText("plunk");
|
||||
toast.success("Copied key to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>plunk</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
Value: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(`10 feedback-smtp.${AWS_REGION}.amazonses.com`);
|
||||
toast.success("Copied value to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>10 feedback-smtp.{AWS_REGION}.amazonses.com</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...identity.tokens.map((token) => {
|
||||
return {
|
||||
Type: <Badge type={"info"}>CNAME</Badge>,
|
||||
Key: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(`${token}._domainkey${subdomain ? '.' + subdomain : ''}`);
|
||||
toast.success("Copied key to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>{token}._domainkey{subdomain ? '.' + subdomain : ''}</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
Value: (
|
||||
<div
|
||||
className={"flex cursor-pointer items-center gap-3"}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(`${token}.dkim.amazonses.com`);
|
||||
toast.success("Copied value to clipboard");
|
||||
}}
|
||||
>
|
||||
<p className={"font-mono text-sm"}>{token}.dkim.amazonses.com</p>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : activeProject.email && activeProject.verified ? (
|
||||
<>
|
||||
<Alert type={"success"} title={"Domain verified"}>
|
||||
You have confirmed {activeProject.email} as your domain. Any emails sent by Plunk will now use this address.
|
||||
</Alert>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(create)} className="space-y-6">
|
||||
<Input register={register("email")} error={errors.email} placeholder={"hello@example.com"} label={"Email"} />
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={
|
||||
"ml-auto flex items-center gap-x-0.5 rounded bg-neutral-800 px-8 py-2 text-center text-sm font-medium text-white"
|
||||
}
|
||||
>
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M12 5.75V18.25"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M18.25 12L5.75 12"
|
||||
/>
|
||||
</svg>
|
||||
Verify domain
|
||||
</motion.button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={
|
||||
"ml-auto flex items-center gap-x-0.5 rounded bg-neutral-800 px-8 py-2 text-center text-sm font-medium text-white"
|
||||
}
|
||||
>
|
||||
Save
|
||||
</motion.button>
|
||||
</form>
|
||||
</Card>
|
||||
</Dashboard>
|
||||
</>
|
||||
);
|
||||
<Card
|
||||
title={"Sender name"}
|
||||
description={
|
||||
"The name that will be used when sending emails from Plunk. Your project name will be used by default"
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmitUpdate(update)} className="space-y-6">
|
||||
<Input
|
||||
register={registerUpdate("from")}
|
||||
placeholder={activeProject.name}
|
||||
label={"Name"}
|
||||
error={errorsUpdate.from}
|
||||
/>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={
|
||||
"ml-auto flex items-center gap-x-0.5 rounded bg-neutral-800 px-8 py-2 text-center text-sm font-medium text-white"
|
||||
}
|
||||
>
|
||||
Save
|
||||
</motion.button>
|
||||
</form>
|
||||
</Card>
|
||||
</Dashboard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import { network } from "../../lib/network";
|
||||
interface TemplateValues {
|
||||
subject: string;
|
||||
body: string;
|
||||
email?: string;
|
||||
from?: string;
|
||||
type: "MARKETING" | "TRANSACTIONAL";
|
||||
style: "PLUNK" | "HTML";
|
||||
}
|
||||
@@ -41,6 +43,8 @@ export default function Index() {
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<TemplateValues>({
|
||||
resolver: zodResolver(TemplateSchemas.update),
|
||||
defaultValues: {
|
||||
@@ -56,6 +60,21 @@ export default function Index() {
|
||||
reset(template);
|
||||
}, [reset, template]);
|
||||
|
||||
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 || !template || (watch("body") as string | undefined) === undefined) {
|
||||
return <FullscreenLoader />;
|
||||
}
|
||||
@@ -248,6 +267,26 @@ export default function Index() {
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{project.verified && (
|
||||
<Input
|
||||
className={"sm:col-span-3"}
|
||||
label={"Sender Email"}
|
||||
placeholder={`${project.email}`}
|
||||
register={register("email")}
|
||||
error={errors.email}
|
||||
/>
|
||||
)}
|
||||
|
||||
{project.verified && (
|
||||
<Input
|
||||
className={"sm:col-span-3"}
|
||||
label={"Sender Name"}
|
||||
placeholder={`${project.from ?? project.name}`}
|
||||
register={register("from")}
|
||||
error={errors.from}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={"sm:col-span-6"}>
|
||||
<Editor
|
||||
value={watch("body")}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TemplateSchemas } from "@plunk/shared";
|
||||
import type { Template } from "@prisma/client";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { Card, Dropdown, Editor, FullscreenLoader, Input, Tooltip } from "../../components";
|
||||
@@ -15,6 +15,8 @@ import { network } from "../../lib/network";
|
||||
interface TemplateValues {
|
||||
subject: string;
|
||||
body: string;
|
||||
email?: string;
|
||||
from?: string;
|
||||
type: "MARKETING" | "TRANSACTIONAL";
|
||||
style: "PLUNK" | "HTML";
|
||||
}
|
||||
@@ -41,6 +43,8 @@ export default function Index() {
|
||||
formState: { errors },
|
||||
watch,
|
||||
setValue,
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<TemplateValues>({
|
||||
resolver: zodResolver(TemplateSchemas.create),
|
||||
defaultValues: {
|
||||
@@ -50,6 +54,21 @@ export default function Index() {
|
||||
},
|
||||
});
|
||||
|
||||
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) {
|
||||
return <FullscreenLoader />;
|
||||
}
|
||||
@@ -141,6 +160,26 @@ export default function Index() {
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{project.verified && (
|
||||
<Input
|
||||
className={"sm:col-span-3"}
|
||||
label={"Sender Email"}
|
||||
placeholder={`${project.email}`}
|
||||
register={register("email")}
|
||||
error={errors.email}
|
||||
/>
|
||||
)}
|
||||
|
||||
{project.verified && (
|
||||
<Input
|
||||
className={"sm:col-span-3"}
|
||||
label={"Sender Name"}
|
||||
placeholder={`${project.from ?? project.name}`}
|
||||
register={register("from")}
|
||||
error={errors.from}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={"sm:col-span-6"}>
|
||||
<Editor
|
||||
value={watch("body")}
|
||||
|
||||
@@ -133,6 +133,8 @@ export const CampaignSchemas = {
|
||||
.min(1, "Subject needs to be at least 1 character long")
|
||||
.max(70, "Subject needs to be less than 70 characters long"),
|
||||
body: z.string().min(1, "Body needs to be at least 1 character long"),
|
||||
email: email.nullish().or(z.literal("")),
|
||||
from: z.string().nullish(),
|
||||
recipients: z.array(z.string()),
|
||||
style: z.nativeEnum(TemplateStyle).default("PLUNK"),
|
||||
}),
|
||||
@@ -143,6 +145,8 @@ export const CampaignSchemas = {
|
||||
.min(1, "Subject needs to be at least 1 character long")
|
||||
.max(70, "Subject needs to be less than 70 characters long"),
|
||||
body: z.string().min(1, "Body needs to be at least 1 character long"),
|
||||
email: email.nullish().or(z.literal("")),
|
||||
from: z.string().nullish(),
|
||||
recipients: z.array(z.string()),
|
||||
style: z.nativeEnum(TemplateStyle).default("PLUNK"),
|
||||
}),
|
||||
@@ -208,6 +212,8 @@ export const TemplateSchemas = {
|
||||
create: z.object({
|
||||
subject: z.string().min(1, "Subject can't be empty").max(70, "Subject needs to be less than 70 characters long"),
|
||||
body: z.string().min(1, "Body can't be empty"),
|
||||
email: email.nullish().or(z.literal("")),
|
||||
from: z.string().nullish(),
|
||||
type: z.nativeEnum(TemplateType).default("MARKETING"),
|
||||
style: z.nativeEnum(TemplateStyle).default("PLUNK"),
|
||||
}),
|
||||
@@ -215,6 +221,8 @@ export const TemplateSchemas = {
|
||||
id,
|
||||
subject: z.string().min(1, "Subject can't be empty").max(70, "Subject needs to be less than 70 characters long"),
|
||||
body: z.string().min(1, "Body can't be empty"),
|
||||
email: email.nullish().or(z.literal("")),
|
||||
from: z.string().nullish(),
|
||||
type: z.nativeEnum(TemplateType).default("MARKETING"),
|
||||
style: z.nativeEnum(TemplateStyle).default("PLUNK"),
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "campaigns" ADD COLUMN "email" TEXT,
|
||||
ADD COLUMN "from" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "templates" ADD COLUMN "email" TEXT,
|
||||
ADD COLUMN "from" TEXT;
|
||||
@@ -108,6 +108,8 @@ model Template {
|
||||
// Details
|
||||
subject String
|
||||
body String
|
||||
email String?
|
||||
from String?
|
||||
|
||||
type TemplateType
|
||||
style TemplateStyle
|
||||
@@ -159,8 +161,11 @@ model Action {
|
||||
model Campaign {
|
||||
id String @id @default(uuid())
|
||||
|
||||
subject String
|
||||
body String
|
||||
subject String
|
||||
body String
|
||||
email String?
|
||||
from String?
|
||||
|
||||
status CampaignStatus @default(DRAFT)
|
||||
delivered DateTime?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user