feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
43
packages/email/src/templates/_components/button.tsx
Normal file
43
packages/email/src/templates/_components/button.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Button as ReactEmailButton } from "@react-email/components";
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { ButtonProps as ReactEmailButtonProps } from "@react-email/components";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"block rounded-md text-center text-sm font-medium whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground",
|
||||
destructive: "bg-destructive text-destructive-foreground",
|
||||
outline: "border-input bg-background border",
|
||||
secondary: "bg-secondary text-secondary-foreground",
|
||||
link: "text-primary underline-offset-4",
|
||||
},
|
||||
size: {
|
||||
default: "px-4 py-2.5",
|
||||
sm: "rounded-md px-3",
|
||||
lg: "rounded-md px-8",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type ButtonProps = ReactEmailButtonProps &
|
||||
VariantProps<typeof buttonVariants>;
|
||||
|
||||
export const Button = ({ variant, size, className, ...props }: ButtonProps) => {
|
||||
return (
|
||||
<ReactEmailButton
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
29
packages/email/src/templates/_components/layout/footer.tsx
Normal file
29
packages/email/src/templates/_components/layout/footer.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Img, Link, Text } from "@react-email/components";
|
||||
|
||||
import { env } from "../../../env";
|
||||
|
||||
interface FooterProps {
|
||||
readonly origin: string;
|
||||
}
|
||||
|
||||
export const Footer = ({ origin }: FooterProps) => {
|
||||
return (
|
||||
<>
|
||||
<Img
|
||||
src={`${origin}/images/logo.png`}
|
||||
alt="Turbostarter Logo"
|
||||
height={45}
|
||||
className="mt-12"
|
||||
/>
|
||||
<Text className="text-muted-foreground max-w-[250px] leading-normal">
|
||||
<Link
|
||||
href="https://turbostarter.dev"
|
||||
className="text-muted-foreground"
|
||||
style={{ textDecoration: "underline" }}
|
||||
>
|
||||
{env.PRODUCT_NAME}
|
||||
</Link>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
16
packages/email/src/templates/_components/layout/header.tsx
Normal file
16
packages/email/src/templates/_components/layout/header.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Img } from "@react-email/components";
|
||||
|
||||
interface HeaderProps {
|
||||
readonly origin: string;
|
||||
}
|
||||
|
||||
export const Header = ({ origin }: HeaderProps) => {
|
||||
return (
|
||||
<Img
|
||||
src={`${origin}/images/logo-text.png`}
|
||||
alt="Turbostarter Logo"
|
||||
className="mb-10"
|
||||
height={45}
|
||||
/>
|
||||
);
|
||||
};
|
||||
91
packages/email/src/templates/_components/layout/layout.tsx
Normal file
91
packages/email/src/templates/_components/layout/layout.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
Container,
|
||||
Font,
|
||||
Head,
|
||||
Html,
|
||||
Section,
|
||||
Tailwind,
|
||||
} from "@react-email/components";
|
||||
|
||||
import { Footer } from "./footer";
|
||||
import { Header } from "./header";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export const Layout = ({
|
||||
children,
|
||||
origin,
|
||||
locale,
|
||||
}: PropsWithChildren<{ origin?: string | null; locale?: string }>) => {
|
||||
return (
|
||||
<Html lang={locale}>
|
||||
<Head>
|
||||
<Font
|
||||
fontFamily="Geist"
|
||||
fallbackFontFamily="Arial"
|
||||
fontWeight={400}
|
||||
fontStyle="normal"
|
||||
webFont={{
|
||||
url: "https://fonts.gstatic.com/s/geist/v3/gyByhwUxId8gMEwYGFWNOITddY4.woff2",
|
||||
format: "woff2",
|
||||
}}
|
||||
/>
|
||||
</Head>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "#ffffff",
|
||||
foreground: "#09090b",
|
||||
card: {
|
||||
DEFAULT: "#ffffff",
|
||||
foreground: "#09090b",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "#ffffff",
|
||||
foreground: "#09090b",
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "#f14704",
|
||||
foreground: "#fff7ed",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "#f4f4f5",
|
||||
foreground: "#18181b",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "#f4f4f5",
|
||||
foreground: "#71717b",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "#f4f4f5",
|
||||
foreground: "#18181b",
|
||||
},
|
||||
success: {
|
||||
DEFAULT: "#4ade80",
|
||||
foreground: "#09090b",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "#e7000b",
|
||||
foreground: "#fff7ed",
|
||||
},
|
||||
border: "#e4e4e7",
|
||||
input: "#e4e4e7",
|
||||
ring: "#f14704",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="p-1">
|
||||
<Container className="bg-card text-card-foreground rounded-lg p-6">
|
||||
{origin && <Header origin={origin} />}
|
||||
{children}
|
||||
{origin && <Footer origin={origin} />}
|
||||
</Container>
|
||||
</Section>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
67
packages/email/src/templates/auth/change-email.tsx
Normal file
67
packages/email/src/templates/auth/change-email.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Heading, Preview, Text } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
import { Trans } from "@turbostarter/i18n";
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
import { getOrigin } from "@turbostarter/shared/utils";
|
||||
|
||||
import { Button } from "../_components/button";
|
||||
import { Layout } from "../_components/layout/layout";
|
||||
|
||||
import type {
|
||||
EmailVariables,
|
||||
EmailTemplate,
|
||||
CommonEmailProps,
|
||||
} from "../../types";
|
||||
|
||||
type Props = EmailVariables[typeof EmailTemplate.CHANGE_EMAIL] &
|
||||
CommonEmailProps;
|
||||
|
||||
export const ChangeEmail = async ({ url, locale, newEmail }: Props) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
const origin = getOrigin(url);
|
||||
|
||||
return (
|
||||
<Layout origin={origin} locale={locale}>
|
||||
<Preview>{t("account.email.change.email.preview", { newEmail })}</Preview>
|
||||
<Heading className="leading-tight tracking-tight">
|
||||
{t("account.email.change.email.subject")}
|
||||
</Heading>
|
||||
|
||||
<Text>
|
||||
<Trans
|
||||
ns="auth"
|
||||
i18nKey="account.email.change.email.body"
|
||||
values={{ newEmail }}
|
||||
components={{
|
||||
bold: <strong />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Button href={url}>{t("account.email.change.email.cta")}</Button>
|
||||
|
||||
<Text>{t("account.email.change.email.or")}</Text>
|
||||
|
||||
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
|
||||
{url}
|
||||
</code>
|
||||
|
||||
<Text className="text-muted-foreground">
|
||||
{t("account.email.change.email.disclaimer")}
|
||||
</Text>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
ChangeEmail.subject = async ({ locale }: CommonEmailProps) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
return t("account.email.change.email.subject");
|
||||
};
|
||||
|
||||
ChangeEmail.PreviewProps = {
|
||||
url: "http://localhost:3000/api/auth/verify-email?token=123&callbackURL=/dashboard/settings",
|
||||
locale: "en",
|
||||
newEmail: "john@doe.com",
|
||||
};
|
||||
|
||||
export default ChangeEmail;
|
||||
57
packages/email/src/templates/auth/confirm-email.tsx
Normal file
57
packages/email/src/templates/auth/confirm-email.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Heading, Preview, Text } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
import { getOrigin } from "@turbostarter/shared/utils";
|
||||
|
||||
import { Button } from "../_components/button";
|
||||
import { Layout } from "../_components/layout/layout";
|
||||
|
||||
import type {
|
||||
EmailVariables,
|
||||
EmailTemplate,
|
||||
CommonEmailProps,
|
||||
} from "../../types";
|
||||
|
||||
type Props = EmailVariables[typeof EmailTemplate.CONFIRM_EMAIL] &
|
||||
CommonEmailProps;
|
||||
|
||||
export const ConfirmEmail = async ({ url, locale }: Props) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
const origin = getOrigin(url);
|
||||
|
||||
return (
|
||||
<Layout origin={origin} locale={locale}>
|
||||
<Preview>{t("account.email.confirm.email.preview")}</Preview>
|
||||
<Heading className="leading-tight tracking-tight">
|
||||
{t("account.email.confirm.email.subject")}
|
||||
</Heading>
|
||||
|
||||
<Text>{t("account.email.confirm.email.body")}</Text>
|
||||
|
||||
<Button href={url}>{t("account.email.confirm.email.cta")}</Button>
|
||||
|
||||
<Text>{t("account.email.confirm.email.or")}</Text>
|
||||
|
||||
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
|
||||
{url}
|
||||
</code>
|
||||
|
||||
<Text className="text-muted-foreground">
|
||||
{t("account.email.confirm.email.disclaimer")}
|
||||
</Text>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
ConfirmEmail.subject = async ({ locale }: CommonEmailProps) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
return t("account.email.confirm.email.subject");
|
||||
};
|
||||
|
||||
ConfirmEmail.PreviewProps = {
|
||||
url: "http://localhost:3000/api/auth/verify-email?token=123&callbackURL=/dashboard",
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
export default ConfirmEmail;
|
||||
57
packages/email/src/templates/auth/delete-account.tsx
Normal file
57
packages/email/src/templates/auth/delete-account.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Heading, Preview, Text } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
import { getOrigin } from "@turbostarter/shared/utils";
|
||||
|
||||
import { Button } from "../_components/button";
|
||||
import { Layout } from "../_components/layout/layout";
|
||||
|
||||
import type {
|
||||
EmailTemplate,
|
||||
EmailVariables,
|
||||
CommonEmailProps,
|
||||
} from "../../types";
|
||||
|
||||
type Props = EmailVariables[typeof EmailTemplate.DELETE_ACCOUNT] &
|
||||
CommonEmailProps;
|
||||
|
||||
export const DeleteAccount = async ({ url, locale }: Props) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
const origin = getOrigin(url);
|
||||
|
||||
return (
|
||||
<Layout origin={origin} locale={locale}>
|
||||
<Preview>{t("account.delete.email.preview")}</Preview>
|
||||
<Heading className="leading-tight tracking-tight">
|
||||
{t("account.delete.email.subject")}
|
||||
</Heading>
|
||||
|
||||
<Text>{t("account.delete.email.body")}</Text>
|
||||
|
||||
<Button href={url}>{t("account.delete.email.cta")}</Button>
|
||||
|
||||
<Text>{t("account.delete.email.or")}</Text>
|
||||
|
||||
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
|
||||
{url}
|
||||
</code>
|
||||
|
||||
<Text className="text-muted-foreground">
|
||||
{t("account.delete.email.disclaimer")}
|
||||
</Text>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
DeleteAccount.subject = async ({ locale }: CommonEmailProps) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
return t("account.delete.email.subject");
|
||||
};
|
||||
|
||||
DeleteAccount.PreviewProps = {
|
||||
url: "http://localhost:3000/api/auth/delete-user/callback?token=123&callbackURL=/",
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
export default DeleteAccount;
|
||||
56
packages/email/src/templates/auth/magic-link.tsx
Normal file
56
packages/email/src/templates/auth/magic-link.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Heading, Preview, Text } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
import { getOrigin } from "@turbostarter/shared/utils";
|
||||
|
||||
import { Button } from "../_components/button";
|
||||
import { Layout } from "../_components/layout/layout";
|
||||
|
||||
import type {
|
||||
EmailVariables,
|
||||
EmailTemplate,
|
||||
CommonEmailProps,
|
||||
} from "../../types";
|
||||
|
||||
type Props = EmailVariables[typeof EmailTemplate.MAGIC_LINK] & CommonEmailProps;
|
||||
|
||||
export const MagicLink = async ({ url, locale }: Props) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
const origin = getOrigin(url);
|
||||
|
||||
return (
|
||||
<Layout origin={origin} locale={locale}>
|
||||
<Preview>{t("login.magicLink.email.preview")}</Preview>
|
||||
<Heading className="leading-tight tracking-tight">
|
||||
{t("login.magicLink.email.subject")}
|
||||
</Heading>
|
||||
|
||||
<Text>{t("login.magicLink.email.body")}</Text>
|
||||
|
||||
<Button href={url}>{t("login.magicLink.email.cta")}</Button>
|
||||
|
||||
<Text>{t("login.magicLink.email.or")}</Text>
|
||||
|
||||
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
|
||||
{url}
|
||||
</code>
|
||||
|
||||
<Text className="text-muted-foreground">
|
||||
{t("login.magicLink.email.disclaimer")}
|
||||
</Text>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
MagicLink.subject = async ({ locale }: CommonEmailProps) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
return t("login.magicLink.email.subject");
|
||||
};
|
||||
|
||||
MagicLink.PreviewProps = {
|
||||
url: "http://localhost:3000/api/auth/magic-link/verify?token=123&callbackURL=/dashboard",
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
export default MagicLink;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Heading, Preview, Text } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
import { Trans } from "@turbostarter/i18n";
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
import { getOrigin } from "@turbostarter/shared/utils";
|
||||
|
||||
import { Button } from "../_components/button";
|
||||
import { Layout } from "../_components/layout/layout";
|
||||
|
||||
import type {
|
||||
EmailVariables,
|
||||
EmailTemplate,
|
||||
CommonEmailProps,
|
||||
} from "../../types";
|
||||
|
||||
type Props = EmailVariables[typeof EmailTemplate.ORGANIZATION_INVITATION] &
|
||||
CommonEmailProps;
|
||||
|
||||
export const OrganizationInvitation = async ({
|
||||
url,
|
||||
inviter,
|
||||
organization,
|
||||
locale,
|
||||
}: Props) => {
|
||||
const { t } = await getTranslation({ locale, ns: "organization" });
|
||||
const origin = getOrigin(url);
|
||||
|
||||
return (
|
||||
<Layout origin={origin} locale={locale}>
|
||||
<Preview>{t("members.invite.email.preview", { inviter })}</Preview>
|
||||
<Heading className="leading-tight tracking-tight">
|
||||
{t("members.invite.email.subject")}
|
||||
</Heading>
|
||||
|
||||
<Text>
|
||||
<Trans
|
||||
i18nKey="members.invite.email.body"
|
||||
ns="organization"
|
||||
values={{ inviter, organization }}
|
||||
components={{
|
||||
bold: <strong />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Button href={url}>
|
||||
{t("members.invite.email.cta", { organization })}
|
||||
</Button>
|
||||
|
||||
<Text>{t("members.invite.email.or")}</Text>
|
||||
|
||||
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
|
||||
{url}
|
||||
</code>
|
||||
|
||||
<Text className="text-muted-foreground">
|
||||
{t("members.invite.email.disclaimer")}
|
||||
</Text>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
OrganizationInvitation.subject = async ({ locale }: CommonEmailProps) => {
|
||||
const { t } = await getTranslation({ locale, ns: "organization" });
|
||||
return t("members.invite.email.subject");
|
||||
};
|
||||
|
||||
OrganizationInvitation.PreviewProps = {
|
||||
url: "http://localhost:3000/auth/join?invitationId=h4zI2pKJrQkP5NQljdA8W57wG0V8LHrv",
|
||||
locale: "en",
|
||||
inviter: "John Doe",
|
||||
organization: "Acme Inc",
|
||||
};
|
||||
|
||||
export default OrganizationInvitation;
|
||||
57
packages/email/src/templates/auth/reset-password.tsx
Normal file
57
packages/email/src/templates/auth/reset-password.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Heading, Preview, Text } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
import { getOrigin } from "@turbostarter/shared/utils";
|
||||
|
||||
import { Button } from "../_components/button";
|
||||
import { Layout } from "../_components/layout/layout";
|
||||
|
||||
import type {
|
||||
EmailVariables,
|
||||
EmailTemplate,
|
||||
CommonEmailProps,
|
||||
} from "../../types";
|
||||
|
||||
type Props = EmailVariables[typeof EmailTemplate.CONFIRM_EMAIL] &
|
||||
CommonEmailProps;
|
||||
|
||||
export const ResetPassword = async ({ url, locale }: Props) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
const origin = getOrigin(url);
|
||||
|
||||
return (
|
||||
<Layout origin={origin}>
|
||||
<Preview>{t("account.password.update.email.preview")}</Preview>
|
||||
<Heading className="leading-tight tracking-tight">
|
||||
{t("account.password.update.email.subject")}
|
||||
</Heading>
|
||||
|
||||
<Text>{t("account.password.update.email.body")}</Text>
|
||||
|
||||
<Button href={url}>{t("account.password.update.email.cta")}</Button>
|
||||
|
||||
<Text>{t("account.password.update.email.or")}</Text>
|
||||
|
||||
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
|
||||
{url}
|
||||
</code>
|
||||
|
||||
<Text className="text-muted-foreground">
|
||||
{t("account.password.update.email.disclaimer")}
|
||||
</Text>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
ResetPassword.subject = async ({ locale }: CommonEmailProps) => {
|
||||
const { t } = await getTranslation({ locale, ns: "auth" });
|
||||
return t("account.password.update.email.subject");
|
||||
};
|
||||
|
||||
ResetPassword.PreviewProps = {
|
||||
url: "http://localhost:3000/api/auth/reset-password/KwiyWf9xsTrfndZY5a0stg4p?callbackURL=/auth/password/update",
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
export default ResetPassword;
|
||||
45
packages/email/src/templates/contact-form.tsx
Normal file
45
packages/email/src/templates/contact-form.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Heading, Preview, Row, Column } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
|
||||
import { Layout } from "./_components/layout/layout";
|
||||
|
||||
import type { EmailTemplate } from "../types";
|
||||
import type { EmailVariables } from "../types";
|
||||
|
||||
type Props = EmailVariables[typeof EmailTemplate.CONTACT_FORM];
|
||||
|
||||
export const ContactForm = async (props: Props) => {
|
||||
const { t } = await getTranslation({ ns: "marketing" });
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Preview>{t("contact.email.subject")}</Preview>
|
||||
<Heading className="leading-tight tracking-tight">
|
||||
{t("contact.email.body")}
|
||||
</Heading>
|
||||
|
||||
{Object.entries(props).map(([key, value]) => (
|
||||
<Row key={key}>
|
||||
<Column>
|
||||
<strong>{key}</strong>: {value}
|
||||
</Column>
|
||||
</Row>
|
||||
))}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
ContactForm.subject = async () => {
|
||||
const { t } = await getTranslation({ ns: "marketing" });
|
||||
return t("contact.email.subject");
|
||||
};
|
||||
|
||||
ContactForm.PreviewProps = {
|
||||
name: "John Doe",
|
||||
email: "john.doe@example.com",
|
||||
message: "Hello, I'm interested in your services.",
|
||||
};
|
||||
|
||||
export default ContactForm;
|
||||
54
packages/email/src/templates/index.ts
Normal file
54
packages/email/src/templates/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
import { EmailTemplate } from "../types";
|
||||
|
||||
import ChangeEmail from "./auth/change-email";
|
||||
import { ConfirmEmail } from "./auth/confirm-email";
|
||||
import DeleteAccount from "./auth/delete-account";
|
||||
import { MagicLink } from "./auth/magic-link";
|
||||
import { OrganizationInvitation } from "./auth/organization-invitation";
|
||||
import { ResetPassword } from "./auth/reset-password";
|
||||
import ContactForm from "./contact-form";
|
||||
|
||||
import type { CommonEmailProps, EmailVariables } from "../types";
|
||||
|
||||
interface EmailTemplateComponent<T extends EmailTemplate> {
|
||||
(
|
||||
props: EmailVariables[T] & CommonEmailProps,
|
||||
): Promise<React.ReactElement> | React.ReactElement;
|
||||
subject: ((props: CommonEmailProps) => Promise<string> | string) | string;
|
||||
}
|
||||
|
||||
export const templates: {
|
||||
[K in EmailTemplate]: EmailTemplateComponent<K>;
|
||||
} = {
|
||||
[EmailTemplate.RESET_PASSWORD]: ResetPassword,
|
||||
[EmailTemplate.MAGIC_LINK]: MagicLink,
|
||||
[EmailTemplate.CONFIRM_EMAIL]: ConfirmEmail,
|
||||
[EmailTemplate.DELETE_ACCOUNT]: DeleteAccount,
|
||||
[EmailTemplate.CHANGE_EMAIL]: ChangeEmail,
|
||||
[EmailTemplate.ORGANIZATION_INVITATION]: OrganizationInvitation,
|
||||
[EmailTemplate.CONTACT_FORM]: ContactForm,
|
||||
} as const;
|
||||
|
||||
export const getTemplate = async <T extends EmailTemplate>({
|
||||
id,
|
||||
locale,
|
||||
variables,
|
||||
}: {
|
||||
id: T;
|
||||
variables: EmailVariables[T];
|
||||
locale?: string;
|
||||
}) => {
|
||||
const template = templates[id];
|
||||
const subject =
|
||||
typeof template.subject === "function"
|
||||
? await template.subject({ locale })
|
||||
: template.subject;
|
||||
const email = await template({ ...variables, locale });
|
||||
|
||||
const html = await render(email);
|
||||
const text = await render(email, { plainText: true });
|
||||
|
||||
return { html, text, subject };
|
||||
};
|
||||
Reference in New Issue
Block a user