B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

View File

@@ -0,0 +1,5 @@
# Emails - @kit/email-templates
This package is responsible for managing email templates using the react.email library.
Here you can define email templates using React components and export them as a function that returns the email content.

View File

@@ -0,0 +1,3 @@
import eslintConfigBase from '@kit/eslint-config/base.js';
export default eslintConfigBase;

View File

@@ -0,0 +1 @@
../../../../tooling/eslint

1
packages/email-templates/node_modules/@kit/i18n generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../i18n

View File

@@ -0,0 +1 @@
../../../../tooling/prettier

1
packages/email-templates/node_modules/@kit/tsconfig generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../tooling/typescript

View File

@@ -0,0 +1 @@
../../../../node_modules/.pnpm/@react-email+components@0.0.41_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/@react-email/components

View File

@@ -0,0 +1,31 @@
{
"name": "@kit/email-templates",
"private": true,
"version": "0.1.0",
"scripts": {
"clean": "git clean -xdf .turbo node_modules",
"format": "prettier --check \"**/*.{ts,tsx}\"",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"prettier": "@kit/prettier-config",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@react-email/components": "0.0.41"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
"@kit/i18n": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
}
}

View File

@@ -0,0 +1,14 @@
export function BodyStyle() {
return (
<style>
{`
body {
background-color: #fafafa;
margin: auto;
font-family: sans-serif;
color: #242424;
}
`}
</style>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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,
};
}

View 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,
};
}

View 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,
};
}

View File

@@ -0,0 +1,3 @@
export * from './emails/invite.email';
export * from './emails/account-delete.email';
export * from './emails/otp.email';

View 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 {};
}
},
);
}

View File

@@ -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"
}

View File

@@ -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}}."
}

View 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."
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@kit/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}