Translations for Email Notifications (#1278)

Co-authored-by: Niko Heller <hellerniko@gmail.com>
This commit is contained in:
Luke Vella 2024-09-02 19:30:58 +01:00 committed by GitHub
parent aa52a0f26f
commit f4218c3115
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1071 additions and 970 deletions

View file

@ -0,0 +1,16 @@
import { i18nDefaultConfig, i18nInstance } from "../i18n";
import { EmailContext } from "../types";
i18nInstance.init({
...i18nDefaultConfig,
initImmediate: true,
});
export const previewEmailContext: EmailContext = {
logoUrl: "https://rallly-public.s3.amazonaws.com/images/rallly-logo-mark.png",
baseUrl: "https://rallly.co",
domain: "rallly.co",
supportEmail: "support@rallly.co",
i18n: i18nInstance,
t: i18nInstance.getFixedT("en"),
};

View file

@ -7,8 +7,9 @@ import {
Preview,
Section,
} from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailContext } from "./email-context";
import { EmailContext } from "../types";
import { darkTextColor, fontFamily, Link, Text } from "./styled-components";
export interface EmailLayoutProps {
@ -47,12 +48,21 @@ export const EmailLayout = ({
alt="Rallly Logo"
/>
{children}
<Section style={{ marginTop: 32, textAlign: "center" }}>
<Section style={{ marginTop: 32 }}>
<Text light={true}>
Powered by{" "}
<Link href="https://rallly.co?utm_source=email&utm_medium=transactional">
rallly.co
</Link>
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="common_poweredBy"
ns="emails"
defaults="Powered by <a>{{domain}}</a>"
values={{ domain: "rallly.co" }}
components={{
a: (
<Link href="https://rallly.co?utm_source=email&utm_medium=transactional" />
),
}}
/>
</Text>
</Section>
</Container>

View file

@ -1,11 +1,11 @@
import { Section } from "@react-email/section";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailContext } from "./email-context";
import type { EmailContext } from "../types";
import { EmailLayout } from "./email-layout";
import { Button, Link, Text } from "./styled-components";
export interface NotificationBaseProps {
name: string;
title: string;
pollUrl: string;
disableNotificationsUrl: string;
@ -28,14 +28,30 @@ export const NotificationEmail = ({
<EmailLayout ctx={ctx} preview={preview}>
{children}
<Section style={{ marginTop: 32, marginBottom: 32 }}>
<Button href={pollUrl}>View on {domain}</Button>
<Button href={pollUrl}>
{ctx.t("common_viewOn", {
ns: "emails",
defaultValue: "View on {{domain}}",
domain,
})}
</Button>
</Section>
<Text light={true}>
If you would like to stop receiving updates you can{" "}
<Link className="whitespace-nowrap" href={disableNotificationsUrl}>
turn notifications off
</Link>
.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="common_disableNotifications"
ns="emails"
defaults="If you would like to stop receiving updates you can <a>turn notifications off</a>."
components={{
a: (
<Link
className="whitespace-nowrap"
href={disableNotificationsUrl}
/>
),
}}
/>
</Text>
</EmailLayout>
);

View file

@ -9,7 +9,7 @@ import {
TextProps,
} from "@react-email/components";
import { EmailContext } from "./email-context";
import type { EmailContext } from "../types";
export const lightTextColor = "#4B5563";
export const darkTextColor = "#1F2937";

View file

@ -0,0 +1,26 @@
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
const i18nInstance = createInstance();
i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`../locales/${language}/${namespace}.json`),
),
);
const i18nDefaultConfig = {
lng: "en",
fallbackLng: "en",
ns: ["emails"],
fallbackNS: "emails",
defaultNS: "emails",
} as const;
export type I18nInstance = typeof i18nInstance;
export { i18nDefaultConfig, i18nInstance };

View file

@ -0,0 +1,19 @@
import { previewEmailContext } from "../components/email-context";
import { FinalizeHostEmail } from "../templates/finalized-host";
export default function FinalizedHostPreview() {
return (
<FinalizeHostEmail
name="John Doe"
location="Zoom"
attendees={["johndoe@example.com", "janedoe@example.com"]}
title="Untitled Poll"
pollUrl="https://rallly.co"
day="12"
dow="Fri"
date="Friday, 12th June 2020"
time="6:00 PM to 11:00 PM BST"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,17 @@
import { previewEmailContext } from "../components/email-context";
import { FinalizeParticipantEmail } from "../templates/finalized-participant";
export default function FinalizedParticipantPreview() {
return (
<FinalizeParticipantEmail
title="Untitled Poll"
hostName="Host"
pollUrl="https://rallly.co"
day="12"
dow="Fri"
date="Friday, 12th June 2020"
time="6:00 PM to 11:00 PM BST"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,12 @@
import { previewEmailContext } from "../components/email-context";
import { LoginEmail } from "../templates/login";
export default function LoginPreview() {
return (
<LoginEmail
code="123456"
magicLink="https://rallly.co"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,16 @@
import { previewEmailContext } from "../components/email-context";
import { NewCommentEmail } from "../templates/new-comment";
function NewCommentEmailPreview() {
return (
<NewCommentEmail
title="Untitled Poll"
authorName="Someone"
pollUrl="https://rallly.co"
disableNotificationsUrl="https://rallly.co"
ctx={previewEmailContext}
/>
);
}
export default NewCommentEmailPreview;

View file

@ -0,0 +1,12 @@
import { previewEmailContext } from "../components/email-context";
import NewParticipantConfirmationEmail from "../templates/new-participant-confirmation";
export default function NewParticipantConfirmationPreview() {
return (
<NewParticipantConfirmationEmail
title="Untitled Poll"
editSubmissionUrl="https://rallly.co"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,14 @@
import { previewEmailContext } from "../components/email-context";
import { NewParticipantEmail } from "../templates/new-participant";
export default function NewParticipantPreview() {
return (
<NewParticipantEmail
participantName="John Doe"
title="Untitled Poll"
pollUrl="https://rallly.co"
disableNotificationsUrl="https://rallly.co"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,14 @@
import { previewEmailContext } from "../components/email-context";
import NewPollEmail from "../templates/new-poll";
export default function NewPollPreview() {
return (
<NewPollEmail
title="Untitled Poll"
name="John Doe"
adminLink="https://rallly.co"
participantLink="https://rallly.co/invite/abc123"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,6 @@
import { previewEmailContext } from "../components/email-context";
import { RegisterEmail } from "../templates/register";
export default function RegisterEmailPreview() {
return <RegisterEmail code="123456" ctx={previewEmailContext} />;
}

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

@ -1,13 +1,15 @@
import * as aws from "@aws-sdk/client-ses";
import { defaultProvider } from "@aws-sdk/credential-provider-node";
import { renderAsync } from "@react-email/render";
import { waitUntil } from "@vercel/functions";
import { createTransport, Transporter } from "nodemailer";
import type Mail from "nodemailer/lib/mailer";
import previewEmail from "preview-email";
import React from "react";
import { i18nDefaultConfig, i18nInstance } from "./i18n";
import * as templates from "./templates";
import { EmailContext } from "./templates/_components/email-context";
import type { EmailContext } from "./types";
type Templates = typeof templates;
@ -17,11 +19,12 @@ type TemplateProps<T extends TemplateName> = Omit<
React.ComponentProps<TemplateComponent<T>>,
"ctx"
>;
type TemplateComponent<T extends TemplateName> = Templates[T];
type TemplateComponent<T extends TemplateName> = Templates[T] & {
getSubject?: (props: TemplateProps<T>, ctx: EmailContext) => string;
};
type SendEmailOptions<T extends TemplateName> = {
to: string;
subject: string;
props: TemplateProps<T>;
attachments?: Mail.Options["attachments"];
};
@ -59,7 +62,15 @@ type EmailClientConfig = {
/**
* Context to pass to each email
*/
context: EmailContext;
config: {
logoUrl: string;
baseUrl: string;
domain: string;
supportEmail: string;
};
locale?: string;
onError?: (error: Error) => void;
};
export class EmailClient {
@ -70,16 +81,41 @@ export class EmailClient {
this.config = config;
}
queueTemplate<T extends TemplateName>(
templateName: T,
options: SendEmailOptions<T>,
) {
return waitUntil(
(async () => {
this.sendTemplate(templateName, options);
})(),
);
}
async sendTemplate<T extends TemplateName>(
templateName: T,
options: SendEmailOptions<T>,
) {
const locale = this.config.locale ?? "en";
await i18nInstance.init({
...i18nDefaultConfig,
lng: locale,
});
const ctx = {
...this.config.config,
i18n: i18nInstance,
t: i18nInstance.getFixedT(locale),
};
const Template = templates[templateName] as TemplateComponent<T>;
const subject = Template.getSubject?.(options.props, ctx);
const component = (
<Template
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(options.props as any)}
ctx={this.config.context}
ctx={ctx}
/>
);
@ -92,13 +128,24 @@ export class EmailClient {
await this.sendEmail({
from: this.config.mail.from,
to: options.to,
subject: options.subject,
subject,
html,
text,
attachments: options.attachments,
});
} catch (e) {
console.error("Error sending email", templateName, e);
const enhancedError = new Error(
`Failed to send email template: ${templateName}`,
{
cause: e instanceof Error ? e : new Error(String(e)),
},
);
Object.assign(enhancedError, {
templateName,
recipient: options.to,
subject,
});
this.config.onError?.(enhancedError);
}
}

View file

@ -1,13 +0,0 @@
export type EmailContext = {
logoUrl: string;
baseUrl: string;
domain: string;
supportEmail: string;
};
export const defaultEmailContext = {
logoUrl: "https://rallly-public.s3.amazonaws.com/images/rallly-logo-mark.png",
baseUrl: "https://rallly.co",
domain: "rallly.co",
supportEmail: "support@rallly.co",
};

View file

@ -1,13 +1,14 @@
import { Column, Row, Section } from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { EmailLayout } from "../components/email-layout";
import {
borderColor,
Button,
Heading,
Text,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
export interface FinalizeHostEmailProps {
date: string;
@ -22,20 +23,43 @@ export interface FinalizeHostEmailProps {
ctx: EmailContext;
}
export const FinalizeHostEmail = ({
title = "Untitled Poll",
pollUrl = "https://rallly.co",
day = "12",
dow = "Fri",
date = "Friday, 12th June 2020",
time = "6:00 PM to 11:00 PM BST",
ctx = defaultEmailContext,
const FinalizeHostEmail = ({
title,
pollUrl,
day,
dow,
date,
time,
ctx,
}: FinalizeHostEmailProps) => {
return (
<EmailLayout ctx={ctx} preview="Final date booked!">
<Heading>Final date booked!</Heading>
<EmailLayout
ctx={ctx}
preview={ctx.t("finalizeHost_preview", {
ns: "emails",
defaultValue:
"Final date booked! We've notified participants and sent them calendar invites.",
title,
})}
>
<Heading>
{ctx.t("finalizeHost_heading", {
defaultValue: "Final date booked!",
ns: "emails",
})}
</Heading>
<Text>
<strong>{title}</strong> has been booked for:
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="finalizeHost_content"
ns="emails"
values={{ title }}
components={{
b: <strong />,
}}
defaults="<b>{{title}}</b> has been booked for:"
/>
</Text>
<Section>
<Row>
@ -76,13 +100,33 @@ export const FinalizeHostEmail = ({
</Row>
</Section>
<Text>
We&apos;ve notified participants and sent them calendar invites.
{ctx.t("finalizeHost_content2", {
defaultValue:
"We've notified participants and sent them calendar invites.",
ns: "emails",
})}
</Text>
<Section style={{ marginTop: 32 }}>
<Button href={pollUrl}>View Event</Button>
<Button href={pollUrl}>
{ctx.t("finalizeHost_button", {
defaultValue: "View Event",
ns: "emails",
})}
</Button>
</Section>
</EmailLayout>
);
};
export default FinalizeHostEmail;
FinalizeHostEmail.getSubject = (
props: FinalizeHostEmailProps,
ctx: EmailContext,
) => {
return ctx.t("finalizeHost_subject", {
defaultValue: "Date booked for {{title}}",
title: props.title,
ns: "emails",
});
};
export { FinalizeHostEmail };

View file

@ -1,44 +1,62 @@
import { Column, Row, Section } from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { EmailLayout } from "../components/email-layout";
import {
borderColor,
Button,
Heading,
Text,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
export interface FinalizeParticipantEmailProps {
date: string;
day: string;
dow: string;
time: string;
name: string;
title: string;
hostName: string;
location: string | null;
pollUrl: string;
attendees: string[];
ctx: EmailContext;
}
export const FinalizeParticipantEmail = ({
title = "Untitled Poll",
hostName = "Host",
pollUrl = "https://rallly.co",
day = "12",
dow = "Fri",
date = "Friday, 12th June 2020",
time = "6:00 PM to 11:00 PM BST",
ctx = defaultEmailContext,
const FinalizeParticipantEmail = ({
title,
hostName,
pollUrl,
day,
dow,
date,
time,
ctx,
}: FinalizeParticipantEmailProps) => {
return (
<EmailLayout ctx={ctx} preview="Final date booked!">
<Heading>Final date booked!</Heading>
<EmailLayout
ctx={ctx}
preview={ctx.t("finalizeParticipant_preview", {
defaultValue: "Final date booked!",
ns: "emails",
})}
>
<Heading>
{ctx.t("finalizeParticipant_heading", {
defaultValue: "Final date booked!",
ns: "emails",
})}
</Heading>
<Text>
<strong>{hostName}</strong> has booked <strong>{title}</strong> for the
following date:
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="finalizeParticipant_content"
ns="emails"
defaults="<b>{{hostName}}</b> has booked <b>{{title}}</b> for the following date:"
values={{ hostName, title }}
components={{
b: <strong />,
}}
/>
</Text>
<Section data-testid="date-section">
<Row>
@ -78,7 +96,12 @@ export const FinalizeParticipantEmail = ({
</Column>
</Row>
</Section>
<Text>Please find attached a calendar invite for this event.</Text>
<Text>
{ctx.t("finalizeParticipant_content2", {
defaultValue:
"Please find attached a calendar invite for this event.",
})}
</Text>
<Section style={{ marginTop: 32 }}>
<Button href={pollUrl}>View Event</Button>
</Section>
@ -86,4 +109,15 @@ export const FinalizeParticipantEmail = ({
);
};
export default FinalizeParticipantEmail;
FinalizeParticipantEmail.getSubject = (
props: FinalizeParticipantEmailProps,
ctx: EmailContext,
) => {
return ctx.t("finalizeParticipant_subject", {
defaultValue: "Date booked for {{title}}",
title: props.title,
ns: "emails",
});
};
export { FinalizeParticipantEmail };

View file

@ -1,32 +1,42 @@
import { Section } from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { EmailLayout } from "../components/email-layout";
import {
Button,
Card,
Domain,
Heading,
Link,
Text,
trackingWide,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
interface LoginEmailProps {
name: string;
code: string;
magicLink: string;
ctx: EmailContext;
}
export const LoginEmail = ({
code = "123456",
magicLink = "https://rallly.co",
ctx = defaultEmailContext,
}: LoginEmailProps) => {
export const LoginEmail = ({ code, magicLink, ctx }: LoginEmailProps) => {
return (
<EmailLayout ctx={ctx} preview="Use this link to log in on this device.">
<Heading>Login</Heading>
<Text>Enter this one-time 6-digit verification code:</Text>
<EmailLayout
ctx={ctx}
preview={ctx.t("login_preview", {
defaultValue: "Use this link to log in on this device.",
ns: "emails",
})}
>
<Heading>
{ctx.t("login_heading", { defaultValue: "Login", ns: "emails" })}
</Heading>
<Text>
{ctx.t("login_content", {
defaultValue: "Enter this one-time 6-digit verification code:",
ns: "emails",
})}
</Text>
<Card style={{ textAlign: "center" }}>
<Text
style={{
@ -40,21 +50,48 @@ export const LoginEmail = ({
{code}
</Text>
<Text style={{ textAlign: "center" }} light={true}>
This code is valid for 15 minutes
{ctx.t("login_codeValid", {
defaultValue: "This code is valid for 15 minutes",
ns: "emails",
})}
</Text>
</Card>
<Section style={{ marginBottom: 32 }}>
<Button href={magicLink} id="magicLink">
Log in to {ctx.domain}
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="login_button"
defaults="Log in to {domain}"
values={{ domain: ctx.domain }}
ns="emails"
/>
</Button>
</Section>
<Text light>
You&apos;re receiving this email because a request was made to login to{" "}
<Domain ctx={ctx} />. If this wasn&apos;t you contact{" "}
<a href={`mailto:${ctx.supportEmail}`}>{ctx.supportEmail}</a>.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="login_content2"
defaults="You're receiving this email because a request was made to login to <domain />. If this wasn't you contact <a>{supportEmail}</a>."
values={{ supportEmail: ctx.supportEmail }}
components={{
domain: <Domain ctx={ctx} />,
a: <Link href={`mailto:${ctx.supportEmail}`} />,
}}
ns="emails"
/>
</Text>
</EmailLayout>
);
};
LoginEmail.getSubject = (props: LoginEmailProps, ctx: EmailContext) => {
return ctx.t("login_subject", {
defaultValue: "{{code}} is your 6-digit code",
code: props.code,
ns: "emails",
});
};
export default LoginEmail;

View file

@ -1,36 +1,70 @@
import { defaultEmailContext } from "./_components/email-context";
import { Trans } from "react-i18next/TransWithoutContext";
import NotificationEmail, {
NotificationBaseProps,
} from "./_components/notification-email";
import { Heading, Text } from "./_components/styled-components";
} from "../components/notification-email";
import { Heading, Text } from "../components/styled-components";
import type { EmailContext } from "../types";
export interface NewCommentEmailProps extends NotificationBaseProps {
authorName: string;
}
export const NewCommentEmail = ({
name = "Guest",
title = "Untitled Poll",
authorName = "Someone",
pollUrl = "https://rallly.co",
disableNotificationsUrl = "https://rallly.co",
ctx = defaultEmailContext,
const NewCommentEmail = ({
title,
authorName,
pollUrl,
disableNotificationsUrl,
ctx,
}: NewCommentEmailProps) => {
return (
<NotificationEmail
ctx={ctx}
name={name}
title={title}
pollUrl={pollUrl}
disableNotificationsUrl={disableNotificationsUrl}
preview="Go to your poll to see what they said."
preview={ctx.t("newComment_preview", {
ns: "emails",
defaultValue: "Go to your poll to see what they said.",
})}
>
<Heading>New Comment</Heading>
<Heading>
<Trans
i18n={ctx.i18n}
ns="emails"
i18nKey="newComment_heading"
defaults="New Comment"
/>
</Heading>
<Text>
<strong>{authorName}</strong> has commented on <strong>{title}</strong>.
<Trans
i18n={ctx.i18n}
ns="emails"
i18nKey="newComment_content"
defaults="<b>{{authorName}}</b> has commented on <b>{{title}}</b>."
components={{
b: <strong />,
}}
values={{
authorName,
title,
}}
/>
</Text>
</NotificationEmail>
);
};
export default NewCommentEmail;
NewCommentEmail.getSubject = (
props: NewCommentEmailProps,
ctx: EmailContext,
) => {
return ctx.t("newComment_subject", {
ns: "emails",
defaultValue: "{{authorName}} has commented on {{title}}",
authorName: props.authorName,
title: props.title,
});
};
export { NewCommentEmail };

View file

@ -1,46 +1,99 @@
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailLayout } from "../components/email-layout";
import {
Button,
Domain,
Heading,
Section,
Text,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
interface NewParticipantConfirmationEmailProps {
name: string;
title: string;
editSubmissionUrl: string;
ctx: EmailContext;
}
export const NewParticipantConfirmationEmail = ({
title = "Untitled Poll",
editSubmissionUrl = "https://rallly.co",
ctx = defaultEmailContext,
title,
editSubmissionUrl,
ctx,
}: NewParticipantConfirmationEmailProps) => {
const { domain } = ctx;
return (
<EmailLayout ctx={ctx} preview="To edit your response use the link below">
<Heading>Poll Response Confirmation</Heading>
<EmailLayout
ctx={ctx}
preview={ctx.t("newParticipantConfirmation_preview", {
defaultValue: "To edit your response use the link below",
ns: "emails",
})}
>
<Heading>
{ctx.t("newParticipantConfirmation_heading", {
defaultValue: "Poll Response Confirmation",
ns: "emails",
})}
</Heading>
<Text>
Your response to <strong>{title}</strong> has been submitted.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipantConfirmation_content"
defaults="Your response to <b>{{title}}</b> has been submitted."
components={{
b: <strong />,
}}
values={{ title }}
ns="emails"
/>
</Text>
<Text>
While the poll is still open you can change your response using the link
below.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipantConfirmation_content2"
defaults="While the poll is still open you can change your response using the link below."
ns="emails"
/>
</Text>
<Section style={{ marginTop: 32 }}>
<Button id="editSubmissionUrl" href={editSubmissionUrl}>
Review response on {domain}
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipantConfirmation_button"
defaults="Review response on {domain}"
values={{ domain }}
ns="emails"
/>
</Button>
</Section>
<Text light>
You are receiving this email because a response was submitted on{" "}
<Domain ctx={ctx} />. If this wasn&apos;t you, please ignore this email.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipantConfirmation_footnote"
defaults="You are receiving this email because a response was submitted on <domain />. If this wasn't you, please ignore this email."
components={{
domain: <Domain ctx={ctx} />,
}}
ns="emails"
/>
</Text>
</EmailLayout>
);
};
NewParticipantConfirmationEmail.getSubject = (
props: NewParticipantConfirmationEmailProps,
ctx: EmailContext,
) => {
return ctx.t("newParticipantConfirmation_subject", {
defaultValue: "Thanks for responding to {{title}}",
title: props.title,
ns: "emails",
});
};
export default NewParticipantConfirmationEmail;

View file

@ -1,38 +1,75 @@
import { defaultEmailContext } from "./_components/email-context";
import { Trans } from "react-i18next/TransWithoutContext";
import NotificationEmail, {
NotificationBaseProps,
} from "./_components/notification-email";
import { Heading, Text } from "./_components/styled-components";
} from "../components/notification-email";
import { Heading, Text } from "../components/styled-components";
import type { EmailContext } from "../types";
export interface NewParticipantEmailProps extends NotificationBaseProps {
participantName: string;
}
export const NewParticipantEmail = ({
name = "John",
title = "Untitled Poll",
participantName = "Someone",
pollUrl = "https://rallly.co",
disableNotificationsUrl = "https://rallly.co",
ctx = defaultEmailContext,
const NewParticipantEmail = ({
title,
participantName,
pollUrl,
disableNotificationsUrl,
ctx,
}: NewParticipantEmailProps) => {
return (
<NotificationEmail
ctx={ctx}
name={name}
title={title}
pollUrl={pollUrl}
disableNotificationsUrl={disableNotificationsUrl}
preview="Go to your poll to see the new response."
preview={ctx.t("newParticipant_preview", {
defaultValue: "Go to your poll to see the new response.",
ns: "emails",
})}
>
<Heading>New Response</Heading>
<Heading>
{ctx.t("newParticipant_heading", {
defaultValue: "New Response",
ns: "emails",
})}
</Heading>
<Text>
<strong>{participantName}</strong> has responded to{" "}
<strong>{title}</strong>.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipant_content"
ns="emails"
defaults="<b>{{name}}</b> has responded to <b>{{title}}</b>."
components={{
b: <strong />,
}}
values={{ name: participantName, title }}
/>
</Text>
<Text>
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipant_content2"
defaults="Go to your poll to see the new response."
ns="emails"
/>
</Text>
<Text>Go to your poll to see the new response.</Text>
</NotificationEmail>
);
};
export default NewParticipantEmail;
NewParticipantEmail.getSubject = (
props: NewParticipantEmailProps,
ctx: EmailContext,
) => {
return ctx.t("newParticipant_subject", {
defaultValue: "{{name}} has responded to {{title}}",
name: props.participantName,
title: props.title,
ns: "emails",
});
};
export { NewParticipantEmail };

View file

@ -1,12 +1,14 @@
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailLayout } from "../components/email-layout";
import {
Button,
Card,
Heading,
Link,
Text,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
export interface NewPollEmailProps {
title: string;
@ -17,29 +19,61 @@ export interface NewPollEmailProps {
}
export const NewPollEmail = ({
title = "Untitled Poll",
adminLink = "https://rallly.co/admin/abcdefg123",
participantLink = "https://rallly.co/invite/wxyz9876",
ctx = defaultEmailContext,
title,
adminLink,
participantLink,
ctx,
}: NewPollEmailProps) => {
return (
<EmailLayout
ctx={ctx}
preview="Share your participant link to start collecting responses."
preview={ctx.t("newPoll_preview", {
defaultValue:
"Share your participant link to start collecting responses.",
ns: "emails",
})}
>
<Heading>New Poll Created</Heading>
<Heading>
{ctx.t("newPoll_heading", {
defaultValue: "New Poll Created",
ns: "emails",
})}
</Heading>
<Text>
Your meeting poll titled <strong>{`"${title}"`}</strong> is ready! Share
it using the link below:
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newPoll_content"
ns="emails"
values={{ title }}
components={{
b: <strong />,
}}
defaults="Your meeting poll titled <b>{{title}}</b> is ready! Share it using the link below:"
/>
</Text>
<Card style={{ textAlign: "center" }}>
<Text style={{ textAlign: "center" }}>
<Link href={participantLink}>{participantLink}</Link>
</Text>
</Card>
<Button href={adminLink}>Manage Poll &rarr;</Button>
<Button href={adminLink}>
{ctx.t("newPoll_button", {
defaultValue: "Manage Poll",
ns: "emails",
})}
&rarr;
</Button>
</EmailLayout>
);
};
NewPollEmail.getSubject = (props: NewPollEmailProps, ctx: EmailContext) => {
return ctx.t("newPoll_subject", {
defaultValue: "Let's find a date for {{title}}!",
title: props.title,
ns: "emails",
});
};
export default NewPollEmail;

View file

@ -1,29 +1,43 @@
import { Section } from "@react-email/section";
import { Trans } from "react-i18next/TransWithoutContext";
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { EmailLayout } from "../components/email-layout";
import {
Card,
Domain,
Heading,
Text,
trackingWide,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
interface RegisterEmailProps {
code: string;
ctx: EmailContext;
}
export const RegisterEmail = ({
code = "123456",
ctx = defaultEmailContext,
}: RegisterEmailProps) => {
export const RegisterEmail = ({ code, ctx }: RegisterEmailProps) => {
return (
<EmailLayout ctx={ctx} preview={`Your 6-digit code is: ${code}`}>
<Heading>Verify your email address</Heading>
<EmailLayout
ctx={ctx}
preview={ctx.t("register_preview", {
ns: "emails",
defaultValue: "Your 6-digit code is: {{code}}",
code,
})}
>
<Heading>
{ctx.t("register_heading", {
defaultValue: "Verify your email address",
ns: "emails",
})}
</Heading>
<Text>
Please use the following 6-digit verification code to verify your email:
{ctx.t("register_text", {
defaultValue:
"Please use the following 6-digit verification code to verify your email",
ns: "emails",
})}
</Text>
<Card style={{ textAlign: "center" }}>
<Text
@ -38,18 +52,36 @@ export const RegisterEmail = ({
{code}
</Text>
<Text style={{ textAlign: "center" }} light={true}>
This code is valid for 15 minutes
{ctx.t("register_codeValid", {
defaultValue: "This code is valid for 15 minutes",
ns: "emails",
})}
</Text>
</Card>
<Section>
<Text light={true}>
You&apos;re receiving this email because a request was made to
register an account on <Domain ctx={ctx} />. If this wasn&apos;t you,
please ignore this email.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="register_footer"
ns="emails"
values={{ domain: ctx.domain }}
components={{
domain: <Domain ctx={ctx} />,
}}
defaults="You're receiving this email because a request was made to register an account on <domain />. If this wasn't you, please ignore this email."
/>
</Text>
</Section>
</EmailLayout>
);
};
RegisterEmail.getSubject = (_props: RegisterEmailProps, ctx: EmailContext) => {
return ctx.t("register_subject", {
defaultValue: "Please verify your email address",
ns: "emails",
});
};
export default RegisterEmail;

View file

@ -0,0 +1,12 @@
import type { TFunction } from "i18next";
import type { I18nInstance } from "./i18n";
export type EmailContext = {
logoUrl: string;
baseUrl: string;
domain: string;
supportEmail: string;
i18n: I18nInstance;
t: TFunction;
};