mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-04 00:48:52 +02:00
♻️ Refactor email templating code (#533)
This commit is contained in:
parent
0a836aeec7
commit
309cb109aa
79 changed files with 3926 additions and 1455 deletions
2
packages/emails/.gitignore
vendored
Normal file
2
packages/emails/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.react-email
|
||||
/out
|
24
packages/emails/package.json
Normal file
24
packages/emails/package.json
Normal 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
27
packages/emails/readme.md
Normal 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
|
1
packages/emails/src/index.tsx
Normal file
1
packages/emails/src/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./send-email";
|
64
packages/emails/src/send-email.tsx
Normal file
64
packages/emails/src/send-email.tsx
Normal 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?.();
|
||||
}
|
||||
};
|
6
packages/emails/src/templates.ts
Normal file
6
packages/emails/src/templates.ts
Normal 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";
|
162
packages/emails/src/templates/components/email-layout.tsx
Normal file
162
packages/emails/src/templates/components/email-layout.tsx
Normal 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>
|
||||
•
|
||||
<Link
|
||||
className="font-sans text-slate-500"
|
||||
href="https://twitter.com/ralllyco"
|
||||
>
|
||||
Twitter
|
||||
</Link>
|
||||
•
|
||||
<Link
|
||||
className="font-sans text-slate-500"
|
||||
href="https://github.com/lukevella/rallly"
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
•
|
||||
<Link
|
||||
className="font-sans text-slate-500"
|
||||
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
|
||||
>
|
||||
Donate
|
||||
</Link>
|
||||
•
|
||||
<Link
|
||||
className="font-sans text-slate-500"
|
||||
href={`mailto:${process.env.SUPPORT_EMAIL}`}
|
||||
>
|
||||
Contact
|
||||
</Link>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
34
packages/emails/src/templates/components/new-poll-base.tsx
Normal file
34
packages/emails/src/templates/components/new-poll-base.tsx
Normal 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>"{title}"</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> →
|
||||
</Link>
|
||||
</Text>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
};
|
|
@ -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)} />
|
||||
);
|
||||
};
|
41
packages/emails/src/templates/guest-verify-email.tsx
Normal file
41
packages/emails/src/templates/guest-verify-email.tsx
Normal 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>"{title}"</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 →
|
||||
</Button>
|
||||
</Section>
|
||||
</Container>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuestVerifyEmail;
|
37
packages/emails/src/templates/new-comment.tsx
Normal file
37
packages/emails/src/templates/new-comment.tsx
Normal 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 →</Button>
|
||||
</Section>
|
||||
<Text>
|
||||
<Link href={unsubscribeUrl}>
|
||||
Stop receiving notifications for this poll.
|
||||
</Link>
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewCommentEmail;
|
40
packages/emails/src/templates/new-participant.tsx
Normal file
40
packages/emails/src/templates/new-participant.tsx
Normal 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 →</Button>
|
||||
</Section>
|
||||
<Text>
|
||||
<Link href={unsubscribeUrl}>
|
||||
Stop receiving notifications for this poll.
|
||||
</Link>
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewParticipantEmail;
|
37
packages/emails/src/templates/new-poll-verification.tsx
Normal file
37
packages/emails/src/templates/new-poll-verification.tsx
Normal 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 →
|
||||
</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
</NewPollBaseEmail>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewPollVerificationEmail;
|
19
packages/emails/src/templates/new-poll.tsx
Normal file
19
packages/emails/src/templates/new-poll.tsx
Normal 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;
|
32
packages/emails/src/templates/verification-code.tsx
Normal file
32
packages/emails/src/templates/verification-code.tsx
Normal 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;
|
5
packages/emails/tsconfig.json
Normal file
5
packages/emails/tsconfig.json
Normal 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
1989
packages/emails/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue