mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-20 04:30:56 -05:00
feat: email package for client side email components (#6986)
This commit is contained in:
@@ -474,7 +474,7 @@
|
||||
"forgot_password_email_subject": "Réinitialise ton mot de passe Formbricks",
|
||||
"forgot_password_email_text": "Vous avez demandé un lien pour changer votre mot de passe. Vous pouvez le faire en cliquant sur le lien ci-dessous :",
|
||||
"hidden_field": "Champ caché",
|
||||
"imprint": "Impressum",
|
||||
"imprint": "Empreinte",
|
||||
"invite_accepted_email_heading": "Salut",
|
||||
"invite_accepted_email_subject": "Vous avez un nouveau membre dans votre organisation !",
|
||||
"invite_accepted_email_text_par1": "Je te fais savoir que",
|
||||
|
||||
@@ -474,7 +474,7 @@
|
||||
"forgot_password_email_subject": "Redefinir sua senha Formbricks",
|
||||
"forgot_password_email_text": "Você pediu um link pra trocar sua senha. Você pode fazer isso clicando no link abaixo:",
|
||||
"hidden_field": "Campo oculto",
|
||||
"imprint": "Impressum",
|
||||
"imprint": "impressão",
|
||||
"invite_accepted_email_heading": "E aí",
|
||||
"invite_accepted_email_subject": "Você tem um novo membro na sua organização!",
|
||||
"invite_accepted_email_text_par1": "Só pra te avisar que",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { TFunction } from "i18next";
|
||||
import { CalendarDaysIcon, ExternalLinkIcon, UploadIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import {
|
||||
Column,
|
||||
Container,
|
||||
ElementHeader,
|
||||
Button as EmailButton,
|
||||
Img,
|
||||
Link,
|
||||
@@ -8,11 +12,8 @@ import {
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import { render } from "@react-email/render";
|
||||
import { TFunction } from "i18next";
|
||||
import { CalendarDaysIcon, ExternalLinkIcon, UploadIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
render,
|
||||
} from "@formbricks/email";
|
||||
import { TSurveyCTAElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { type TSurvey, type TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
@@ -24,7 +25,6 @@ import { isLight, mixColor } from "@/lib/utils/colors";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { RatingSmiley } from "@/modules/analysis/components/RatingSmiley";
|
||||
import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils";
|
||||
import { ElementHeader } from "./email-element-header";
|
||||
|
||||
interface PreviewEmailTemplateProps {
|
||||
survey: TSurvey;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface ForgotPasswordEmailProps {
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export async function ForgotPasswordEmail({
|
||||
verifyLink,
|
||||
}: ForgotPasswordEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.forgot_password_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.forgot_password_email_text")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.forgot_password_email_change_password")} />
|
||||
<Text className="text-sm font-bold">{t("emails.forgot_password_email_link_valid_for_24_hours")}</Text>
|
||||
<Text className="mb-0 text-sm">{t("emails.forgot_password_email_did_not_request")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default ForgotPasswordEmail;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface VerificationEmailProps {
|
||||
readonly verifyLink: string;
|
||||
}
|
||||
|
||||
export async function NewEmailVerification({
|
||||
verifyLink,
|
||||
}: VerificationEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.new_email_verification_text")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_security_notice")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.verification_email_verify_email")} />
|
||||
<Text className="text-sm">{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="break-all text-sm text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewEmailVerification;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
export async function PasswordResetNotifyEmail(): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.password_changed_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.password_changed_email_text")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default PasswordResetNotifyEmail;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface EmailCustomizationPreviewEmailProps {
|
||||
userName: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export async function EmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
logoUrl,
|
||||
}: EmailCustomizationPreviewEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.email_customization_preview_email_heading", { userName })}</Heading>
|
||||
<Text className="text-sm">{t("emails.email_customization_preview_email_text")}</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailCustomizationPreviewEmail;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface InviteAcceptedEmailProps {
|
||||
inviterName: string;
|
||||
inviteeName: string;
|
||||
}
|
||||
|
||||
export async function InviteAcceptedEmail({
|
||||
inviterName,
|
||||
inviteeName,
|
||||
}: InviteAcceptedEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>
|
||||
{t("emails.invite_accepted_email_heading", { inviterName })} {inviterName}
|
||||
</Heading>
|
||||
<Text className="text-sm">
|
||||
{t("emails.invite_accepted_email_text_par1", { inviteeName })} {inviteeName}{" "}
|
||||
{t("emails.invite_accepted_email_text_par2")}
|
||||
</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteAcceptedEmail;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface InviteEmailProps {
|
||||
inviteeName: string;
|
||||
inviterName: string;
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export async function InviteEmail({
|
||||
inviteeName,
|
||||
inviterName,
|
||||
verifyLink,
|
||||
}: InviteEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>
|
||||
{t("emails.invite_email_heading", { inviteeName })} {inviteeName}
|
||||
</Heading>
|
||||
<Text className="text-sm">
|
||||
{t("emails.invite_email_text_par1", { inviterName })} {inviterName}{" "}
|
||||
{t("emails.invite_email_text_par2")}
|
||||
</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.invite_email_button_label")} />
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteEmail;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface EmbedSurveyPreviewEmailProps {
|
||||
html: string;
|
||||
environmentId: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export async function EmbedSurveyPreviewEmail({
|
||||
html,
|
||||
environmentId,
|
||||
logoUrl,
|
||||
}: EmbedSurveyPreviewEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.embed_survey_preview_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.embed_survey_preview_email_text")}</Text>
|
||||
<Text className="text-sm">
|
||||
<b>{t("emails.embed_survey_preview_email_didnt_request")}</b>{" "}
|
||||
{t("emails.embed_survey_preview_email_fight_spam")}
|
||||
</Text>
|
||||
<div className="text-sm" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<Text className="text-center text-sm text-slate-700">
|
||||
{t("emails.embed_survey_preview_email_environment_id")}: {environmentId}
|
||||
</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmbedSurveyPreviewEmail;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface LinkSurveyEmailProps {
|
||||
surveyName: string;
|
||||
surveyLink: string;
|
||||
logoUrl: string;
|
||||
}
|
||||
|
||||
export async function LinkSurveyEmail({
|
||||
surveyName,
|
||||
surveyLink,
|
||||
logoUrl,
|
||||
}: LinkSurveyEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_hey")}</Heading>
|
||||
<Text className="text-sm">{t("emails.verification_email_thanks")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_email_to_fill_survey")}</Text>
|
||||
<EmailButton href={surveyLink} label={t("emails.verification_email_take_survey")} />
|
||||
<Text className="text-sm text-slate-400">
|
||||
{t("emails.verification_email_survey_name")}: {surveyName}
|
||||
</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkSurveyEmail;
|
||||
@@ -1,6 +1,18 @@
|
||||
import { render } from "@react-email/render";
|
||||
import { createTransport } from "nodemailer";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import {
|
||||
renderEmailCustomizationPreviewEmail,
|
||||
renderEmbedSurveyPreviewEmail,
|
||||
renderForgotPasswordEmail,
|
||||
renderInviteAcceptedEmail,
|
||||
renderInviteEmail,
|
||||
renderLinkSurveyEmail,
|
||||
renderNewEmailVerification,
|
||||
renderPasswordResetNotifyEmail,
|
||||
renderResponseFinishedEmail,
|
||||
renderVerificationEmail,
|
||||
} from "@formbricks/email";
|
||||
import { TEmailTemplateLegalProps } from "@formbricks/email/src/types/email";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
@@ -9,8 +21,11 @@ import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserEmail, TUserLocale } from "@formbricks/types/user";
|
||||
import {
|
||||
DEBUG,
|
||||
IMPRINT_ADDRESS,
|
||||
IMPRINT_URL,
|
||||
MAIL_FROM,
|
||||
MAIL_FROM_NAME,
|
||||
PRIVACY_URL,
|
||||
SMTP_AUTHENTICATED,
|
||||
SMTP_HOST,
|
||||
SMTP_PASSWORD,
|
||||
@@ -18,25 +33,24 @@ import {
|
||||
SMTP_REJECT_UNAUTHORIZED_TLS,
|
||||
SMTP_SECURE_ENABLED,
|
||||
SMTP_USER,
|
||||
TERMS_URL,
|
||||
WEBAPP_URL,
|
||||
} from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { createEmailChangeToken, createInviteToken, createToken, createTokenForLinkSurvey } from "@/lib/jwt";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import NewEmailVerification from "@/modules/email/emails/auth/new-email-verification";
|
||||
import { EmailCustomizationPreviewEmail } from "@/modules/email/emails/general/email-customization-preview-email";
|
||||
import { ForgotPasswordEmail } from "./emails/auth/forgot-password-email";
|
||||
import { PasswordResetNotifyEmail } from "./emails/auth/password-reset-notify-email";
|
||||
import { VerificationEmail } from "./emails/auth/verification-email";
|
||||
import { InviteAcceptedEmail } from "./emails/invite/invite-accepted-email";
|
||||
import { InviteEmail } from "./emails/invite/invite-email";
|
||||
import { EmbedSurveyPreviewEmail } from "./emails/survey/embed-survey-preview-email";
|
||||
import { LinkSurveyEmail } from "./emails/survey/link-survey-email";
|
||||
import { ResponseFinishedEmail } from "./emails/survey/response-finished-email";
|
||||
|
||||
export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT);
|
||||
|
||||
const legalProps: TEmailTemplateLegalProps = {
|
||||
privacyUrl: PRIVACY_URL || undefined,
|
||||
termsUrl: TERMS_URL || undefined,
|
||||
imprintUrl: IMPRINT_URL || undefined,
|
||||
imprintAddress: IMPRINT_ADDRESS || undefined,
|
||||
};
|
||||
|
||||
interface SendEmailDataProps {
|
||||
to: string;
|
||||
replyTo?: string;
|
||||
@@ -89,7 +103,7 @@ export const sendVerificationNewEmail = async (id: string, email: string): Promi
|
||||
const token = createEmailChangeToken(id, email);
|
||||
const verifyLink = `${WEBAPP_URL}/verify-email-change?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const html = await render(await NewEmailVerification({ verifyLink }));
|
||||
const html = await renderNewEmailVerification({ verifyLink, t, ...legalProps });
|
||||
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
@@ -117,7 +131,12 @@ export const sendVerificationEmail = async ({
|
||||
const verifyLink = `${WEBAPP_URL}/auth/verify?token=${encodeURIComponent(token)}`;
|
||||
const verificationRequestLink = `${WEBAPP_URL}/auth/verification-requested?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const html = await render(await VerificationEmail({ verificationRequestLink, verifyLink }));
|
||||
const html = await renderVerificationEmail({
|
||||
verificationRequestLink,
|
||||
verifyLink,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
@@ -140,7 +159,7 @@ export const sendForgotPasswordEmail = async (user: {
|
||||
expiresIn: "1d",
|
||||
});
|
||||
const verifyLink = `${WEBAPP_URL}/auth/forgot-password/reset?token=${encodeURIComponent(token)}`;
|
||||
const html = await render(await ForgotPasswordEmail({ verifyLink }));
|
||||
const html = await renderForgotPasswordEmail({ verifyLink, t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: user.email,
|
||||
subject: t("emails.forgot_password_email_subject"),
|
||||
@@ -150,7 +169,7 @@ export const sendForgotPasswordEmail = async (user: {
|
||||
|
||||
export const sendPasswordResetNotifyEmail = async (user: { email: string }): Promise<boolean> => {
|
||||
const t = await getTranslate();
|
||||
const html = await render(await PasswordResetNotifyEmail());
|
||||
const html = await renderPasswordResetNotifyEmail({ t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: user.email,
|
||||
subject: t("emails.password_reset_notify_email_subject"),
|
||||
@@ -171,7 +190,7 @@ export const sendInviteMemberEmail = async (
|
||||
|
||||
const verifyLink = `${WEBAPP_URL}/invite?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const html = await render(await InviteEmail({ inviteeName, inviterName, verifyLink }));
|
||||
const html = await renderInviteEmail({ inviteeName, inviterName, verifyLink, t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.invite_member_email_subject"),
|
||||
@@ -185,7 +204,7 @@ export const sendInviteAcceptedEmail = async (
|
||||
email: string
|
||||
): Promise<void> => {
|
||||
const t = await getTranslate();
|
||||
const html = await render(await InviteAcceptedEmail({ inviteeName, inviterName }));
|
||||
const html = await renderInviteAcceptedEmail({ inviteeName, inviterName, t, ...legalProps });
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.invite_accepted_email_subject"),
|
||||
@@ -208,16 +227,20 @@ export const sendResponseFinishedEmail = async (
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const html = await render(
|
||||
await ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
})
|
||||
);
|
||||
// Pre-process the element response mapping before passing to email
|
||||
const elements = getElementResponseMapping(survey, response);
|
||||
|
||||
const html = await renderResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
elements,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
@@ -241,7 +264,13 @@ export const sendEmbedSurveyPreviewEmail = async (
|
||||
logoUrl?: string
|
||||
): Promise<boolean> => {
|
||||
const t = await getTranslate();
|
||||
const html = await render(await EmbedSurveyPreviewEmail({ html: innerHtml, environmentId, logoUrl }));
|
||||
const html = await renderEmbedSurveyPreviewEmail({
|
||||
html: innerHtml,
|
||||
environmentId,
|
||||
logoUrl,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
return await sendEmail({
|
||||
to,
|
||||
subject: t("emails.embed_survey_preview_email_subject"),
|
||||
@@ -255,7 +284,12 @@ export const sendEmailCustomizationPreviewEmail = async (
|
||||
logoUrl?: string
|
||||
): Promise<boolean> => {
|
||||
const t = await getTranslate();
|
||||
const emailHtmlBody = await render(await EmailCustomizationPreviewEmail({ userName, logoUrl }));
|
||||
const emailHtmlBody = await renderEmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
logoUrl,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
|
||||
return await sendEmail({
|
||||
to,
|
||||
@@ -280,7 +314,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
|
||||
};
|
||||
const surveyLink = getSurveyLink();
|
||||
|
||||
const html = await render(await LinkSurveyEmail({ surveyName, surveyLink, logoUrl }));
|
||||
const html = await renderLinkSurveyEmail({ surveyName, surveyLink, logoUrl, t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: data.email,
|
||||
subject: t("emails.verified_link_survey_email_subject"),
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Column, Hr, Row, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { TSurveyFollowUp } from "@formbricks/database/types/survey-follow-up";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailTemplate } from "@/modules/email/components/email-template";
|
||||
import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils";
|
||||
|
||||
interface FollowUpEmailProps {
|
||||
readonly followUp: TSurveyFollowUp;
|
||||
readonly logoUrl?: string;
|
||||
readonly attachResponseData: boolean;
|
||||
readonly includeVariables: boolean;
|
||||
readonly includeHiddenFields: boolean;
|
||||
readonly survey: TSurvey;
|
||||
readonly response: TResponse;
|
||||
}
|
||||
|
||||
export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JSX.Element> {
|
||||
const { properties } = props.followUp.action;
|
||||
let { body } = properties;
|
||||
|
||||
// Parse recall tags and replace with actual response values
|
||||
body = parseRecallInfo(body, props.response.data, props.response.variables);
|
||||
|
||||
const elements = props.attachResponseData ? getElementResponseMapping(props.survey, props.response) : [];
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<EmailTemplate logoUrl={props.logoUrl} t={t}>
|
||||
<>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtml(body, {
|
||||
allowedTags: ["p", "span", "b", "strong", "i", "em", "a", "br"],
|
||||
allowedAttributes: {
|
||||
a: ["href", "rel", "target"],
|
||||
"*": ["dir", "class"],
|
||||
},
|
||||
allowedSchemes: ["http", "https"],
|
||||
allowedSchemesByTag: {
|
||||
a: ["http", "https"],
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
{elements.length > 0 ? (
|
||||
<>
|
||||
<Hr />
|
||||
<Text className="mb-4 text-base font-semibold text-slate-900">{t("emails.response_data")}</Text>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{elements.map((e) => {
|
||||
if (!e.response) return;
|
||||
return (
|
||||
<Row key={e.element}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">{e.element}</Text>
|
||||
{renderEmailResponseValue(e.response, e.type, t, true)}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
{props.attachResponseData &&
|
||||
props.includeVariables &&
|
||||
props.survey.variables
|
||||
.filter((variable) => {
|
||||
const variableResponse = props.response.variables[variable.id];
|
||||
if (typeof variableResponse !== "string" && typeof variableResponse !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return variableResponse !== undefined;
|
||||
})
|
||||
.map((variable) => {
|
||||
const variableResponse = props.response.variables[variable.id];
|
||||
return (
|
||||
<Row key={variable.id}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">
|
||||
{variable.type === "number"
|
||||
? `${t("emails.number_variable")}: ${variable.name}`
|
||||
: `${t("emails.text_variable")}: ${variable.name}`}
|
||||
</Text>
|
||||
<Text className="mt-0 text-sm break-words whitespace-pre-wrap text-slate-700">
|
||||
{variableResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
{props.attachResponseData &&
|
||||
props.includeHiddenFields &&
|
||||
props.survey.hiddenFields.fieldIds
|
||||
?.filter((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = props.response.data[hiddenFieldId];
|
||||
return hiddenFieldResponse && typeof hiddenFieldResponse === "string";
|
||||
})
|
||||
.map((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = props.response.data[hiddenFieldId] as string;
|
||||
return (
|
||||
<Row key={hiddenFieldId}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">
|
||||
{t("emails.hidden_field")}: {hiddenFieldId}
|
||||
</Text>
|
||||
<Text className="mt-0 text-sm break-words whitespace-pre-wrap text-slate-700">
|
||||
{hiddenFieldResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import { render } from "@react-email/components";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { TSurveyFollowUp } from "@formbricks/database/types/survey-follow-up";
|
||||
import {
|
||||
ProcessedHiddenField,
|
||||
ProcessedResponseElement,
|
||||
ProcessedVariable,
|
||||
renderFollowUpEmail,
|
||||
} from "@formbricks/email";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL, TERMS_URL } from "@/lib/constants";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { sendEmail } from "@/modules/email";
|
||||
import { FollowUpEmail } from "@/modules/survey/follow-ups/components/follow-up-email";
|
||||
|
||||
export const sendFollowUpEmail = async ({
|
||||
followUp,
|
||||
@@ -28,21 +37,79 @@ export const sendFollowUpEmail = async ({
|
||||
}): Promise<void> => {
|
||||
const {
|
||||
action: {
|
||||
properties: { subject },
|
||||
properties: { subject, body },
|
||||
},
|
||||
} = followUp;
|
||||
|
||||
const emailHtmlBody = await render(
|
||||
await FollowUpEmail({
|
||||
followUp,
|
||||
logoUrl,
|
||||
attachResponseData,
|
||||
includeVariables,
|
||||
includeHiddenFields,
|
||||
survey,
|
||||
response,
|
||||
})
|
||||
);
|
||||
const t = await getTranslate();
|
||||
|
||||
// Process body: parse recall tags and sanitize HTML
|
||||
const processedBody = sanitizeHtml(parseRecallInfo(body, response.data, response.variables), {
|
||||
allowedTags: ["p", "span", "b", "strong", "i", "em", "a", "br"],
|
||||
allowedAttributes: {
|
||||
a: ["href", "rel", "target"],
|
||||
"*": ["dir", "class"],
|
||||
},
|
||||
allowedSchemes: ["http", "https"],
|
||||
allowedSchemesByTag: {
|
||||
a: ["http", "https"],
|
||||
},
|
||||
});
|
||||
|
||||
// Process response data
|
||||
const responseData: ProcessedResponseElement[] = attachResponseData
|
||||
? getElementResponseMapping(survey, response).map((e) => ({
|
||||
element: e.element,
|
||||
response: e.response,
|
||||
type: e.type,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Process variables
|
||||
const variables: ProcessedVariable[] =
|
||||
attachResponseData && includeVariables
|
||||
? survey.variables
|
||||
.filter((variable) => {
|
||||
const variableResponse = response.variables[variable.id];
|
||||
return (
|
||||
(typeof variableResponse === "string" || typeof variableResponse === "number") &&
|
||||
variableResponse !== undefined
|
||||
);
|
||||
})
|
||||
.map((variable) => ({
|
||||
id: variable.id,
|
||||
name: variable.name,
|
||||
type: variable.type,
|
||||
value: response.variables[variable.id],
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Process hidden fields
|
||||
const hiddenFields: ProcessedHiddenField[] =
|
||||
attachResponseData && includeHiddenFields
|
||||
? (survey.hiddenFields.fieldIds
|
||||
?.filter((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = response.data[hiddenFieldId];
|
||||
return hiddenFieldResponse && typeof hiddenFieldResponse === "string";
|
||||
})
|
||||
.map((hiddenFieldId) => ({
|
||||
id: hiddenFieldId,
|
||||
value: response.data[hiddenFieldId] as string,
|
||||
})) ?? [])
|
||||
: [];
|
||||
|
||||
const emailHtmlBody = await renderFollowUpEmail({
|
||||
body: processedBody,
|
||||
responseData,
|
||||
variables,
|
||||
hiddenFields,
|
||||
logoUrl,
|
||||
t,
|
||||
privacyUrl: PRIVACY_URL || undefined,
|
||||
termsUrl: TERMS_URL || undefined,
|
||||
imprintUrl: IMPRINT_URL || undefined,
|
||||
imprintAddress: IMPRINT_ADDRESS || undefined,
|
||||
});
|
||||
|
||||
await sendEmail({
|
||||
to,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@formbricks/cache": "workspace:*",
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/email": "workspace:*",
|
||||
"@formbricks/i18n-utils": "workspace:*",
|
||||
"@formbricks/js-core": "workspace:*",
|
||||
"@formbricks/logger": "workspace:*",
|
||||
@@ -70,7 +71,6 @@
|
||||
"@radix-ui/react-toggle": "1.1.8",
|
||||
"@radix-ui/react-toggle-group": "1.1.9",
|
||||
"@radix-ui/react-tooltip": "1.2.6",
|
||||
"@react-email/components": "0.0.38",
|
||||
"@sentry/nextjs": "10.5.0",
|
||||
"@t3-oss/env-nextjs": "0.13.4",
|
||||
"@tailwindcss/forms": "0.5.10",
|
||||
|
||||
4
packages/email/.eslintrc.cjs
Normal file
4
packages/email/.eslintrc.cjs
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ["@formbricks/eslint-config/legacy-react.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
86
packages/email/README.md
Normal file
86
packages/email/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# @formbricks/emails
|
||||
|
||||
Email templates for Formbricks with React Email preview server.
|
||||
|
||||
## Purpose
|
||||
|
||||
This package provides email templates for visual QA and preview. It includes:
|
||||
|
||||
- Email templates (auth, invite, survey, general)
|
||||
- Shared email UI components
|
||||
- Mock translation utilities for preview
|
||||
- Example data for template rendering
|
||||
- Tailwind CSS for styling with full intellisense support
|
||||
|
||||
## Development
|
||||
|
||||
### Preview Server
|
||||
|
||||
Run the React Email preview server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Visit `localhost:3456` to preview all email templates with mock data.
|
||||
|
||||
### Styling
|
||||
|
||||
The package uses Tailwind CSS via `@react-email/components`. Tailwind intellisense is configured and should work automatically in your IDE. The config files are:
|
||||
|
||||
- `tailwind.config.js` - Tailwind configuration for intellisense
|
||||
- `postcss.config.js` - PostCSS configuration
|
||||
|
||||
### Path Aliases
|
||||
|
||||
Use `@/` prefix for clean imports:
|
||||
|
||||
```typescript
|
||||
import { FollowUpEmail } from "@/emails/survey/follow-up-email";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { mockT } from "@/src/lib/mock-translate";
|
||||
```
|
||||
|
||||
## Usage in Production
|
||||
|
||||
The web app imports render helper functions from this package:
|
||||
|
||||
```typescript
|
||||
import { renderVerificationEmail } from "@formbricks/email";
|
||||
|
||||
// Pass real translation function and data
|
||||
const html = await renderVerificationEmail({
|
||||
verifyLink,
|
||||
verificationRequestLink,
|
||||
t, // Real i18n function from getTranslate()
|
||||
});
|
||||
```
|
||||
|
||||
For complex emails with pre-processing:
|
||||
|
||||
```typescript
|
||||
import { renderResponseFinishedEmail } from "@formbricks/email";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
|
||||
// Pre-process data before rendering
|
||||
const elements = getElementResponseMapping(survey, response);
|
||||
|
||||
const html = await renderResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
elements, // Pre-processed data
|
||||
t,
|
||||
});
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Preview Mode**: Templates use mock `t()` function and example data for visual QA
|
||||
- **Production Mode**: Web app passes real `t()` function and pre-processed data
|
||||
- **Render Functions**: Typed helper functions abstract `@react-email/render` from web app
|
||||
- **No Business Logic**: SMTP, i18n, JWT, database queries, and data processing stay in web app
|
||||
- **Clean Separation**: Web app processes data → Email package renders HTML
|
||||
36
packages/email/emails/auth/forgot-password-email.tsx
Normal file
36
packages/email/emails/auth/forgot-password-email.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailButton } from "../../src/components/email-button";
|
||||
import { EmailFooter } from "../../src/components/email-footer";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface ForgotPasswordEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly verifyLink: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function ForgotPasswordEmail({
|
||||
verifyLink,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: ForgotPasswordEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>{t("emails.forgot_password_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.forgot_password_email_text")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.forgot_password_email_change_password")} />
|
||||
<Text className="text-sm font-bold">{t("emails.forgot_password_email_link_valid_for_24_hours")}</Text>
|
||||
<Text className="mb-0 text-sm">{t("emails.forgot_password_email_did_not_request")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ForgotPasswordEmailPreview(): React.JSX.Element {
|
||||
return <ForgotPasswordEmail {...exampleData.forgotPasswordEmail} />;
|
||||
}
|
||||
40
packages/email/emails/auth/new-email-verification.tsx
Normal file
40
packages/email/emails/auth/new-email-verification.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import { EmailButton } from "../../src/components/email-button";
|
||||
import { EmailFooter } from "../../src/components/email-footer";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface NewEmailVerificationProps extends TEmailTemplateLegalProps {
|
||||
readonly verifyLink: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function NewEmailVerification({
|
||||
verifyLink,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: NewEmailVerificationProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.new_email_verification_text")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_security_notice")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.verification_email_verify_email")} />
|
||||
<Text className="text-sm">{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="text-sm break-all text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NewEmailVerificationPreview(): React.JSX.Element {
|
||||
return <NewEmailVerification {...exampleData.newEmailVerification} />;
|
||||
}
|
||||
30
packages/email/emails/auth/password-reset-notify-email.tsx
Normal file
30
packages/email/emails/auth/password-reset-notify-email.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailFooter } from "../../src/components/email-footer";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface PasswordResetNotifyEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function PasswordResetNotifyEmail({
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: PasswordResetNotifyEmailProps = {}): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>{t("emails.password_changed_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.password_changed_email_text")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PasswordResetNotifyEmailPreview(): React.JSX.Element {
|
||||
return <PasswordResetNotifyEmail {...exampleData.passwordResetNotifyEmail} />;
|
||||
}
|
||||
@@ -1,28 +1,32 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
import { EmailButton } from "../../src/components/email-button";
|
||||
import { EmailFooter } from "../../src/components/email-footer";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface VerificationEmailProps {
|
||||
verifyLink: string;
|
||||
verificationRequestLink: string;
|
||||
interface VerificationEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly verifyLink: string;
|
||||
readonly verificationRequestLink: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export async function VerificationEmail({
|
||||
export function VerificationEmail({
|
||||
verifyLink,
|
||||
verificationRequestLink,
|
||||
}: VerificationEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: VerificationEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<EmailTemplate t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.verification_email_text")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.verification_email_verify_email")} />
|
||||
<Text className="text-sm">{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="break-all text-sm text-black" href={verifyLink}>
|
||||
<Link className="text-sm break-all text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
@@ -38,4 +42,6 @@ export async function VerificationEmail({
|
||||
);
|
||||
}
|
||||
|
||||
export default VerificationEmail;
|
||||
export default function VerificationEmailPreview(): React.JSX.Element {
|
||||
return <VerificationEmail {...exampleData.verificationEmail} />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface EmailCustomizationPreviewEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly userName: string;
|
||||
readonly logoUrl?: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function EmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
logoUrl,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: EmailCustomizationPreviewEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>{t("emails.email_customization_preview_email_heading", { userName })}</Heading>
|
||||
<Text className="text-sm">{t("emails.email_customization_preview_email_text")}</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EmailCustomizationPreviewEmailPreview(): React.JSX.Element {
|
||||
return <EmailCustomizationPreviewEmail {...exampleData.emailCustomizationPreviewEmail} />;
|
||||
}
|
||||
39
packages/email/emails/invite/invite-accepted-email.tsx
Normal file
39
packages/email/emails/invite/invite-accepted-email.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailFooter } from "../../src/components/email-footer";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface InviteAcceptedEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly inviterName: string;
|
||||
readonly inviteeName: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function InviteAcceptedEmail({
|
||||
inviterName,
|
||||
inviteeName,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: InviteAcceptedEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>
|
||||
{t("emails.invite_accepted_email_heading", { inviterName })} {inviterName}
|
||||
</Heading>
|
||||
<Text className="text-sm">
|
||||
{t("emails.invite_accepted_email_text_par1", { inviteeName })} {inviteeName}{" "}
|
||||
{t("emails.invite_accepted_email_text_par2")}
|
||||
</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InviteAcceptedEmailPreview(): React.JSX.Element {
|
||||
return <InviteAcceptedEmail {...exampleData.inviteAcceptedEmail} />;
|
||||
}
|
||||
43
packages/email/emails/invite/invite-email.tsx
Normal file
43
packages/email/emails/invite/invite-email.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailButton } from "../../src/components/email-button";
|
||||
import { EmailFooter } from "../../src/components/email-footer";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface InviteEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly inviteeName: string;
|
||||
readonly inviterName: string;
|
||||
readonly verifyLink: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function InviteEmail({
|
||||
inviteeName,
|
||||
inviterName,
|
||||
verifyLink,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: InviteEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>
|
||||
{t("emails.invite_email_heading", { inviteeName })} {inviteeName}
|
||||
</Heading>
|
||||
<Text className="text-sm">
|
||||
{t("emails.invite_email_text_par1", { inviterName })} {inviterName}{" "}
|
||||
{t("emails.invite_email_text_par2")}
|
||||
</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.invite_email_button_label")} />
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InviteEmailPreview(): React.JSX.Element {
|
||||
return <InviteEmail {...exampleData.inviteEmail} />;
|
||||
}
|
||||
42
packages/email/emails/survey/embed-survey-preview-email.tsx
Normal file
42
packages/email/emails/survey/embed-survey-preview-email.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface EmbedSurveyPreviewEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly html: string;
|
||||
readonly environmentId: string;
|
||||
readonly logoUrl?: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function EmbedSurveyPreviewEmail({
|
||||
html,
|
||||
environmentId,
|
||||
logoUrl,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: EmbedSurveyPreviewEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>{t("emails.embed_survey_preview_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.embed_survey_preview_email_text")}</Text>
|
||||
<Text className="text-sm">
|
||||
<b>{t("emails.embed_survey_preview_email_didnt_request")}</b>{" "}
|
||||
{t("emails.embed_survey_preview_email_fight_spam")}
|
||||
</Text>
|
||||
<div className="text-sm" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<Text className="text-center text-sm text-slate-700">
|
||||
{t("emails.embed_survey_preview_email_environment_id")}: {environmentId}
|
||||
</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EmbedSurveyPreviewEmailPreview(): React.JSX.Element {
|
||||
return <EmbedSurveyPreviewEmail {...exampleData.embedSurveyPreviewEmail} />;
|
||||
}
|
||||
86
packages/email/emails/survey/follow-up-email.tsx
Normal file
86
packages/email/emails/survey/follow-up-email.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Column, Hr, Row, Text } from "@react-email/components";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { renderEmailResponseValue } from "../../src/lib/email-utils";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { ProcessedHiddenField, ProcessedResponseElement, ProcessedVariable } from "../../src/types/follow-up";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
export interface FollowUpEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly body: string; // Already processed HTML with recall tags replaced
|
||||
readonly responseData?: ProcessedResponseElement[]; // Already mapped elements
|
||||
readonly variables?: ProcessedVariable[]; // Already filtered variables
|
||||
readonly hiddenFields?: ProcessedHiddenField[]; // Already filtered hidden fields
|
||||
readonly logoUrl?: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function FollowUpEmail({
|
||||
body,
|
||||
responseData = [],
|
||||
variables = [],
|
||||
hiddenFields = [],
|
||||
logoUrl,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: FollowUpEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t} {...legalProps}>
|
||||
<>
|
||||
<div dangerouslySetInnerHTML={{ __html: body }} />
|
||||
|
||||
{responseData.length > 0 ? (
|
||||
<>
|
||||
<Hr />
|
||||
<Text className="mb-4 text-base font-semibold text-slate-900">{t("emails.response_data")}</Text>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{responseData.map((e) => {
|
||||
if (!e.response) return null;
|
||||
return (
|
||||
<Row key={e.element}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">{e.element}</Text>
|
||||
{renderEmailResponseValue(e.response, e.type, t, true)}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
{variables.map((variable) => (
|
||||
<Row key={variable.id}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">
|
||||
{variable.type === "number"
|
||||
? `${t("emails.number_variable")}: ${variable.name}`
|
||||
: `${t("emails.text_variable")}: ${variable.name}`}
|
||||
</Text>
|
||||
<Text className="mt-0 text-sm break-words whitespace-pre-wrap text-slate-700">
|
||||
{variable.value}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
))}
|
||||
|
||||
{hiddenFields.map((hiddenField) => (
|
||||
<Row key={hiddenField.id}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">
|
||||
{t("emails.hidden_field")}: {hiddenField.id}
|
||||
</Text>
|
||||
<Text className="mt-0 text-sm break-words whitespace-pre-wrap text-slate-700">
|
||||
{hiddenField.value}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
))}
|
||||
</>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FollowUpEmailPreview(): React.JSX.Element {
|
||||
return <FollowUpEmail {...(exampleData.followUpEmail as unknown as FollowUpEmailProps)} />;
|
||||
}
|
||||
43
packages/email/emails/survey/link-survey-email.tsx
Normal file
43
packages/email/emails/survey/link-survey-email.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailButton } from "../../src/components/email-button";
|
||||
import { EmailFooter } from "../../src/components/email-footer";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface LinkSurveyEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly surveyName: string;
|
||||
readonly surveyLink: string;
|
||||
readonly logoUrl?: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function LinkSurveyEmail({
|
||||
surveyName,
|
||||
surveyLink,
|
||||
logoUrl,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: LinkSurveyEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_hey")}</Heading>
|
||||
<Text className="text-sm">{t("emails.verification_email_thanks")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_email_to_fill_survey")}</Text>
|
||||
<EmailButton href={surveyLink} label={t("emails.verification_email_take_survey")} />
|
||||
<Text className="text-sm text-slate-400">
|
||||
{t("emails.verification_email_survey_name")}: {surveyName}
|
||||
</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function LinkSurveyEmailPreview(): React.JSX.Element {
|
||||
return <LinkSurveyEmail {...exampleData.linkSurveyEmail} />;
|
||||
}
|
||||
@@ -2,35 +2,52 @@ import { Column, Container, Heading, Hr, Link, Row, Section, Text } from "@react
|
||||
import { FileDigitIcon, FileType2Icon } from "lucide-react";
|
||||
import type { TOrganization } from "@formbricks/types/organizations";
|
||||
import type { TResponse } from "@formbricks/types/responses";
|
||||
import { type TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { EmailButton } from "../../src/components/email-button";
|
||||
import { EmailTemplate } from "../../src/components/email-template";
|
||||
import { renderEmailResponseValue } from "../../src/lib/email-utils";
|
||||
import { exampleData } from "../../src/lib/example-data";
|
||||
import { t as mockT } from "../../src/lib/mock-translate";
|
||||
import { TEmailTemplateLegalProps } from "../../src/types/email";
|
||||
import { ProcessedResponseElement } from "../../src/types/follow-up";
|
||||
import { TFunction } from "../../src/types/translations";
|
||||
|
||||
interface ResponseFinishedEmailProps {
|
||||
survey: TSurvey;
|
||||
responseCount: number;
|
||||
response: TResponse;
|
||||
WEBAPP_URL: string;
|
||||
environmentId: string;
|
||||
organization: TOrganization;
|
||||
export interface ResponseFinishedEmailProps extends TEmailTemplateLegalProps {
|
||||
readonly survey: TSurvey;
|
||||
readonly responseCount: number;
|
||||
readonly response: TResponse;
|
||||
readonly WEBAPP_URL: string;
|
||||
readonly environmentId: string;
|
||||
readonly organization: TOrganization;
|
||||
readonly elements: ProcessedResponseElement[]; // Pre-processed data, not a function
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export async function ResponseFinishedEmail({
|
||||
const mockGetElementResponseMapping = (survey: TSurvey, response: TResponse) => {
|
||||
// For preview, just return the response data as elements
|
||||
return Object.entries(response.data)
|
||||
.filter(([key]) => !survey.hiddenFields.fieldIds?.includes(key))
|
||||
.map(([key, value]) => ({
|
||||
element: key,
|
||||
response: value as string | string[],
|
||||
type: TSurveyElementTypeEnum.OpenText, // Default type for preview
|
||||
}));
|
||||
};
|
||||
|
||||
export function ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
}: ResponseFinishedEmailProps): Promise<React.JSX.Element> {
|
||||
const elements = getElementResponseMapping(survey, response);
|
||||
const t = await getTranslate();
|
||||
|
||||
elements,
|
||||
t = mockT,
|
||||
...legalProps
|
||||
}: ResponseFinishedEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<EmailTemplate t={t} {...legalProps}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Column>
|
||||
@@ -42,7 +59,7 @@ export async function ResponseFinishedEmail({
|
||||
</Text>
|
||||
<Hr />
|
||||
{elements.map((e) => {
|
||||
if (!e.response) return;
|
||||
if (!e.response) return null;
|
||||
return (
|
||||
<Row key={e.element}>
|
||||
<Column className="w-full font-medium">
|
||||
@@ -58,7 +75,6 @@ export async function ResponseFinishedEmail({
|
||||
if (typeof variableResponse !== "string" && typeof variableResponse !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return variableResponse !== undefined;
|
||||
})
|
||||
.map((variable) => {
|
||||
@@ -158,3 +174,11 @@ function EyeOffIcon(): React.JSX.Element {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function ResponseFinishedEmailPreview(): React.JSX.Element {
|
||||
const { survey, response, ...rest } = exampleData.responseFinishedEmail;
|
||||
const elements = mockGetElementResponseMapping(survey, response);
|
||||
|
||||
return <ResponseFinishedEmail {...rest} survey={survey} response={response} elements={elements} />;
|
||||
}
|
||||
30
packages/email/package.json
Normal file
30
packages/email/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@formbricks/email",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Email templates for Formbricks with React Email preview server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"dev": "email dev --port 3456",
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint src --fix --ext .ts,.tsx",
|
||||
"clean": "rimraf .turbo node_modules dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "1.0.1",
|
||||
"react-email": "5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@react-email/preview-server": "5.0.8",
|
||||
"autoprefixer": "10.4.21",
|
||||
"clsx": "2.1.1",
|
||||
"postcss": "8.5.3",
|
||||
"tailwind-merge": "3.2.0",
|
||||
"tailwindcss": "3.4.17"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Button } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
interface EmailButtonProps {
|
||||
label: string;
|
||||
href: string;
|
||||
readonly label: string;
|
||||
readonly href: string;
|
||||
}
|
||||
|
||||
export function EmailButton({ label, href }: EmailButtonProps): React.JSX.Element {
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Container } from "@react-email/components";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { cn } from "../../src/lib/cn";
|
||||
|
||||
interface ElementHeaderProps {
|
||||
headline: string;
|
||||
subheader?: string;
|
||||
className?: string;
|
||||
readonly headline: string;
|
||||
readonly subheader?: string;
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export function ElementHeader({ headline, subheader, className }: ElementHeaderProps): React.JSX.Element {
|
||||
@@ -21,3 +21,5 @@ export function ElementHeader({ headline, subheader, className }: ElementHeaderP
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ElementHeader;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Text } from "@react-email/components";
|
||||
import { TFunction } from "i18next";
|
||||
import React from "react";
|
||||
import { TFunction } from "../types/translations";
|
||||
|
||||
export function EmailFooter({ t }: { t: TFunction }): React.JSX.Element {
|
||||
return (
|
||||
@@ -1,22 +1,24 @@
|
||||
import { Body, Container, Html, Img, Link, Section, Tailwind, Text } from "@react-email/components";
|
||||
import { TFunction } from "i18next";
|
||||
import React from "react";
|
||||
import { FB_LOGO_URL, IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@/lib/constants";
|
||||
import { TEmailTemplateLegalProps } from "../types/email";
|
||||
import { TFunction } from "../types/translations";
|
||||
|
||||
const fbLogoUrl = FB_LOGO_URL;
|
||||
const fbLogoUrl = "https://app.formbricks.com/logo-transparent.png";
|
||||
const logoLink = "https://formbricks.com?utm_source=email_header&utm_medium=email";
|
||||
|
||||
interface EmailTemplateProps {
|
||||
interface EmailTemplateProps extends TEmailTemplateLegalProps {
|
||||
readonly children: React.ReactNode;
|
||||
readonly logoUrl?: string;
|
||||
readonly t: TFunction;
|
||||
}
|
||||
|
||||
export async function EmailTemplate({
|
||||
export function EmailTemplate({
|
||||
children,
|
||||
logoUrl,
|
||||
t,
|
||||
}: EmailTemplateProps): Promise<React.JSX.Element> {
|
||||
privacyUrl,
|
||||
imprintUrl,
|
||||
imprintAddress,
|
||||
}: EmailTemplateProps): React.JSX.Element {
|
||||
const isDefaultLogo = !logoUrl || logoUrl === fbLogoUrl;
|
||||
|
||||
return (
|
||||
@@ -53,23 +55,23 @@ export async function EmailTemplate({
|
||||
rel="noopener noreferrer">
|
||||
{t("emails.email_template_text_1")}
|
||||
</Link>
|
||||
{IMPRINT_ADDRESS && (
|
||||
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
{imprintAddress && (
|
||||
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">{imprintAddress}</Text>
|
||||
)}
|
||||
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">
|
||||
{IMPRINT_URL && (
|
||||
{imprintUrl && (
|
||||
<Link
|
||||
href={IMPRINT_URL}
|
||||
href={imprintUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-slate-500">
|
||||
{t("emails.imprint")}
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && " • "}
|
||||
{PRIVACY_URL && (
|
||||
{imprintUrl && privacyUrl && " • "}
|
||||
{privacyUrl && (
|
||||
<Link
|
||||
href={PRIVACY_URL}
|
||||
href={privacyUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-slate-500">
|
||||
@@ -83,3 +85,5 @@ export async function EmailTemplate({
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailTemplate;
|
||||
52
packages/email/src/index.ts
Normal file
52
packages/email/src/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export { VerificationEmail } from "../emails/auth/verification-email";
|
||||
export { ForgotPasswordEmail } from "../emails/auth/forgot-password-email";
|
||||
export { NewEmailVerification } from "../emails/auth/new-email-verification";
|
||||
export { PasswordResetNotifyEmail } from "../emails/auth/password-reset-notify-email";
|
||||
export { InviteEmail } from "../emails/invite/invite-email";
|
||||
export { InviteAcceptedEmail } from "../emails/invite/invite-accepted-email";
|
||||
export { LinkSurveyEmail } from "../emails/survey/link-survey-email";
|
||||
export { EmbedSurveyPreviewEmail } from "../emails/survey/embed-survey-preview-email";
|
||||
export { ResponseFinishedEmail } from "../emails/survey/response-finished-email";
|
||||
export { EmailCustomizationPreviewEmail } from "../emails/general/email-customization-preview-email";
|
||||
export { FollowUpEmail } from "../emails/survey/follow-up-email";
|
||||
|
||||
export { EmailButton } from "./components/email-button";
|
||||
export { EmailFooter } from "./components/email-footer";
|
||||
export { EmailTemplate } from "./components/email-template";
|
||||
export { ElementHeader } from "./components/email-element-header";
|
||||
|
||||
export {
|
||||
renderVerificationEmail,
|
||||
renderForgotPasswordEmail,
|
||||
renderNewEmailVerification,
|
||||
renderPasswordResetNotifyEmail,
|
||||
renderInviteEmail,
|
||||
renderInviteAcceptedEmail,
|
||||
renderLinkSurveyEmail,
|
||||
renderEmbedSurveyPreviewEmail,
|
||||
renderResponseFinishedEmail,
|
||||
renderEmailCustomizationPreviewEmail,
|
||||
renderFollowUpEmail,
|
||||
} from "./lib/render";
|
||||
|
||||
export { render } from "@react-email/render";
|
||||
|
||||
export {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type { ProcessedHiddenField, ProcessedResponseElement, ProcessedVariable } from "./types/follow-up";
|
||||
6
packages/email/src/lib/cn.ts
Normal file
6
packages/email/src/lib/cn.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => {
|
||||
return twMerge(clsx(inputs));
|
||||
};
|
||||
@@ -1,15 +1,26 @@
|
||||
import { Column, Container, Img, Link, Row, Text } from "@react-email/components";
|
||||
import { TFunction } from "i18next";
|
||||
import { FileIcon } from "lucide-react";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { getOriginalFileNameFromUrl } from "@/modules/storage/utils";
|
||||
import { TFunction } from "../types/translations";
|
||||
|
||||
export const renderEmailResponseValue = async (
|
||||
// Simplified version - just get the filename from URL
|
||||
const getOriginalFileNameFromUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathname = urlObj.pathname;
|
||||
const filename = pathname.split("/").pop() || "file";
|
||||
return decodeURIComponent(filename);
|
||||
} catch {
|
||||
return url.split("/").pop() || "file";
|
||||
}
|
||||
};
|
||||
|
||||
export const renderEmailResponseValue = (
|
||||
response: string | string[],
|
||||
questionType: TSurveyElementTypeEnum,
|
||||
t: TFunction,
|
||||
overrideFileUploadResponse = false
|
||||
): Promise<React.JSX.Element> => {
|
||||
): React.JSX.Element => {
|
||||
switch (questionType) {
|
||||
case TSurveyElementTypeEnum.FileUpload:
|
||||
return (
|
||||
@@ -65,6 +76,6 @@ export const renderEmailResponseValue = async (
|
||||
);
|
||||
|
||||
default:
|
||||
return <Text className="mt-0 text-sm break-words whitespace-pre-wrap">{response}</Text>;
|
||||
return <Text className="mt-0 text-sm break-words whitespace-pre-wrap">{response as string}</Text>;
|
||||
}
|
||||
};
|
||||
184
packages/email/src/lib/example-data.ts
Normal file
184
packages/email/src/lib/example-data.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
// Mock data for email templates to use in React Email preview server
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const exampleData = {
|
||||
verificationEmail: {
|
||||
verifyLink: "https://app.formbricks.com/auth/verify?token=example-verification-token",
|
||||
verificationRequestLink: "https://app.formbricks.com/auth/verification-requested",
|
||||
},
|
||||
|
||||
forgotPasswordEmail: {
|
||||
verifyLink: "https://app.formbricks.com/auth/forgot-password/reset?token=example-reset-token",
|
||||
},
|
||||
|
||||
newEmailVerification: {
|
||||
verifyLink: "https://app.formbricks.com/verify-email-change?token=example-email-change-token",
|
||||
},
|
||||
|
||||
passwordResetNotifyEmail: {
|
||||
// No props needed
|
||||
},
|
||||
|
||||
inviteEmail: {
|
||||
inviteeName: "Jane Smith",
|
||||
inviterName: "John Doe",
|
||||
verifyLink: "https://app.formbricks.com/invite?token=example-invite-token",
|
||||
},
|
||||
|
||||
inviteAcceptedEmail: {
|
||||
inviterName: "John Doe",
|
||||
inviteeName: "Jane Smith",
|
||||
},
|
||||
|
||||
linkSurveyEmail: {
|
||||
surveyName: "Customer Satisfaction Survey",
|
||||
surveyLink:
|
||||
"https://app.formbricks.com/s/example-survey-id?verify=example-token&suId=example-single-use-id",
|
||||
},
|
||||
|
||||
embedSurveyPreviewEmail: {
|
||||
html: '<div style="padding: 20px; background-color: #f3f4f6; border-radius: 8px;"><h3 style="margin-top: 0;">Example Survey Embed</h3><p>This is a preview of how your survey will look when embedded in an email.</p></div>',
|
||||
environmentId: "clxyz123456789",
|
||||
},
|
||||
|
||||
responseFinishedEmail: {
|
||||
survey: {
|
||||
id: "survey-123",
|
||||
name: "Customer Feedback Survey",
|
||||
variables: [
|
||||
{
|
||||
id: "var-1",
|
||||
name: "Customer ID",
|
||||
type: "text" as const,
|
||||
},
|
||||
],
|
||||
hiddenFields: {
|
||||
enabled: true,
|
||||
fieldIds: ["userId"],
|
||||
},
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: "openText" as const,
|
||||
headline: { default: "What did you like most?" },
|
||||
required: true,
|
||||
inputType: "text" as const,
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: "rating" as const,
|
||||
headline: { default: "How would you rate your experience?" },
|
||||
required: true,
|
||||
scale: "number" as const,
|
||||
range: 5,
|
||||
},
|
||||
],
|
||||
endings: [],
|
||||
styling: {},
|
||||
createdBy: null,
|
||||
} as unknown as TSurvey,
|
||||
responseCount: 15,
|
||||
response: {
|
||||
id: "response-123",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey-123",
|
||||
finished: true,
|
||||
data: {
|
||||
q1: "The customer service was excellent!",
|
||||
q2: 5,
|
||||
userId: "user-abc-123",
|
||||
},
|
||||
variables: {
|
||||
"var-1": "CUST-456",
|
||||
},
|
||||
contactAttributes: {
|
||||
email: "customer@example.com",
|
||||
},
|
||||
meta: {
|
||||
userAgent: {},
|
||||
url: "https://example.com",
|
||||
},
|
||||
tags: [],
|
||||
notes: [],
|
||||
ttc: {},
|
||||
singleUseId: null,
|
||||
language: "default",
|
||||
displayId: null,
|
||||
} as unknown as TResponse,
|
||||
WEBAPP_URL: "https://app.formbricks.com",
|
||||
environmentId: "env-123",
|
||||
organization: {
|
||||
id: "org-123",
|
||||
name: "Acme Corporation",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
subscriptionStatus: null,
|
||||
features: {
|
||||
inAppSurvey: { status: "active" as const, unlimited: true },
|
||||
linkSurvey: { status: "active" as const, unlimited: true },
|
||||
userTargeting: { status: "active" as const, unlimited: true },
|
||||
},
|
||||
limits: {
|
||||
monthly: {
|
||||
responses: 1000,
|
||||
miu: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
isAIEnabled: false,
|
||||
} as unknown as TOrganization,
|
||||
},
|
||||
|
||||
followUpEmail: {
|
||||
body: "<p>Thank you for your feedback! We've received your response and will review it shortly.</p><p>Here's a summary of what you submitted:</p>",
|
||||
responseData: [
|
||||
{
|
||||
element: "What did you like most?",
|
||||
response: "The customer service was excellent!",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
},
|
||||
{
|
||||
element: "How would you rate your experience?",
|
||||
response: "5",
|
||||
type: TSurveyElementTypeEnum.Rating,
|
||||
},
|
||||
],
|
||||
variables: [
|
||||
{
|
||||
id: "var-1",
|
||||
name: "Customer ID",
|
||||
type: "text",
|
||||
value: "CUST-456",
|
||||
},
|
||||
],
|
||||
hiddenFields: [
|
||||
{
|
||||
id: "userId",
|
||||
value: "user-abc-123",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
emailCustomizationPreviewEmail: {
|
||||
userName: "Alex Johnson",
|
||||
},
|
||||
|
||||
legalProps: {
|
||||
privacyUrl: "https://formbricks.com/privacy",
|
||||
termsUrl: "https://formbricks.com/terms",
|
||||
imprintUrl: "https://formbricks.com/imprint",
|
||||
imprintAddress: "Formbricks GmbH, Example Street 123, 12345 Berlin, Germany",
|
||||
},
|
||||
};
|
||||
|
||||
export type ExampleDataKeys = keyof typeof exampleData;
|
||||
export type ExampleData<K extends ExampleDataKeys> = (typeof exampleData)[K];
|
||||
108
packages/email/src/lib/mock-translate.ts
Normal file
108
packages/email/src/lib/mock-translate.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// Mock translation function for React Email preview server
|
||||
// Returns English strings extracted from apps/web/locales/en-US.json
|
||||
|
||||
type TranslationKey = string;
|
||||
type TranslationValue = string;
|
||||
|
||||
const translations: Record<TranslationKey, TranslationValue> = {
|
||||
"emails.accept": "Accept",
|
||||
"emails.click_or_drag_to_upload_files": "Click or drag to upload files.",
|
||||
"emails.email_customization_preview_email_heading": "Hey {userName}",
|
||||
"emails.email_customization_preview_email_subject": "Formbricks Email Customization Preview",
|
||||
"emails.email_customization_preview_email_text":
|
||||
"This is an email preview to show you which logo will be rendered in the emails.",
|
||||
"emails.email_footer_text_1": "Have a great day!",
|
||||
"emails.email_footer_text_2": "The Formbricks Team",
|
||||
"emails.email_template_text_1": "This email was sent via Formbricks.",
|
||||
"emails.embed_survey_preview_email_didnt_request": "Didn't request this?",
|
||||
"emails.embed_survey_preview_email_environment_id": "Environment ID",
|
||||
"emails.embed_survey_preview_email_fight_spam":
|
||||
"Help us fight spam and forward this mail to hola@formbricks.com",
|
||||
"emails.embed_survey_preview_email_heading": "Preview Email Embed",
|
||||
"emails.embed_survey_preview_email_subject": "Formbricks Email Survey Preview",
|
||||
"emails.embed_survey_preview_email_text": "This is how the code snippet looks embedded into an email:",
|
||||
"emails.forgot_password_email_change_password": "Change password",
|
||||
"emails.forgot_password_email_did_not_request": "If you didn't request this, please ignore this email.",
|
||||
"emails.forgot_password_email_heading": "Change password",
|
||||
"emails.forgot_password_email_link_valid_for_24_hours": "The link is valid for 24 hours.",
|
||||
"emails.forgot_password_email_subject": "Reset your Formbricks password",
|
||||
"emails.forgot_password_email_text":
|
||||
"You have requested a link to change your password. You can do this by clicking the link below:",
|
||||
"emails.hidden_field": "Hidden field",
|
||||
"emails.invite_accepted_email_heading": "Hey",
|
||||
"emails.invite_accepted_email_subject": "You've got a new organization member!",
|
||||
"emails.invite_accepted_email_text_par1": "Just letting you know that",
|
||||
"emails.invite_accepted_email_text_par2": "accepted your invitation. Have fun collaborating!",
|
||||
"emails.invite_email_button_label": "Join organization",
|
||||
"emails.invite_email_heading": "Hey",
|
||||
"emails.invite_email_text_par1": "Your colleague",
|
||||
"emails.invite_email_text_par2":
|
||||
"invited you to join them at Formbricks. To accept the invitation, please click the link below:",
|
||||
"emails.invite_member_email_subject": "You're invited to collaborate on Formbricks!",
|
||||
"emails.new_email_verification_text": "To verify your new email address, please click the button below:",
|
||||
"emails.number_variable": "Number variable",
|
||||
"emails.password_changed_email_heading": "Password changed",
|
||||
"emails.password_changed_email_text": "Your password has been changed successfully.",
|
||||
"emails.password_reset_notify_email_subject": "Your Formbricks password has been changed",
|
||||
"emails.reject": "Reject",
|
||||
"emails.render_email_response_value_file_upload_response_link_not_included":
|
||||
"Link to uploaded file is not included for data privacy reasons",
|
||||
"emails.response_data": "Response data",
|
||||
"emails.response_finished_email_subject": "A response for {surveyName} was completed ✅",
|
||||
"emails.response_finished_email_subject_with_email":
|
||||
"{personEmail} just completed your {surveyName} survey ✅",
|
||||
"emails.schedule_your_meeting": "Schedule your meeting",
|
||||
"emails.select_a_date": "Select a date",
|
||||
"emails.survey_response_finished_email_congrats":
|
||||
"Congrats, you received a new response to your survey! Someone just completed your survey: {surveyName}",
|
||||
"emails.survey_response_finished_email_dont_want_notifications": "Don't want to get these notifications?",
|
||||
"emails.survey_response_finished_email_hey": "Hey 👋",
|
||||
"emails.survey_response_finished_email_turn_off_notifications_for_all_new_forms":
|
||||
"Turn off notifications for all newly created forms",
|
||||
"emails.survey_response_finished_email_turn_off_notifications_for_this_form":
|
||||
"Turn off notifications for this form",
|
||||
"emails.survey_response_finished_email_view_more_responses": "View {responseCount} more responses",
|
||||
"emails.survey_response_finished_email_view_survey_summary": "View survey summary",
|
||||
"emails.text_variable": "Text variable",
|
||||
"emails.verification_email_click_on_this_link": "You can also click on this link:",
|
||||
"emails.verification_email_heading": "Almost there!",
|
||||
"emails.verification_email_hey": "Hey 👋",
|
||||
"emails.verification_email_if_expired_request_new_token":
|
||||
"If it has expired please request a new token here:",
|
||||
"emails.verification_email_link_valid_for_24_hours": "The link is valid for 24 hours.",
|
||||
"emails.verification_email_request_new_verification": "Request new verification",
|
||||
"emails.verification_email_subject": "Please verify your email to use Formbricks",
|
||||
"emails.verification_email_survey_name": "Survey name",
|
||||
"emails.verification_email_take_survey": "Take survey",
|
||||
"emails.verification_email_text": "To start using Formbricks please verify your email below:",
|
||||
"emails.verification_email_thanks": "Thanks for validating your email!",
|
||||
"emails.verification_email_to_fill_survey": "To fill out the survey please click on the button below:",
|
||||
"emails.verification_email_verify_email": "Verify email",
|
||||
"emails.verification_new_email_subject": "Email change verification",
|
||||
"emails.verification_security_notice":
|
||||
"If you did not request this email change, please ignore this email or contact support immediately.",
|
||||
"emails.verified_link_survey_email_subject": "Your survey is ready to be filled out.",
|
||||
};
|
||||
|
||||
// Simple string replacement for placeholders like {userName}, {surveyName}, etc.
|
||||
const replacePlaceholders = (text: string, replacements?: Record<string, string>): string => {
|
||||
if (!replacements) return text;
|
||||
|
||||
let result = text;
|
||||
Object.entries(replacements).forEach(([key, value]) => {
|
||||
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock translation function for preview server
|
||||
* @param key - Translation key (e.g., "emails.forgot_password_email_heading")
|
||||
* @param replacements - Optional object with placeholder replacements
|
||||
* @returns Translated string with placeholders replaced
|
||||
*/
|
||||
export const t = (key: string, replacements?: Record<string, string>): string => {
|
||||
const translation = translations[key] || key;
|
||||
return replacePlaceholders(translation, replacements);
|
||||
};
|
||||
116
packages/email/src/lib/render.ts
Normal file
116
packages/email/src/lib/render.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { render } from "@react-email/render";
|
||||
import { ForgotPasswordEmail } from "../../emails/auth/forgot-password-email";
|
||||
import { NewEmailVerification } from "../../emails/auth/new-email-verification";
|
||||
import { PasswordResetNotifyEmail } from "../../emails/auth/password-reset-notify-email";
|
||||
import { VerificationEmail } from "../../emails/auth/verification-email";
|
||||
import { EmailCustomizationPreviewEmail } from "../../emails/general/email-customization-preview-email";
|
||||
import { InviteAcceptedEmail } from "../../emails/invite/invite-accepted-email";
|
||||
import { InviteEmail } from "../../emails/invite/invite-email";
|
||||
import { EmbedSurveyPreviewEmail } from "../../emails/survey/embed-survey-preview-email";
|
||||
import { FollowUpEmail, FollowUpEmailProps } from "../../emails/survey/follow-up-email";
|
||||
import { LinkSurveyEmail } from "../../emails/survey/link-survey-email";
|
||||
import {
|
||||
ResponseFinishedEmail,
|
||||
ResponseFinishedEmailProps,
|
||||
} from "../../emails/survey/response-finished-email";
|
||||
import { TEmailTemplateLegalProps } from "../types/email";
|
||||
import { TFunction } from "../types/translations";
|
||||
|
||||
export async function renderVerificationEmail(
|
||||
props: {
|
||||
verifyLink: string;
|
||||
verificationRequestLink: string;
|
||||
t: TFunction;
|
||||
} & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(VerificationEmail(props));
|
||||
}
|
||||
|
||||
export async function renderForgotPasswordEmail(
|
||||
props: {
|
||||
verifyLink: string;
|
||||
t: TFunction;
|
||||
} & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(ForgotPasswordEmail(props));
|
||||
}
|
||||
|
||||
export async function renderNewEmailVerification(
|
||||
props: {
|
||||
verifyLink: string;
|
||||
t: TFunction;
|
||||
} & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(NewEmailVerification(props));
|
||||
}
|
||||
|
||||
export async function renderPasswordResetNotifyEmail(
|
||||
props: { t: TFunction } & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(PasswordResetNotifyEmail(props));
|
||||
}
|
||||
|
||||
export async function renderInviteEmail(
|
||||
props: {
|
||||
inviteeName: string;
|
||||
inviterName: string;
|
||||
verifyLink: string;
|
||||
t: TFunction;
|
||||
} & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(InviteEmail(props));
|
||||
}
|
||||
|
||||
export async function renderInviteAcceptedEmail(
|
||||
props: {
|
||||
inviterName: string;
|
||||
inviteeName: string;
|
||||
t: TFunction;
|
||||
} & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(InviteAcceptedEmail(props));
|
||||
}
|
||||
|
||||
export async function renderLinkSurveyEmail(
|
||||
props: {
|
||||
surveyName: string;
|
||||
surveyLink: string;
|
||||
logoUrl: string;
|
||||
t: TFunction;
|
||||
} & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(LinkSurveyEmail(props));
|
||||
}
|
||||
|
||||
export async function renderEmbedSurveyPreviewEmail(
|
||||
props: {
|
||||
html: string;
|
||||
environmentId: string;
|
||||
logoUrl?: string;
|
||||
t: TFunction;
|
||||
} & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(EmbedSurveyPreviewEmail(props));
|
||||
}
|
||||
|
||||
export async function renderResponseFinishedEmail(
|
||||
props: ResponseFinishedEmailProps & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(ResponseFinishedEmail(props));
|
||||
}
|
||||
|
||||
export async function renderEmailCustomizationPreviewEmail(
|
||||
props: {
|
||||
userName: string;
|
||||
logoUrl?: string;
|
||||
t: TFunction;
|
||||
} & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(EmailCustomizationPreviewEmail(props));
|
||||
}
|
||||
|
||||
export async function renderFollowUpEmail(
|
||||
props: FollowUpEmailProps & TEmailTemplateLegalProps
|
||||
): Promise<string> {
|
||||
return await render(FollowUpEmail(props));
|
||||
}
|
||||
6
packages/email/src/types/email.ts
Normal file
6
packages/email/src/types/email.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface TEmailTemplateLegalProps {
|
||||
privacyUrl?: string;
|
||||
termsUrl?: string;
|
||||
imprintUrl?: string;
|
||||
imprintAddress?: string;
|
||||
}
|
||||
19
packages/email/src/types/follow-up.ts
Normal file
19
packages/email/src/types/follow-up.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
|
||||
export interface ProcessedResponseElement {
|
||||
element: string;
|
||||
response: string | string[];
|
||||
type: TSurveyElementTypeEnum;
|
||||
}
|
||||
|
||||
export interface ProcessedVariable {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "text" | "number";
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface ProcessedHiddenField {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
1
packages/email/src/types/translations.ts
Normal file
1
packages/email/src/types/translations.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
5
packages/email/tailwind.config.js
Normal file
5
packages/email/tailwind.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{ts,tsx}", "./emails/**/*.{ts,tsx}"],
|
||||
plugins: [],
|
||||
};
|
||||
10
packages/email/tsconfig.json
Normal file
10
packages/email/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"extends": "@formbricks/config-typescript/react-library.json",
|
||||
"include": ["src/**/*", "emails/**/*"]
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const __dirname = dirname(__filename);
|
||||
|
||||
// Configuration for Web App
|
||||
const WEB_APP_DIR = path.join(__dirname, "..", "..", "..", "apps", "web");
|
||||
const EMAIL_PKG_DIR = path.join(__dirname, "..", "..", "..", "packages", "email");
|
||||
const WEB_APP_LOCALES_DIR = path.join(WEB_APP_DIR, "locales");
|
||||
const WEB_APP_DEFAULT_LOCALE = "en-US";
|
||||
|
||||
@@ -139,27 +140,32 @@ export function extractKeysFromContent(content: string): string[] {
|
||||
/**
|
||||
* Scan source files for translation keys
|
||||
*/
|
||||
async function scanSourceFiles(sourceDir: string, packageName: string): Promise<Set<string>> {
|
||||
async function scanSourceFiles(sourceDirs: string | string[], packageName: string): Promise<Set<string>> {
|
||||
console.log(`🔍 Scanning ${packageName} source files for translation keys...`);
|
||||
|
||||
const usedKeys = new Set<string>();
|
||||
const dirs = Array.isArray(sourceDirs) ? sourceDirs : [sourceDirs];
|
||||
|
||||
// Find all TypeScript and TypeScript React files
|
||||
const files = await glob("**/*.{ts,tsx}", {
|
||||
cwd: sourceDir,
|
||||
ignore: EXCLUDE_DIRS,
|
||||
absolute: true,
|
||||
});
|
||||
for (const dir of dirs) {
|
||||
// Find all TypeScript and TypeScript React files
|
||||
const files = await glob("**/*.{ts,tsx}", {
|
||||
cwd: dir,
|
||||
ignore: EXCLUDE_DIRS,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
console.log(` Found ${files.length.toString()} files to scan`);
|
||||
console.log(
|
||||
` Found ${files.length.toString()} files to scan in ${path.relative(path.join(__dirname, "..", "..", ".."), dir)}`
|
||||
);
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await fs.promises.readFile(file, "utf-8");
|
||||
const keys = extractKeysFromContent(content);
|
||||
keys.forEach((key) => usedKeys.add(key));
|
||||
} catch (error) {
|
||||
console.error(`❌ Error: Could not read file ${file}:`, error);
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await fs.promises.readFile(file, "utf-8");
|
||||
const keys = extractKeysFromContent(content);
|
||||
keys.forEach((key) => usedKeys.add(key));
|
||||
} catch (error) {
|
||||
console.error(`❌ Error: Could not read file ${file}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,13 +429,13 @@ function displayResults(results: ScanResults, packageName: string, defaultLocale
|
||||
* Validate translations for a single package
|
||||
*/
|
||||
async function validatePackage(
|
||||
sourceDir: string,
|
||||
sourceDirs: string | string[],
|
||||
localesDir: string,
|
||||
defaultLocale: string,
|
||||
packageName: string
|
||||
): Promise<ScanResults> {
|
||||
// Scan source files for used keys
|
||||
const usedKeys = await scanSourceFiles(sourceDir, packageName);
|
||||
const usedKeys = await scanSourceFiles(sourceDirs, packageName);
|
||||
|
||||
// Load translation keys from all locale files
|
||||
const translationsByLocale = await loadAllTranslationKeys(localesDir, defaultLocale, packageName);
|
||||
@@ -461,7 +467,7 @@ async function main(): Promise<void> {
|
||||
try {
|
||||
// Validate Web App
|
||||
const webAppResults = await validatePackage(
|
||||
WEB_APP_DIR,
|
||||
[WEB_APP_DIR, EMAIL_PKG_DIR],
|
||||
WEB_APP_LOCALES_DIR,
|
||||
WEB_APP_DEFAULT_LOCALE,
|
||||
"Web App"
|
||||
|
||||
5413
pnpm-lock.yaml
generated
5413
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user