fix: emails font size (#6228)

This commit is contained in:
Piyush Gupta
2025-07-15 19:07:13 +05:30
committed by GitHub
parent f06d48698a
commit b3a1f24683
27 changed files with 269 additions and 178 deletions

View File

@@ -9,6 +9,11 @@ vi.mock("@/lib/utils/recall", () => ({
vi.mock("./i18n/utils", () => ({
getLocalizedValue: vi.fn((obj, lang) => obj[lang] || obj.default),
getLanguageCode: vi.fn((surveyLanguages, languageCode) => {
if (!surveyLanguages?.length || !languageCode) return null; // Changed from "default" to null
const language = surveyLanguages.find((surveyLanguage) => surveyLanguage.language.code === languageCode);
return language?.default ? "default" : language?.language.code || "default";
}),
}));
describe("Response Processing", () => {
@@ -43,6 +48,16 @@ describe("Response Processing", () => {
test("should return empty string for unsupported types", () => {
expect(processResponseData(undefined as any)).toBe("");
});
test("should filter out null values from array", () => {
const input = ["a", null, "c"] as any;
expect(processResponseData(input)).toBe("a; c");
});
test("should filter out undefined values from array", () => {
const input = ["a", undefined, "c"] as any;
expect(processResponseData(input)).toBe("a; c");
});
});
describe("convertResponseValue", () => {
@@ -125,6 +140,22 @@ describe("Response Processing", () => {
expect(convertResponseValue("invalid", mockPictureSelectionQuestion)).toEqual([]);
});
test("should handle pictureSelection type with number input", () => {
expect(convertResponseValue(42, mockPictureSelectionQuestion)).toEqual([]);
});
test("should handle pictureSelection type with object input", () => {
expect(convertResponseValue({ key: "value" }, mockPictureSelectionQuestion)).toEqual([]);
});
test("should handle pictureSelection type with null input", () => {
expect(convertResponseValue(null as any, mockPictureSelectionQuestion)).toEqual([]);
});
test("should handle pictureSelection type with undefined input", () => {
expect(convertResponseValue(undefined as any, mockPictureSelectionQuestion)).toEqual([]);
});
test("should handle default case with string input", () => {
expect(convertResponseValue("answer", mockOpenTextQuestion)).toBe("answer");
});
@@ -320,6 +351,32 @@ describe("Response Processing", () => {
charLimit: { enabled: false },
},
],
languages: [
{
language: {
id: "lang1",
code: "default",
createdAt: new Date(),
updatedAt: new Date(),
alias: null,
projectId: "proj1",
},
default: true,
enabled: true,
},
{
language: {
id: "lang2",
code: "en",
createdAt: new Date(),
updatedAt: new Date(),
alias: null,
projectId: "proj1",
},
default: false,
enabled: true,
},
],
};
const response = {
id: "response1",
@@ -349,5 +406,102 @@ describe("Response Processing", () => {
const mapping = getQuestionResponseMapping(survey, response);
expect(mapping[0].question).toBe("Question 1 EN");
});
test("should handle null response language", () => {
const response = {
id: "response1",
surveyId: "survey1",
createdAt: new Date(),
updatedAt: new Date(),
finished: true,
data: { q1: "Answer 1" },
language: null,
meta: {
url: undefined,
country: undefined,
action: undefined,
source: undefined,
userAgent: undefined,
},
notes: [],
tags: [],
person: null,
personAttributes: {},
ttc: {},
variables: {},
contact: null,
contactAttributes: {},
singleUseId: null,
};
const mapping = getQuestionResponseMapping(mockSurvey, response);
expect(mapping).toHaveLength(2);
expect(mapping[0].question).toBe("Question 1");
});
test("should handle undefined response language", () => {
const response = {
id: "response1",
surveyId: "survey1",
createdAt: new Date(),
updatedAt: new Date(),
finished: true,
data: { q1: "Answer 1" },
language: null,
meta: {
url: undefined,
country: undefined,
action: undefined,
source: undefined,
userAgent: undefined,
},
notes: [],
tags: [],
person: null,
personAttributes: {},
ttc: {},
variables: {},
contact: null,
contactAttributes: {},
singleUseId: null,
};
const mapping = getQuestionResponseMapping(mockSurvey, response);
expect(mapping).toHaveLength(2);
expect(mapping[0].question).toBe("Question 1");
});
test("should handle empty survey languages", () => {
const survey = {
...mockSurvey,
languages: [], // Empty languages array
};
const response = {
id: "response1",
surveyId: "survey1",
createdAt: new Date(),
updatedAt: new Date(),
finished: true,
data: { q1: "Answer 1" },
language: "en",
meta: {
url: undefined,
country: undefined,
action: undefined,
source: undefined,
userAgent: undefined,
},
notes: [],
tags: [],
person: null,
personAttributes: {},
ttc: {},
variables: {},
contact: null,
contactAttributes: {},
singleUseId: null,
};
const mapping = getQuestionResponseMapping(survey, response);
expect(mapping).toHaveLength(2);
expect(mapping[0].question).toBe("Question 1"); // Should fallback to default
});
});
});

View File

@@ -1,7 +1,7 @@
import { parseRecallInfo } from "@/lib/utils/recall";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys/types";
import { getLocalizedValue } from "./i18n/utils";
import { getLanguageCode, getLocalizedValue } from "./i18n/utils";
// function to convert response value of type string | number | string[] or Record<string, string> to string | string[]
export const convertResponseValue = (
@@ -39,12 +39,14 @@ export const getQuestionResponseMapping = (
response: string | string[];
type: TSurveyQuestionType;
}[] = [];
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
for (const question of survey.questions) {
const answer = response.data[question.id];
questionResponseMapping.push({
question: parseRecallInfo(
getLocalizedValue(question.headline, response.language ?? "default"),
getLocalizedValue(question.headline, responseLanguageCode ?? "default"),
response.data
),
response: convertResponseValue(answer, question),

View File

@@ -8,7 +8,7 @@ interface EmailButtonProps {
export function EmailButton({ label, href }: EmailButtonProps): React.JSX.Element {
return (
<Button className="rounded-md bg-black px-6 py-3 text-white" href={href}>
<Button className="rounded-md bg-black px-6 py-3 text-sm text-white" href={href}>
{label}
</Button>
);

View File

@@ -4,7 +4,7 @@ import React from "react";
export function EmailFooter({ t }: { t: TFnType }): React.JSX.Element {
return (
<Text>
<Text className="text-sm">
{t("emails.email_footer_text_1")}
<br />
{t("emails.email_footer_text_2")}

View File

@@ -23,7 +23,7 @@ export async function EmailTemplate({
<Html>
<Tailwind>
<Body
className="m-0 h-full w-full justify-center bg-slate-50 p-6 text-center text-base font-medium text-slate-800"
className="m-0 h-full w-full justify-center bg-slate-50 p-6 text-center text-sm text-slate-800"
style={{
fontFamily: "'Jost', 'Helvetica Neue', 'Segoe UI', 'Helvetica', 'sans-serif'",
}}>
@@ -47,24 +47,32 @@ export async function EmailTemplate({
<Section className="mt-4 text-center text-sm">
<Link
className="m-0 font-normal text-slate-500"
className="m-0 text-sm font-normal text-slate-500"
href="https://formbricks.com/?utm_source=email_header&utm_medium=email"
target="_blank"
rel="noopener noreferrer">
{t("emails.email_template_text_1")}
</Link>
{IMPRINT_ADDRESS && (
<Text className="m-0 font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
)}
<Text className="m-0 font-normal text-slate-500 opacity-50">
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">
{IMPRINT_URL && (
<Link href={IMPRINT_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
<Link
href={IMPRINT_URL}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-slate-500">
{t("emails.imprint")}
</Link>
)}
{IMPRINT_URL && PRIVACY_URL && " • "}
{PRIVACY_URL && (
<Link href={PRIVACY_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
<Link
href={PRIVACY_URL}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-slate-500">
{t("emails.privacy_policy")}
</Link>
)}

View File

@@ -17,10 +17,10 @@ export async function ForgotPasswordEmail({
<EmailTemplate t={t}>
<Container>
<Heading>{t("emails.forgot_password_email_heading")}</Heading>
<Text>{t("emails.forgot_password_email_text")}</Text>
<Text className="text-sm">{t("emails.forgot_password_email_text")}</Text>
<EmailButton href={verifyLink} label={t("emails.forgot_password_email_change_password")} />
<Text className="font-bold">{t("emails.forgot_password_email_link_valid_for_24_hours")}</Text>
<Text className="mb-0">{t("emails.forgot_password_email_did_not_request")}</Text>
<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>

View File

@@ -17,14 +17,14 @@ export async function NewEmailVerification({
<EmailTemplate t={t}>
<Container>
<Heading>{t("emails.verification_email_heading")}</Heading>
<Text>{t("emails.new_email_verification_text")}</Text>
<Text>{t("emails.verification_security_notice")}</Text>
<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>{t("emails.verification_email_click_on_this_link")}</Text>
<Link className="break-all text-black" href={verifyLink}>
<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="font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
<EmailFooter t={t} />
</Container>
</EmailTemplate>

View File

@@ -10,7 +10,7 @@ export async function PasswordResetNotifyEmail(): Promise<React.JSX.Element> {
<EmailTemplate t={t}>
<Container>
<Heading>{t("emails.password_changed_email_heading")}</Heading>
<Text>{t("emails.password_changed_email_text")}</Text>
<Text className="text-sm">{t("emails.password_changed_email_text")}</Text>
<EmailFooter t={t} />
</Container>
</EmailTemplate>

View File

@@ -19,16 +19,16 @@ export async function VerificationEmail({
<EmailTemplate t={t}>
<Container>
<Heading>{t("emails.verification_email_heading")}</Heading>
<Text>{t("emails.verification_email_text")}</Text>
<Text className="text-sm">{t("emails.verification_email_text")}</Text>
<EmailButton href={verifyLink} label={t("emails.verification_email_verify_email")} />
<Text>{t("emails.verification_email_click_on_this_link")}</Text>
<Link className="break-all text-black" href={verifyLink}>
<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="font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
<Text>
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
<Text className="text-sm">
{t("emails.verification_email_if_expired_request_new_token")}
<Link className="text-black underline" href={verificationRequestLink}>
<Link className="text-sm text-black underline" href={verificationRequestLink}>
{t("emails.verification_email_request_new_verification")}
</Link>
</Text>

View File

@@ -16,10 +16,8 @@ export async function EmailCustomizationPreviewEmail({
return (
<EmailTemplate logoUrl={logoUrl} t={t}>
<Container>
<Heading className="text-xl">
{t("emails.email_customization_preview_email_heading", { userName })}
</Heading>
<Text className="font-normal">{t("emails.email_customization_preview_email_text")}</Text>
<Heading>{t("emails.email_customization_preview_email_heading", { userName })}</Heading>
<Text className="text-sm">{t("emails.email_customization_preview_email_text")}</Text>
</Container>
</EmailTemplate>
);

View File

@@ -17,10 +17,10 @@ export async function InviteAcceptedEmail({
return (
<EmailTemplate t={t}>
<Container>
<Heading className="text-xl">
<Heading>
{t("emails.invite_accepted_email_heading", { inviterName })} {inviterName}
</Heading>
<Text className="font-normal">
<Text className="text-sm">
{t("emails.invite_accepted_email_text_par1", { inviteeName })} {inviteeName}{" "}
{t("emails.invite_accepted_email_text_par2")}
</Text>

View File

@@ -20,10 +20,10 @@ export async function InviteEmail({
return (
<EmailTemplate t={t}>
<Container>
<Heading className="text-xl">
<Heading>
{t("emails.invite_email_heading", { inviteeName })} {inviteeName}
</Heading>
<Text className="font-normal">
<Text className="text-sm">
{t("emails.invite_email_text_par1", { inviterName })} {inviterName}{" "}
{t("emails.invite_email_text_par2")}
</Text>

View File

@@ -1,42 +0,0 @@
import { getTranslate } from "@/tolgee/server";
import { Container, Heading, Text } from "@react-email/components";
import { EmailButton } from "../../components/email-button";
import { EmailFooter } from "../../components/email-footer";
import { EmailTemplate } from "../../components/email-template";
interface OnboardingInviteEmailProps {
inviteMessage: string;
inviterName: string;
verifyLink: string;
inviteeName: string;
}
export async function OnboardingInviteEmail({
inviteMessage,
inviterName,
verifyLink,
inviteeName,
}: OnboardingInviteEmailProps): Promise<React.JSX.Element> {
const t = await getTranslate();
return (
<EmailTemplate t={t}>
<Container>
<Heading>{t("emails.onboarding_invite_email_heading", { inviteeName })}</Heading>
<Text>{inviteMessage}</Text>
<Text className="font-medium">{t("emails.onboarding_invite_email_get_started_in_minutes")}</Text>
<ol>
<li>{t("emails.onboarding_invite_email_create_account", { inviterName })}</li>
<li>{t("emails.onboarding_invite_email_connect_formbricks")}</li>
<li>{t("emails.onboarding_invite_email_done")} </li>
</ol>
<EmailButton
href={verifyLink}
label={t("emails.onboarding_invite_email_button_label", { inviterName })}
/>
<EmailFooter t={t} />
</Container>
</EmailTemplate>
);
}
export default OnboardingInviteEmail;

View File

@@ -83,10 +83,10 @@ describe("renderEmailResponseValue", () => {
expect(screen.getByText(expectedMessage)).toBeInTheDocument();
expect(screen.getByText(expectedMessage)).toHaveClass(
"mt-0",
"font-bold",
"break-words",
"whitespace-pre-wrap",
"italic"
"italic",
"text-sm"
);
});
});
@@ -225,7 +225,7 @@ ${"This is a very long sentence that should wrap properly within the email layou
// Check if the text has the expected styling classes
const textElement = screen.getByText(response);
expect(textElement).toHaveClass("mt-0", "font-bold", "break-words", "whitespace-pre-wrap");
expect(textElement).toHaveClass("mt-0", "break-words", "whitespace-pre-wrap", "text-sm");
});
test("handles array responses in the default case by rendering them as text", async () => {
@@ -248,7 +248,7 @@ ${"This is a very long sentence that should wrap properly within the email layou
// Check if the text element contains all items from the response array
const textElement = container.querySelector("p");
expect(textElement).not.toBeNull();
expect(textElement).toHaveClass("mt-0", "font-bold", "break-words", "whitespace-pre-wrap");
expect(textElement).toHaveClass("mt-0", "break-words", "whitespace-pre-wrap", "text-sm");
// Verify each item is present in the text content
response.forEach((item) => {

View File

@@ -15,18 +15,20 @@ export const renderEmailResponseValue = async (
return (
<Container>
{overrideFileUploadResponse ? (
<Text className="mt-0 whitespace-pre-wrap break-words font-bold italic">
<Text className="mt-0 whitespace-pre-wrap break-words text-sm italic">
{t("emails.render_email_response_value_file_upload_response_link_not_included")}
</Text>
) : (
Array.isArray(response) &&
response.map((responseItem) => (
<Link
className="mt-2 flex flex-col items-center justify-center rounded-lg bg-slate-200 p-2 text-black shadow-sm"
className="mt-2 flex flex-col items-center justify-center rounded-lg bg-slate-200 p-2 text-sm text-black shadow-sm"
href={responseItem}
key={responseItem}>
<FileIcon />
<Text className="mx-auto mb-0 truncate">{getOriginalFileNameFromUrl(responseItem)}</Text>
<FileIcon className="h-4 w-4" />
<Text className="mx-auto mb-0 truncate text-sm">
{getOriginalFileNameFromUrl(responseItem)}
</Text>
</Link>
))
)}
@@ -50,7 +52,7 @@ export const renderEmailResponseValue = async (
case TSurveyQuestionTypeEnum.Ranking:
return (
<Container>
<Row className="my-1 font-semibold text-slate-700" dir="auto">
<Row className="mb-2 text-sm text-slate-700" dir="auto">
{Array.isArray(response) &&
response.map(
(item, index) =>
@@ -66,6 +68,6 @@ export const renderEmailResponseValue = async (
);
default:
return <Text className="mt-0 whitespace-pre-wrap break-words font-bold">{response}</Text>;
return <Text className="mt-0 whitespace-pre-wrap break-words text-sm">{response}</Text>;
}
};

View File

@@ -18,13 +18,13 @@ export async function EmbedSurveyPreviewEmail({
return (
<EmailTemplate logoUrl={logoUrl} t={t}>
<Container>
<Heading className="text-xl">{t("emails.embed_survey_preview_email_heading")}</Heading>
<Text className="font-normal">{t("emails.embed_survey_preview_email_text")}</Text>
<Text className="text-sm font-normal">
<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 dangerouslySetInnerHTML={{ __html: html }} />
<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>

View File

@@ -20,11 +20,11 @@ export async function LinkSurveyEmail({
return (
<EmailTemplate logoUrl={logoUrl} t={t}>
<Container>
<Heading className="text-xl">{t("emails.verification_email_hey")}</Heading>
<Text className="font-normal">{t("emails.verification_email_thanks")}</Text>
<Text className="font-normal">{t("emails.verification_email_to_fill_survey")}</Text>
<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-xs text-slate-400">
<Text className="text-sm text-slate-400">
{t("emails.verification_email_survey_name")}: {surveyName}
</Text>
<EmailFooter t={t} />

View File

@@ -1,7 +1,7 @@
import { getQuestionResponseMapping } from "@/lib/responses";
import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils";
import { getTranslate } from "@/tolgee/server";
import { Column, Container, Hr, Link, Row, Section, Text } from "@react-email/components";
import { Column, Container, Heading, Hr, Link, Row, Section, Text } from "@react-email/components";
import { FileDigitIcon, FileType2Icon } from "lucide-react";
import type { TOrganization } from "@formbricks/types/organizations";
import type { TResponse } from "@formbricks/types/responses";
@@ -34,8 +34,8 @@ export async function ResponseFinishedEmail({
<Container>
<Row>
<Column>
<Text className="mb-4 text-xl font-bold"> {t("emails.survey_response_finished_email_hey")}</Text>
<Text className="mb-4 font-normal">
<Heading> {t("emails.survey_response_finished_email_hey")}</Heading>
<Text className="mb-4 text-sm">
{t("emails.survey_response_finished_email_congrats", {
surveyName: survey.name,
})}
@@ -45,8 +45,8 @@ export async function ResponseFinishedEmail({
if (!question.response) return;
return (
<Row key={question.question}>
<Column className="w-full">
<Text className="mb-2 font-medium">{question.question}</Text>
<Column className="w-full font-medium">
<Text className="mb-2 text-sm">{question.question}</Text>
{renderEmailResponseValue(question.response, question.type, t)}
</Column>
</Row>
@@ -57,8 +57,8 @@ export async function ResponseFinishedEmail({
if (variableResponse && ["number", "string"].includes(typeof variable)) {
return (
<Row key={variable.id}>
<Column className="w-full">
<Text className="mb-2 flex items-center gap-2 font-medium">
<Column className="w-full text-sm font-medium">
<Text className="mb-1 flex items-center gap-2">
{variable.type === "number" ? (
<FileDigitIcon className="h-4 w-4" />
) : (
@@ -66,7 +66,7 @@ export async function ResponseFinishedEmail({
)}
{variable.name}
</Text>
<Text className="mt-0 whitespace-pre-wrap break-words font-bold">
<Text className="mt-0 whitespace-pre-wrap break-words font-medium">
{variableResponse}
</Text>
</Column>
@@ -80,11 +80,11 @@ export async function ResponseFinishedEmail({
if (hiddenFieldResponse && typeof hiddenFieldResponse === "string") {
return (
<Row key={hiddenFieldId}>
<Column className="w-full">
<Text className="mb-2 flex items-center gap-2 font-medium">
<Column className="w-full font-medium">
<Text className="mb-2 flex items-center gap-2 text-sm">
{hiddenFieldId} <EyeOffIcon />
</Text>
<Text className="mt-0 whitespace-pre-wrap break-words font-bold">
<Text className="mt-0 whitespace-pre-wrap break-words text-sm">
{hiddenFieldResponse}
</Text>
</Column>
@@ -105,19 +105,19 @@ export async function ResponseFinishedEmail({
/>
<Hr />
<Section className="mt-4 text-center text-sm">
<Text className="font-bold">
<Text className="text-sm font-medium">
{t("emails.survey_response_finished_email_dont_want_notifications")}
</Text>
<Text className="mb-0">
<Link
className="text-black underline"
className="text-sm text-black underline"
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=alert&elementId=${survey.id}`}>
{t("emails.survey_response_finished_email_turn_off_notifications_for_this_form")}
</Link>
</Text>
<Text className="mt-0">
<Link
className="text-black underline"
className="text-sm text-black underline"
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=unsubscribedOrganizationIds&elementId=${organization.id}`}>
{t("emails.survey_response_finished_email_turn_off_notifications_for_all_new_forms")}
</Link>

View File

@@ -16,19 +16,19 @@ export async function CreateReminderNotificationBody({
const t = await getTranslate();
return (
<Container>
<Text>
<Text className="text-sm">
{t("emails.weekly_summary_create_reminder_notification_body_text", {
projectName: notificationData.projectName,
})}
</Text>
<Text className="pt-4 font-bold">
<Text className="pt-4 text-sm font-medium">
{t("emails.weekly_summary_create_reminder_notification_body_dont_let_a_week_pass")}
</Text>
<EmailButton
href={`${WEBAPP_URL}/environments/${notificationData.environmentId}/surveys?utm_source=weekly&utm_medium=email&utm_content=SetupANewSurveyCTA`}
label={t("emails.weekly_summary_create_reminder_notification_body_setup_a_new_survey")}
/>
<Text className="pt-4">
<Text className="pt-4 text-sm">
{t("emails.weekly_summary_create_reminder_notification_body_need_help")}
<a href="https://cal.com/johannes/15">
{t("emails.weekly_summary_create_reminder_notification_body_cal_slot")}

View File

@@ -48,7 +48,9 @@ export async function LiveSurveyNotification({
if (surveyResponses.length === 0) {
return (
<Container className="mt-4">
<Text className="m-0 font-bold">{t("emails.live_survey_notification_no_responses_yet")}</Text>
<Text className="m-0 text-sm font-medium">
{t("emails.live_survey_notification_no_responses_yet")}
</Text>
</Container>
);
}
@@ -62,7 +64,7 @@ export async function LiveSurveyNotification({
surveyFields.push(
<Container className="mt-4" key={`${index.toString()}-${surveyResponse.headline}`}>
<Text className="m-0">{surveyResponse.headline}</Text>
<Text className="m-0 text-sm">{surveyResponse.headline}</Text>
{renderEmailResponseValue(surveyResponse.responseValue, surveyResponse.questionType, t)}
</Container>
);
@@ -87,7 +89,7 @@ export async function LiveSurveyNotification({
<Container className="mt-12">
<Text className="mb-0 inline">
<Link
className="text-xl text-black underline"
className="text-sm text-black underline"
href={`${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=weekly&utm_medium=email&utm_content=ViewResponsesCTA`}>
{survey.name}
</Link>
@@ -98,7 +100,7 @@ export async function LiveSurveyNotification({
{displayStatus}
</Text>
{noResponseLastWeek ? (
<Text>{t("emails.live_survey_notification_no_new_response")}</Text>
<Text className="text-sm">{t("emails.live_survey_notification_no_new_response")}</Text>
) : (
createSurveyFields(survey.responses)
)}

View File

@@ -13,15 +13,15 @@ export async function NotificationFooter({
return (
<Tailwind>
<Container className="w-full">
<Text className="mb-0 pt-4 font-medium">{t("emails.notification_footer_all_the_best")}</Text>
<Text className="mt-0">{t("emails.notification_footer_the_formbricks_team")}</Text>
<Text className="mb-0 pt-4 text-sm font-medium">{t("emails.notification_footer_all_the_best")}</Text>
<Text className="mt-0 text-sm">{t("emails.notification_footer_the_formbricks_team")}</Text>
<Container
className="mt-0 w-full rounded-md bg-slate-100 px-4 text-center text-xs leading-5"
style={{ fontStyle: "italic" }}>
<Text>
<Text className="text-sm">
{t("emails.notification_footer_to_halt_weekly_updates")}
<Link
className="text-black underline"
className="text-sm text-black underline"
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications`}>
{t("emails.notification_footer_please_turn_them_off")}
</Link>{" "}

View File

@@ -18,17 +18,17 @@ export async function NotificationHeader({
endYear,
}: NotificationHeaderProps): Promise<React.JSX.Element> {
const t = await getTranslate();
const getNotificationHeaderimePeriod = (): React.JSX.Element => {
const getNotificationHeaderTimePeriod = (): React.JSX.Element => {
if (startYear === endYear) {
return (
<Text className="m-0 text-right">
<Text className="m-0 text-right text-sm">
{startDate} - {endDate} {endYear}
</Text>
);
}
return (
<Text className="m-0 text-right">
<Text className="m-0 text-right text-sm">
{startDate} {startYear} - {endDate} {endYear}
</Text>
);
@@ -40,10 +40,10 @@ export async function NotificationHeader({
<Heading className="m-0">{t("emails.notification_header_hey")}</Heading>
</div>
<div className="float-right">
<Text className="m-0 text-right font-semibold">
<Text className="m-0 text-right text-sm font-medium">
{t("emails.notification_header_weekly_report_for")} {projectName}
</Text>
{getNotificationHeaderimePeriod()}
{getNotificationHeaderTimePeriod()}
</div>
</div>
</Container>

View File

@@ -17,24 +17,24 @@ export async function NotificationInsight({
<Row>
<Column className="text-center">
<Text className="text-sm">{t("emails.notification_insight_surveys")}</Text>
<Text className="text-lg font-bold">{insights.numLiveSurvey}</Text>
<Text className="text-sm font-medium">{insights.numLiveSurvey}</Text>
</Column>
<Column className="text-center">
<Text className="text-sm">{t("emails.notification_insight_displays")}</Text>
<Text className="text-lg font-bold">{insights.totalDisplays}</Text>
<Text className="text-sm font-medium">{insights.totalDisplays}</Text>
</Column>
<Column className="text-center">
<Text className="text-sm">{t("emails.notification_insight_responses")}</Text>
<Text className="text-lg font-bold">{insights.totalResponses}</Text>
<Text className="text-sm font-medium">{insights.totalResponses}</Text>
</Column>
<Column className="text-center">
<Text className="text-sm">{t("emails.notification_insight_completed")}</Text>
<Text className="text-lg font-bold">{insights.totalCompletedResponses}</Text>
<Text className="text-sm font-medium">{insights.totalCompletedResponses}</Text>
</Column>
{insights.totalDisplays !== 0 ? (
<Column className="text-center">
<Text className="text-sm">{t("emails.notification_insight_completion_rate")}</Text>
<Text className="text-lg font-bold">{Math.round(insights.completionRate)}%</Text>
<Text className="text-sm font-medium">{Math.round(insights.completionRate)}%</Text>
</Column>
) : (
""

View File

@@ -32,7 +32,6 @@ import { PasswordResetNotifyEmail } from "./emails/auth/password-reset-notify-em
import { VerificationEmail } from "./emails/auth/verification-email";
import { InviteAcceptedEmail } from "./emails/invite/invite-accepted-email";
import { InviteEmail } from "./emails/invite/invite-email";
import { OnboardingInviteEmail } from "./emails/invite/onboarding-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";
@@ -166,9 +165,7 @@ export const sendInviteMemberEmail = async (
inviteId: string,
email: string,
inviterName: string,
inviteeName: string,
isOnboardingInvite?: boolean,
inviteMessage?: string
inviteeName: string
): Promise<boolean> => {
const token = createInviteToken(inviteId, email, {
expiresIn: "7d",
@@ -177,26 +174,12 @@ export const sendInviteMemberEmail = async (
const verifyLink = `${WEBAPP_URL}/invite?token=${encodeURIComponent(token)}`;
if (isOnboardingInvite && inviteMessage) {
const html = await render(
await OnboardingInviteEmail({ verifyLink, inviteMessage, inviterName, inviteeName })
);
return await sendEmail({
to: email,
subject: t("emails.onboarding_invite_email_subject", {
inviterName,
}),
html,
});
} else {
const t = await getTranslate();
const html = await render(await InviteEmail({ inviteeName, inviterName, verifyLink }));
return await sendEmail({
to: email,
subject: t("emails.invite_member_email_subject"),
html,
});
}
const html = await render(await InviteEmail({ inviteeName, inviterName, verifyLink }));
return await sendEmail({
to: email,
subject: t("emails.invite_member_email_subject"),
html,
});
};
export const sendInviteAcceptedEmail = async (

View File

@@ -188,9 +188,7 @@ export const resendInviteAction = authenticatedActionClient.schema(ZResendInvite
parsedInput.inviteId,
updatedInvite.email,
invite?.creator?.name ?? "",
updatedInvite.name ?? "",
undefined,
ctx.user.locale
updatedInvite.name ?? ""
);
return updatedInvite;
}
@@ -266,14 +264,7 @@ export const inviteUserAction = authenticatedActionClient.schema(ZInviteUserActi
};
if (inviteId) {
await sendInviteMemberEmail(
inviteId,
parsedInput.email,
ctx.user.name ?? "",
parsedInput.name ?? "",
false,
undefined
);
await sendInviteMemberEmail(inviteId, parsedInput.email, ctx.user.name ?? "", parsedInput.name ?? "");
}
return inviteId;

View File

@@ -57,14 +57,7 @@ export const inviteOrganizationMemberAction = authenticatedActionClient
currentUserId: ctx.user.id,
});
await sendInviteMemberEmail(
invitedUserId,
parsedInput.email,
ctx.user.name,
"",
false, // is onboarding invite
undefined
);
await sendInviteMemberEmail(invitedUserId, parsedInput.email, ctx.user.name, "");
ctx.auditLoggingCtx.inviteId = invitedUserId;
ctx.auditLoggingCtx.newObject = {

View File

@@ -76,8 +76,8 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JS
if (!question.response) return;
return (
<Row key={question.question}>
<Column className="w-full">
<Text className="mb-2 font-medium">{question.question}</Text>
<Column className="w-full font-medium">
<Text className="mb-2 text-sm">{question.question}</Text>
{renderEmailResponseValue(question.response, question.type, t, true)}
</Column>
</Row>
@@ -89,22 +89,22 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JS
{isDefaultLogo ? (
<Section className="mt-4 text-center text-sm">
<Link
className="m-0 font-normal text-slate-500"
className="m-0 text-sm text-slate-500"
href="https://formbricks.com/?utm_source=email_header&utm_medium=email"
target="_blank"
rel="noopener noreferrer">
{t("emails.email_template_text_1")}
</Link>
{IMPRINT_ADDRESS && (
<Text className="m-0 font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
<Text className="m-0 text-sm text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
)}
<Text className="m-0 font-normal text-slate-500 opacity-50">
<Text className="m-0 text-sm text-slate-500 opacity-50">
{IMPRINT_URL && (
<Link
href={IMPRINT_URL}
target="_blank"
rel="noopener noreferrer"
className="text-slate-500">
className="text-sm text-slate-500">
{t("emails.imprint")}
</Link>
)}
@@ -114,7 +114,7 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JS
href={PRIVACY_URL}
target="_blank"
rel="noopener noreferrer"
className="text-slate-500">
className="text-sm text-slate-500">
{t("emails.privacy_policy")}
</Link>
)}