mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-19 01:07:47 +02:00
✨ Translations for Email Notifications (#1278)
Co-authored-by: Niko Heller <hellerniko@gmail.com>
This commit is contained in:
parent
aa52a0f26f
commit
f4218c3115
51 changed files with 1071 additions and 970 deletions
16
packages/emails/src/components/email-context.tsx
Normal file
16
packages/emails/src/components/email-context.tsx
Normal 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"),
|
||||
};
|
|
@ -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>
|
|
@ -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>
|
||||
);
|
|
@ -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";
|
26
packages/emails/src/i18n.ts
Normal file
26
packages/emails/src/i18n.ts
Normal 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 };
|
19
packages/emails/src/previews/finalized-host.tsx
Normal file
19
packages/emails/src/previews/finalized-host.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
17
packages/emails/src/previews/finalized-participant.tsx
Normal file
17
packages/emails/src/previews/finalized-participant.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
12
packages/emails/src/previews/login.tsx
Normal file
12
packages/emails/src/previews/login.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
16
packages/emails/src/previews/new-comment.tsx
Normal file
16
packages/emails/src/previews/new-comment.tsx
Normal 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;
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
14
packages/emails/src/previews/new-participant.tsx
Normal file
14
packages/emails/src/previews/new-participant.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
14
packages/emails/src/previews/new-poll.tsx
Normal file
14
packages/emails/src/previews/new-poll.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
6
packages/emails/src/previews/register.tsx
Normal file
6
packages/emails/src/previews/register.tsx
Normal 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} />;
|
||||
}
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
};
|
|
@ -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'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 };
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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're receiving this email because a request was made to login to{" "}
|
||||
<Domain ctx={ctx} />. If this wasn'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;
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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'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;
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 →</Button>
|
||||
<Button href={adminLink}>
|
||||
{ctx.t("newPoll_button", {
|
||||
defaultValue: "Manage Poll",
|
||||
ns: "emails",
|
||||
})}
|
||||
→
|
||||
</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;
|
||||
|
|
|
@ -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're receiving this email because a request was made to
|
||||
register an account on <Domain ctx={ctx} />. If this wasn'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;
|
||||
|
|
12
packages/emails/src/types.ts
Normal file
12
packages/emails/src/types.ts
Normal 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;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue