B2B-88: add starter kit structure and elements
This commit is contained in:
14
packages/email-templates/src/components/body-style.tsx
Normal file
14
packages/email-templates/src/components/body-style.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export function BodyStyle() {
|
||||
return (
|
||||
<style>
|
||||
{`
|
||||
body {
|
||||
background-color: #fafafa;
|
||||
margin: auto;
|
||||
font-family: sans-serif;
|
||||
color: #242424;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
);
|
||||
}
|
||||
19
packages/email-templates/src/components/content.tsx
Normal file
19
packages/email-templates/src/components/content.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Container } from '@react-email/components';
|
||||
|
||||
export function EmailContent({
|
||||
children,
|
||||
className,
|
||||
}: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
displayLogo?: boolean;
|
||||
}>) {
|
||||
return (
|
||||
<Container
|
||||
className={
|
||||
'mx-auto rounded-xl bg-white px-[48px] py-[36px] ' + className || ''
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
16
packages/email-templates/src/components/cta-button.tsx
Normal file
16
packages/email-templates/src/components/cta-button.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Button } from '@react-email/components';
|
||||
|
||||
export function CtaButton(
|
||||
props: React.PropsWithChildren<{
|
||||
href: string;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<Button
|
||||
className="w-full rounded bg-[#000000] py-3 text-center text-[16px] font-semibold text-white no-underline"
|
||||
href={props.href}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
11
packages/email-templates/src/components/footer.tsx
Normal file
11
packages/email-templates/src/components/footer.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Container, Text } from '@react-email/components';
|
||||
|
||||
export function EmailFooter(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<Container>
|
||||
<Text className="px-4 text-[12px] leading-[20px] text-gray-300">
|
||||
{props.children}
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
9
packages/email-templates/src/components/header.tsx
Normal file
9
packages/email-templates/src/components/header.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Container, Section } from '@react-email/components';
|
||||
|
||||
export function EmailHeader(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<Container>
|
||||
<Section>{props.children}</Section>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
9
packages/email-templates/src/components/heading.tsx
Normal file
9
packages/email-templates/src/components/heading.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Heading } from '@react-email/components';
|
||||
|
||||
export function EmailHeading(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<Heading className="mx-0 p-0 font-sans text-[22px] font-semibold text-[#242424]">
|
||||
{props.children}
|
||||
</Heading>
|
||||
);
|
||||
}
|
||||
30
packages/email-templates/src/components/wrapper.tsx
Normal file
30
packages/email-templates/src/components/wrapper.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Container } from '@react-email/components';
|
||||
|
||||
export function EmailWrapper(
|
||||
props: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<Container
|
||||
style={{
|
||||
backgroundColor: '#fafafa',
|
||||
margin: 'auto',
|
||||
fontFamily: 'sans-serif',
|
||||
color: '#242424',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
maxWidth: '720px',
|
||||
backgroundColor: '#fafafa',
|
||||
margin: 'auto',
|
||||
}}
|
||||
className={'mx-auto px-[20px] py-[40px] ' + props.className || ''}
|
||||
>
|
||||
{props.children}
|
||||
</Container>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
97
packages/email-templates/src/emails/account-delete.email.tsx
Normal file
97
packages/email-templates/src/emails/account-delete.email.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import { EmailContent } from '../components/content';
|
||||
import { EmailFooter } from '../components/footer';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
interface Props {
|
||||
productName: string;
|
||||
userDisplayName: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export async function renderAccountDeleteEmail(props: Props) {
|
||||
const namespace = 'account-delete-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language: props.language,
|
||||
namespace,
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`, {
|
||||
productName: props.productName,
|
||||
});
|
||||
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
productName: props.productName,
|
||||
});
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:hello`, {
|
||||
displayName: props.userDisplayName,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:paragraph1`, {
|
||||
productName: props.productName,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:paragraph2`)}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:paragraph3`, {
|
||||
productName: props.productName,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:paragraph4`, {
|
||||
productName: props.productName,
|
||||
})}
|
||||
</Text>
|
||||
</EmailContent>
|
||||
|
||||
<EmailFooter>{props.productName}</EmailFooter>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
137
packages/email-templates/src/emails/invite.email.tsx
Normal file
137
packages/email-templates/src/emails/invite.email.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
Body,
|
||||
Column,
|
||||
Head,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import { EmailContent } from '../components/content';
|
||||
import { CtaButton } from '../components/cta-button';
|
||||
import { EmailFooter } from '../components/footer';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
interface Props {
|
||||
teamName: string;
|
||||
teamLogo?: string;
|
||||
inviter: string | undefined;
|
||||
invitedUserEmail: string;
|
||||
link: string;
|
||||
productName: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export async function renderInviteEmail(props: Props) {
|
||||
const namespace = 'invite-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language: props.language,
|
||||
namespace,
|
||||
});
|
||||
|
||||
const previewText = `Join ${props.invitedUserEmail} on ${props.productName}`;
|
||||
const subject = t(`${namespace}:subject`);
|
||||
|
||||
const heading = t(`${namespace}:heading`, {
|
||||
teamName: props.teamName,
|
||||
productName: props.productName,
|
||||
});
|
||||
|
||||
const hello = t(`${namespace}:hello`, {
|
||||
invitedUserEmail: props.invitedUserEmail,
|
||||
});
|
||||
|
||||
const mainText = t(`${namespace}:mainText`, {
|
||||
inviter: props.inviter,
|
||||
teamName: props.teamName,
|
||||
productName: props.productName,
|
||||
});
|
||||
|
||||
const joinTeam = t(`${namespace}:joinTeam`, {
|
||||
teamName: props.teamName,
|
||||
});
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{hello}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className="text-[16px] leading-[24px] text-[#242424]"
|
||||
dangerouslySetInnerHTML={{ __html: mainText }}
|
||||
/>
|
||||
|
||||
{props.teamLogo && (
|
||||
<Section>
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Img
|
||||
className="rounded-full"
|
||||
src={props.teamLogo}
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section className="mb-[32px] mt-[32px] text-center">
|
||||
<CtaButton href={props.link}>{joinTeam}</CtaButton>
|
||||
</Section>
|
||||
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:copyPasteLink`)}{' '}
|
||||
<Link href={props.link} className="text-blue-600 no-underline">
|
||||
{props.link}
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
|
||||
|
||||
<Text className="text-[12px] leading-[24px] text-[#666666]">
|
||||
{t(`${namespace}:invitationIntendedFor`, {
|
||||
invitedUserEmail: props.invitedUserEmail,
|
||||
})}
|
||||
</Text>
|
||||
</EmailContent>
|
||||
|
||||
<EmailFooter>{props.productName}</EmailFooter>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
97
packages/email-templates/src/emails/otp.email.tsx
Normal file
97
packages/email-templates/src/emails/otp.email.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import { EmailContent } from '../components/content';
|
||||
import { EmailFooter } from '../components/footer';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
|
||||
interface Props {
|
||||
otp: string;
|
||||
productName: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export async function renderOtpEmail(props: Props) {
|
||||
const namespace = 'otp-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language: props.language,
|
||||
namespace,
|
||||
});
|
||||
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
productName: props.productName,
|
||||
});
|
||||
|
||||
const previewText = subject;
|
||||
|
||||
const heading = t(`${namespace}:heading`, {
|
||||
productName: props.productName,
|
||||
});
|
||||
|
||||
const otpText = t(`${namespace}:otpText`, {
|
||||
otp: props.otp,
|
||||
});
|
||||
|
||||
const mainText = t(`${namespace}:mainText`);
|
||||
const footerText = t(`${namespace}:footerText`);
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<Text className="text-[16px] text-[#242424]">{mainText}</Text>
|
||||
|
||||
<Text className="text-[16px] text-[#242424]">{otpText}</Text>
|
||||
|
||||
<Section className="mb-[16px] mt-[16px] text-center">
|
||||
<Button className={'w-full rounded bg-neutral-950 text-center'}>
|
||||
<Text className="text-[16px] font-medium font-semibold leading-[16px] text-white">
|
||||
{props.otp}
|
||||
</Text>
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Text
|
||||
className="text-[16px] text-[#242424]"
|
||||
dangerouslySetInnerHTML={{ __html: footerText }}
|
||||
/>
|
||||
</EmailContent>
|
||||
|
||||
<EmailFooter>{props.productName}</EmailFooter>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
3
packages/email-templates/src/index.ts
Normal file
3
packages/email-templates/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './emails/invite.email';
|
||||
export * from './emails/account-delete.email';
|
||||
export * from './emails/otp.email';
|
||||
30
packages/email-templates/src/lib/i18n.ts
Normal file
30
packages/email-templates/src/lib/i18n.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { initializeServerI18n } from '@kit/i18n/server';
|
||||
|
||||
export function initializeEmailI18n(params: {
|
||||
language: string | undefined;
|
||||
namespace: string;
|
||||
}) {
|
||||
const language = params.language ?? 'en';
|
||||
|
||||
return initializeServerI18n(
|
||||
{
|
||||
supportedLngs: [language],
|
||||
lng: language,
|
||||
ns: params.namespace,
|
||||
},
|
||||
async (language, namespace) => {
|
||||
try {
|
||||
const data = await import(`../locales/${language}/${namespace}.json`);
|
||||
|
||||
return data as Record<string, string>;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Error loading i18n file: locales/${language}/${namespace}.json`,
|
||||
error,
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"subject": "We have deleted your {{productName}} account",
|
||||
"previewText": "We have deleted your {{productName}} account",
|
||||
"hello": "Hello {{displayName}},",
|
||||
"paragraph1": "This is to confirm that we have processed your request to delete your account with {{productName}}.",
|
||||
"paragraph2": "We're sorry to see you go. Please note that this action is irreversible, and we'll make sure to delete all of your data from our systems.",
|
||||
"paragraph3": "We thank you again for using {{productName}}.",
|
||||
"paragraph4": "The {{productName}} Team"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"subject": "You have been invited to join a team",
|
||||
"heading": "Join {{teamName}} on {{productName}}",
|
||||
"hello": "Hello {{invitedUserEmail}},",
|
||||
"mainText": "<strong>{{inviter}}</strong> has invited you to the <strong>{{teamName}}</strong> team on <strong>{{productName}}</strong>.",
|
||||
"joinTeam": "Join {{teamName}}",
|
||||
"copyPasteLink": "or copy and paste this URL into your browser:",
|
||||
"invitationIntendedFor": "This invitation is intended for {{invitedUserEmail}}."
|
||||
}
|
||||
7
packages/email-templates/src/locales/en/otp-email.json
Normal file
7
packages/email-templates/src/locales/en/otp-email.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"subject": "One-time password for {{productName}}",
|
||||
"heading": "One-time password for {{productName}}",
|
||||
"otpText": "Your one-time password is: {{otp}}",
|
||||
"footerText": "Please enter the one-time password in the app to continue.",
|
||||
"mainText": "You're receiving this email because you need to verify your identity using a one-time password."
|
||||
}
|
||||
Reference in New Issue
Block a user