♻️ Refactor email templating code (#533)

This commit is contained in:
Luke Vella 2023-03-03 11:46:30 +00:00 committed by GitHub
parent 0a836aeec7
commit 309cb109aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3926 additions and 1455 deletions

2
packages/emails/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.react-email
/out

View file

@ -0,0 +1,24 @@
{
"name": "@rallly/emails",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "email dev --port 3333 --dir ./src/templates"
},
"main": "./src/index.tsx",
"types": "./src/index.tsx",
"dependencies": {
"@react-email/components": "0.0.2",
"@react-email/render": "0.0.6",
"@react-email/tailwind": "0.0.6",
"clsx": "^1.2.1",
"nodemailer": "^6.9.1",
"react-email": "1.7.15"
},
"devDependencies": {
"@rallly/tailwind-config": "*",
"@rallly/tsconfig": "*",
"@rallly/utils": "*",
"@types/nodemailer": "^6.4.7"
}
}

27
packages/emails/readme.md Normal file
View file

@ -0,0 +1,27 @@
# React Email Starter
A live preview right in your browser so you don't need to keep sending real emails during development.
## Getting Started
First, install the dependencies:
```sh
npm install
# or
yarn
```
Then, run the development server:
```sh
npm run dev
# or
yarn dev
```
Open [localhost:3000](http://localhost:3000) with your browser to see the result.
## License
MIT License

View file

@ -0,0 +1 @@
export * from "./send-email";

View file

@ -0,0 +1,64 @@
import { render } from "@react-email/render";
import { createTransport, Transporter } from "nodemailer";
import React from "react";
import * as templates from "./templates";
type Templates = typeof templates;
type TemplateName = keyof typeof templates;
type TemplateProps<T extends TemplateName> = React.ComponentProps<
TemplateComponent<T>
>;
type TemplateComponent<T extends TemplateName> = Templates[T];
const env = process.env["NODE" + "_ENV"] || "development";
let transport: Transporter;
const getTransport = () => {
if (env === "test") {
transport = createTransport({ port: 4025 });
} else {
transport = createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : undefined,
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PWD,
},
});
}
return transport;
};
type SendEmailOptions<T extends TemplateName> = {
to: string;
subject: string;
props: TemplateProps<T>;
onError?: () => void;
};
export const sendEmail = async <T extends TemplateName>(
templateName: T,
options: SendEmailOptions<T>,
) => {
const transport = getTransport();
const Template = templates[templateName] as TemplateComponent<T>;
try {
return await transport.sendMail({
from: process.env.SUPPORT_EMAIL,
to: options.to,
subject: options.subject,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
html: render(<Template {...(options.props as any)} />),
});
} catch (e) {
console.error("Error sending email", templateName);
options.onError?.();
}
};

View file

@ -0,0 +1,6 @@
export * from "./templates/guest-verify-email";
export * from "./templates/new-comment";
export * from "./templates/new-participant";
export * from "./templates/new-poll";
export * from "./templates/new-poll-verification";
export * from "./templates/verification-code";

View file

@ -0,0 +1,162 @@
import tailwindConfig from "@rallly/tailwind-config";
import { absoluteUrl } from "@rallly/utils";
import {
Body,
Container,
Head,
Html,
Img,
Link,
Preview,
Section,
} from "@react-email/components";
import { Tailwind } from "@react-email/tailwind";
export const EmailLayout = (props: {
children: React.ReactNode;
preview: string;
}) => {
return (
<Html>
<Head />
<Preview>{props.preview}</Preview>
<Tailwind
config={{
theme: {
extend: {
...tailwindConfig.theme.extend,
spacing: {
screen: "100vw",
full: "100%",
px: "1px",
0: "0",
0.5: "2px",
1: "4px",
1.5: "6px",
2: "8px",
2.5: "10px",
3: "12px",
3.5: "14px",
4: "16px",
4.5: "18px",
5: "20px",
5.5: "22px",
6: "24px",
6.5: "26px",
7: "28px",
7.5: "30px",
8: "32px",
8.5: "34px",
9: "36px",
9.5: "38px",
10: "40px",
11: "44px",
12: "48px",
14: "56px",
16: "64px",
20: "80px",
24: "96px",
28: "112px",
32: "128px",
36: "144px",
40: "160px",
44: "176px",
48: "192px",
52: "208px",
56: "224px",
60: "240px",
64: "256px",
72: "288px",
80: "320px",
96: "384px",
97.5: "390px",
120: "480px",
150: "600px",
160: "640px",
175: "700px",
"1/2": "50%",
"1/3": "33.333333%",
"2/3": "66.666667%",
"1/4": "25%",
"2/4": "50%",
"3/4": "75%",
"1/5": "20%",
"2/5": "40%",
"3/5": "60%",
"4/5": "80%",
"1/6": "16.666667%",
"2/6": "33.333333%",
"3/6": "50%",
"4/6": "66.666667%",
"5/6": "83.333333%",
"1/12": "8.333333%",
"2/12": "16.666667%",
"3/12": "25%",
"4/12": "33.333333%",
"5/12": "41.666667%",
"6/12": "50%",
"7/12": "58.333333%",
"8/12": "66.666667%",
"9/12": "75%",
"10/12": "83.333333%",
"11/12": "91.666667%",
},
borderRadius: {
none: "0px",
sm: "2px",
DEFAULT: "4px",
md: "6px",
lg: "8px",
xl: "12px",
"2xl": "16px",
"3xl": "24px",
},
},
},
}}
>
<Body className="bg-gray-50 p-4">
<Container className="mx-auto bg-white p-6">
<Section className="mb-4">
<Img src={absoluteUrl("/logo.png")} alt="Rallly" width={128} />
</Section>
<Section>{props.children}</Section>
<Section className="mt-4 text-sm text-slate-500">
<Link className="font-sans text-slate-500" href={absoluteUrl()}>
Home
</Link>
&nbsp;&bull;&nbsp;
<Link
className="font-sans text-slate-500"
href="https://twitter.com/ralllyco"
>
Twitter
</Link>
&nbsp;&bull;&nbsp;
<Link
className="font-sans text-slate-500"
href="https://github.com/lukevella/rallly"
>
Github
</Link>
&nbsp;&bull;&nbsp;
<Link
className="font-sans text-slate-500"
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
>
Donate
</Link>
&nbsp;&bull;&nbsp;
<Link
className="font-sans text-slate-500"
href={`mailto:${process.env.SUPPORT_EMAIL}`}
>
Contact
</Link>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};

View file

@ -0,0 +1,34 @@
import { Container } from "@react-email/container";
import { Link, Text } from "./styled-components";
export interface NewPollBaseEmailProps {
title: string;
name: string;
adminLink: string;
}
export const NewPollBaseEmail = ({
name,
title,
adminLink,
children,
}: React.PropsWithChildren<NewPollBaseEmailProps>) => {
return (
<Container>
<Text>Hi {name},</Text>
<Text>
Your poll <strong>&quot;{title}&quot;</strong> has been created.
</Text>
<Text>
To manage your poll use the <em>admin link</em> below.
</Text>
<Text>
<Link href={adminLink}>
<span className="font-mono">{adminLink}</span> &rarr;
</Link>
</Text>
{children}
</Container>
);
};

View file

@ -0,0 +1,63 @@
import {
Button as UnstyledButton,
ButtonProps,
Heading as UnstyledHeading,
Link as UnstyledLink,
LinkProps,
Section as UnstyledSection,
SectionProps,
Text as UnstyledText,
TextProps,
} from "@react-email/components";
import clsx from "clsx";
export const Text = (props: TextProps) => {
return (
<UnstyledText
{...props}
className={clsx(
"my-4 font-sans text-base text-slate-800",
props.className,
)}
/>
);
};
export const Button = (props: ButtonProps) => {
return (
<UnstyledButton
{...props}
className={clsx(
"bg-primary-500 rounded px-3 py-2 font-sans text-white",
props.className,
)}
/>
);
};
export const Link = (props: LinkProps) => {
return (
<UnstyledLink
{...props}
className={clsx("text-primary-500 font-sans text-base", props.className)}
/>
);
};
export const Heading = (
props: React.ComponentProps<typeof UnstyledHeading>,
) => {
return (
<UnstyledHeading
{...props}
as={props.as || "h3"}
className={clsx("my-4 font-sans text-slate-800", props.className)}
/>
);
};
export const Section = (props: SectionProps) => {
return (
<UnstyledSection {...props} className={clsx("my-4", props.className)} />
);
};

View file

@ -0,0 +1,41 @@
import { Button, Container } from "@react-email/components";
import { EmailLayout } from "./components/email-layout";
import { Section, Text } from "./components/styled-components";
type GuestVerifyEmailProps = {
title: string;
name: string;
verificationLink: string;
adminLink: string;
};
export const GuestVerifyEmail = ({
title = "Untitled Poll",
name = "Guest",
verificationLink = "https://rallly.co",
}: GuestVerifyEmailProps) => {
return (
<EmailLayout preview="Click the button below to verify your email">
<Container>
<Text>Hi {name},</Text>
<Text>
To receive notifications for <strong>&quot;{title}&quot;</strong> you
will need to verify your email address.
</Text>
<Text>To verify your email please click the button below.</Text>
<Section>
<Button
className="bg-primary-500 rounded px-3 py-2 font-sans text-white"
href={verificationLink}
id="verifyEmailUrl"
>
Verify your email &rarr;
</Button>
</Section>
</Container>
</EmailLayout>
);
};
export default GuestVerifyEmail;

View file

@ -0,0 +1,37 @@
import { EmailLayout } from "./components/email-layout";
import { Button, Link, Section, Text } from "./components/styled-components";
export interface NewCommentEmailProps {
name: string;
title: string;
authorName: string;
pollUrl: string;
unsubscribeUrl: string;
}
export const NewCommentEmail = ({
name = "Guest",
title = "Untitled Poll",
authorName = "Someone",
pollUrl = "https://rallly.co",
unsubscribeUrl = "https://rallly.co",
}: NewCommentEmailProps) => {
return (
<EmailLayout preview={`${authorName} has commented on ${title}`}>
<Text>Hi {name},</Text>
<Text>
<strong>{authorName}</strong> has commented on <strong>{title}</strong>.
</Text>
<Section>
<Button href={pollUrl}>Go to poll &rarr;</Button>
</Section>
<Text>
<Link href={unsubscribeUrl}>
Stop receiving notifications for this poll.
</Link>
</Text>
</EmailLayout>
);
};
export default NewCommentEmail;

View file

@ -0,0 +1,40 @@
import { EmailLayout } from "./components/email-layout";
import { Button, Link, Section, Text } from "./components/styled-components";
export interface NewParticipantEmailProps {
name: string;
title: string;
participantName: string;
pollUrl: string;
unsubscribeUrl: string;
}
export const NewParticipantEmail = ({
name = "Guest",
title = "Untitled Poll",
participantName = "Someone",
pollUrl = "https://rallly.co",
unsubscribeUrl = "https://rallly.co",
}: NewParticipantEmailProps) => {
return (
<EmailLayout
preview={`${participantName} has shared their availability for ${title}`}
>
<Text>Hi {name},</Text>
<Text>
<strong>{participantName}</strong> has shared their availability for{" "}
<strong>{title}</strong>.
</Text>
<Section>
<Button href={pollUrl}>Go to poll &rarr;</Button>
</Section>
<Text>
<Link href={unsubscribeUrl}>
Stop receiving notifications for this poll.
</Link>
</Text>
</EmailLayout>
);
};
export default NewParticipantEmail;

View file

@ -0,0 +1,37 @@
import { EmailLayout } from "./components/email-layout";
import {
NewPollBaseEmail,
NewPollBaseEmailProps,
} from "./components/new-poll-base";
import { Button, Heading, Section, Text } from "./components/styled-components";
export interface NewPollVerificationEmailProps extends NewPollBaseEmailProps {
verificationLink: string;
}
export const NewPollVerificationEmail = ({
title = "Untitled Poll",
name = "Guest",
verificationLink = "https://rallly.co",
adminLink = "https://rallly.co/admin/abcdefg123",
}: NewPollVerificationEmailProps) => {
return (
<EmailLayout preview="Please verify your email address to turn on notifications">
<NewPollBaseEmail name={name} title={title} adminLink={adminLink}>
<Section className="mt-8 bg-gray-100 px-4 text-center">
<Heading as="h3">
Want to get notified when participants vote?
</Heading>
<Text>Verify your email address to turn on notifications.</Text>
<Section>
<Button id="verifyEmailUrl" href={verificationLink}>
Verify your email &rarr;
</Button>
</Section>
</Section>
</NewPollBaseEmail>
</EmailLayout>
);
};
export default NewPollVerificationEmail;

View file

@ -0,0 +1,19 @@
import { EmailLayout } from "./components/email-layout";
import {
NewPollBaseEmail,
NewPollBaseEmailProps,
} from "./components/new-poll-base";
export const NewPollEmail = ({
title = "Untitled Poll",
name = "Guest",
adminLink = "https://rallly.co/admin/abcdefg123",
}: NewPollBaseEmailProps) => {
return (
<EmailLayout preview="Please verify your email address to turn on notifications">
<NewPollBaseEmail name={name} title={title} adminLink={adminLink} />
</EmailLayout>
);
};
export default NewPollEmail;

View file

@ -0,0 +1,32 @@
import { Heading } from "@react-email/heading";
import { EmailLayout } from "./components/email-layout";
import { Text } from "./components/styled-components";
interface VerificationCodeEmailProps {
name: string;
code: string;
}
export const VerificationCodeEmail = ({
name = "Guest",
code = "123456",
}: VerificationCodeEmailProps) => {
return (
<EmailLayout preview="Here is your 6-digit code">
<Text>Hi {name},</Text>
<Text>Your 6-digit code is:</Text>
<Heading className="font-sans tracking-widest" id="code">
{code}
</Heading>
<Text>
<span className="text-slate-500">
This code is valid for 10 minutes
</span>
</Text>
<Text>Use this code to complete the verification process.</Text>
</EmailLayout>
);
};
export default VerificationCodeEmail;

View file

@ -0,0 +1,5 @@
{
"extends": "@rallly/tsconfig/react-library.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", ".react-email"]
}

1989
packages/emails/yarn.lock Normal file

File diff suppressed because it is too large Load diff