✨ Add react-email for email templates (#2421)

This commit is contained in:
Alejandra
2026-08-10 19:59:35 +02:00
committed by GitHub
parent de53c8fd35
commit b0ccf1e80e
26 changed files with 802 additions and 166 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["../../frontend/biome.json"]
}
@@ -0,0 +1,51 @@
import { Text } from "@react-email/components"
import { LinkButton } from "../ui/Button"
import { Callout, Detail } from "../ui/Callout"
import { Heading } from "../ui/Heading"
import { Layout } from "../ui/Layout"
import { Link } from "../ui/Link"
type NewAccountProps = {
project_name: string
username: string
password: string
link: string
}
export default function NewAccount({
project_name = "{{ project_name }}",
username = "{{ username }}",
password = "{{ password }}",
link = "{{ link }}",
}: NewAccountProps) {
return (
<Layout
title={`${project_name} - New account`}
preview={`Your ${project_name} account is ready`}
project_name={project_name}
>
<Heading>Welcome to {project_name}!</Heading>
<Text className="text-[15px] leading-7 text-body">Hi,</Text>
<Text className="text-[15px] leading-7 text-body">
Your account has been successfully created and is ready to use. These
are your credentials:
</Text>
<Callout>
<Detail label="Username" value={username} />
<Detail label="Password" value={password} />
</Callout>
<Text className="text-[15px] leading-7 text-body">
Get started by signing in to your dashboard:
</Text>
<LinkButton href={link}>Go to Dashboard</LinkButton>
<Text className="text-sm leading-6 text-muted">
Or copy and paste this link into your browser:
<br />
<Link href={link}>{link}</Link>
</Text>
<Text className="text-sm leading-6 text-muted">
For security reasons, change your password after your first sign in.
</Text>
</Layout>
)
}
@@ -0,0 +1,47 @@
import { Text } from "@react-email/components"
import { LinkButton } from "../ui/Button"
import { Heading } from "../ui/Heading"
import { Layout } from "../ui/Layout"
import { Link } from "../ui/Link"
type ResetPasswordProps = {
project_name: string
username: string
link: string
valid_hours: string
}
export default function ResetPassword({
project_name = "{{ project_name }}",
username = "{{ username }}",
link = "{{ link }}",
valid_hours = "{{ valid_hours }}",
}: ResetPasswordProps) {
return (
<Layout
title={`${project_name} - Password recovery`}
preview={`Reset your ${project_name} password`}
project_name={project_name}
>
<Heading>Reset your password</Heading>
<Text className="text-[15px] leading-7 text-body">Hi {username},</Text>
<Text className="text-[15px] leading-7 text-body">
We've received a request to reset the password for your {project_name}{" "}
account. Choose a new one by clicking the button below:
</Text>
<LinkButton href={link}>Reset password</LinkButton>
<Text className="text-sm leading-6 text-muted">
Or copy and paste this link into your browser:
<br />
<Link href={link}>{link}</Link>
</Text>
<Text className="text-sm leading-6 text-muted">
This link will expire in {valid_hours} hours.
</Text>
<Text className="text-sm leading-6 text-muted">
If you didn't request a password recovery, you can safely ignore this
email.
</Text>
</Layout>
)
}
@@ -0,0 +1,35 @@
import { Text } from "@react-email/components"
import { Callout, Detail } from "../ui/Callout"
import { Heading } from "../ui/Heading"
import { Layout } from "../ui/Layout"
type TestEmailProps = {
project_name: string
email: string
}
export default function TestEmail({
project_name = "{{ project_name }}",
email = "{{ email }}",
}: TestEmailProps) {
return (
<Layout
title={`${project_name} - Test email`}
preview={`${project_name} email delivery is working`}
project_name={project_name}
>
<Heading>Test email</Heading>
<Text className="text-[15px] leading-7 text-body">Hi,</Text>
<Text className="text-[15px] leading-7 text-body">
This is a test email from {project_name}. If you're reading it, email
delivery is configured correctly.
</Text>
<Callout>
<Detail label="Sent to" value={email} />
</Callout>
<Text className="text-sm leading-6 text-muted">
If you weren't expecting this email, you can safely ignore it.
</Text>
</Layout>
)
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "emails",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "email build",
"dev": "email dev --port 3333",
"lint": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
"export": "email export --outDir ../../backend/app/email-templates"
},
"dependencies": {
"@react-email/components": "1.0.12",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-email": "6.9.1"
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
"@react-email/ui": "6.9.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"composite": false,
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"strictNullChecks": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", ".react-email"]
}
+12
View File
@@ -0,0 +1,12 @@
import { Button } from "@react-email/components"
type ButtonProps = React.PropsWithChildren & { href: string }
export const LinkButton = ({ children, href }: ButtonProps) => (
<Button
href={href}
className="bg-brand text-white rounded-md font-semibold text-sm mt-2 mb-7 inline-block text-center no-underline px-7 py-3.5"
>
{children}
</Button>
)
+22
View File
@@ -0,0 +1,22 @@
import { Section, Text } from "@react-email/components"
type CalloutProps = React.PropsWithChildren
export const Callout = ({ children }: CalloutProps) => (
<Section className="border-0 border-l-2 border-solid border-brand pl-5 my-7">
{children}
</Section>
)
type DetailProps = { label: string; value: string }
export const Detail = ({ label, value }: DetailProps) => (
<Text className="m-0 my-3">
<span className="block text-[11px] uppercase tracking-[0.08em] text-muted">
{label}
</span>
<span className="block text-sm font-semibold break-all font-mono mt-1">
{value}
</span>
</Text>
)
+12
View File
@@ -0,0 +1,12 @@
import { Heading as BaseHeading } from "@react-email/components"
type HeadingProps = React.PropsWithChildren
export const Heading = ({ children }: HeadingProps) => (
<BaseHeading
as="h1"
className="text-2xl leading-8 font-semibold tracking-[-0.02em] text-body m-0 mb-5"
>
{children}
</BaseHeading>
)
+85
View File
@@ -0,0 +1,85 @@
import {
Body,
Head,
Hr,
Html,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components"
type LayoutProps = React.PropsWithChildren & {
title: string
project_name: string
preview?: string
}
export const Layout = ({
project_name,
title,
preview,
children,
}: LayoutProps) => {
return (
<Tailwind
config={{
theme: {
extend: {
colors: {
surface: "#ffffff",
body: "#1e293b",
muted: "#64748b",
rule: "#e2e8f0",
brand: {
DEFAULT: "#009688",
dark: "#00695c",
},
},
fontFamily: {
sans: [
"-apple-system",
"BlinkMacSystemFont",
'"Segoe UI"',
"Roboto",
"Oxygen",
"Ubuntu",
"Cantarell",
'"Fira Sans"',
'"Droid Sans"',
'"Helvetica Neue"',
"sans-serif",
],
},
},
},
}}
>
<Html lang="en" dir="ltr">
<Head>
<title>{title}</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
</Head>
{preview ? <Preview>{preview}</Preview> : null}
<Body className="bg-surface font-sans text-body max-w-[560px] w-full mx-auto my-0 py-12 px-6">
<Section>
<Hr className="border-0 border-t-2 border-solid border-brand w-8 m-0" />
<Text className="text-body text-[13px] font-semibold uppercase tracking-[0.1em] mt-4 mb-9">
{project_name}
</Text>
</Section>
<Section>{children}</Section>
<Hr className="border-0 border-t border-solid border-rule mt-10 mb-5" />
<Section>
<Text className="text-xs leading-5 text-muted m-0">
© {new Date().getFullYear()} {project_name}. All rights reserved.
</Text>
</Section>
</Body>
</Html>
</Tailwind>
)
}
+7
View File
@@ -0,0 +1,7 @@
type LinkProps = React.PropsWithChildren & { href: string }
export const Link = ({ children, href }: LinkProps) => (
<a href={href} className="text-brand-dark underline break-all">
{children}
</a>
)