mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-23 13:48:58 -05:00
9a6cbd05b6
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
489 lines
20 KiB
TypeScript
489 lines
20 KiB
TypeScript
import { TFunction } from "i18next";
|
|
import { CalendarDaysIcon, ExternalLinkIcon, UploadIcon } from "lucide-react";
|
|
import React from "react";
|
|
import {
|
|
Column,
|
|
Container,
|
|
ElementHeader,
|
|
Button as EmailButton,
|
|
Img,
|
|
Link,
|
|
Row,
|
|
Section,
|
|
Tailwind,
|
|
Text,
|
|
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";
|
|
import { WEBAPP_URL } from "@/lib/constants";
|
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
|
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
|
|
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
|
import { isLight, mixColor } from "@/lib/utils/colors";
|
|
import { parseRecallInfo } from "@/lib/utils/recall";
|
|
import { RatingSmiley } from "@/modules/analysis/components/RatingSmiley";
|
|
import { resolveStorageUrl } from "@/modules/storage/utils";
|
|
import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils";
|
|
|
|
interface PreviewEmailTemplateProps {
|
|
survey: TSurvey;
|
|
surveyUrl: string;
|
|
styling: TSurveyStyling;
|
|
locale: string;
|
|
t: TFunction;
|
|
}
|
|
|
|
export const getPreviewEmailTemplateHtml = async (
|
|
survey: TSurvey,
|
|
surveyUrl: string,
|
|
styling: TSurveyStyling,
|
|
locale: string,
|
|
t: TFunction
|
|
): Promise<string> => {
|
|
return render(
|
|
<PreviewEmailTemplate styling={styling} survey={survey} surveyUrl={surveyUrl} locale={locale} t={t} />,
|
|
{
|
|
pretty: true,
|
|
}
|
|
);
|
|
};
|
|
|
|
const getRatingContent = (scale: string, i: number, range: number, isColorCodingEnabled: boolean) => {
|
|
if (scale === "smiley") {
|
|
return (
|
|
<RatingSmiley
|
|
active={false}
|
|
idx={i}
|
|
range={range}
|
|
addColors={isColorCodingEnabled}
|
|
baseUrl={WEBAPP_URL}
|
|
/>
|
|
);
|
|
}
|
|
if (scale === "number") {
|
|
return <Text className="m-0 h-[44px] text-center text-[14px] leading-[44px]">{i + 1}</Text>;
|
|
}
|
|
if (scale === "star") {
|
|
return <Text className="m-auto text-3xl">⭐</Text>;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
export async function PreviewEmailTemplate({
|
|
survey,
|
|
surveyUrl,
|
|
styling,
|
|
t,
|
|
}: PreviewEmailTemplateProps): Promise<React.JSX.Element> {
|
|
const url = `${surveyUrl}?preview=true`;
|
|
const urlWithPrefilling = `${surveyUrl}?preview=true&skipPrefilled=true&`;
|
|
const defaultLanguageCode = "default";
|
|
|
|
// Derive questions from blocks
|
|
const questions = getElementsFromBlocks(survey.blocks);
|
|
const firstQuestion = questions[0];
|
|
|
|
const headline = parseRecallInfo(getLocalizedValue(firstQuestion.headline, defaultLanguageCode));
|
|
const subheader = parseRecallInfo(getLocalizedValue(firstQuestion.subheader, defaultLanguageCode));
|
|
const brandColor = styling.brandColor?.light ?? COLOR_DEFAULTS.brandColor;
|
|
|
|
switch (firstQuestion.type) {
|
|
case TSurveyElementTypeEnum.OpenText:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Section className="border-input-border-color rounded-custom mt-4 block h-20 w-full border border-solid bg-slate-50" />
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.Consent:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Container className="border-input-border-color bg-input-color rounded-custom m-0 mt-4 block w-full max-w-none border border-solid p-4 font-medium text-slate-800">
|
|
<Text className="text-question-color m-0 inline-block">
|
|
{getLocalizedValue(firstQuestion.label, defaultLanguageCode)}
|
|
</Text>
|
|
</Container>
|
|
<Container className="mx-0 mt-4 flex max-w-none justify-end">
|
|
{!firstQuestion.required && (
|
|
<EmailButton
|
|
className="rounded-custom inline-flex cursor-pointer appearance-none px-6 py-3 text-sm font-medium text-black"
|
|
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}>
|
|
{t("emails.reject")}
|
|
</EmailButton>
|
|
)}
|
|
<EmailButton
|
|
className={cn(
|
|
"bg-brand-color rounded-custom ml-2 inline-flex cursor-pointer appearance-none px-6 py-3 text-sm font-medium",
|
|
isLight(brandColor) ? "text-black" : "text-white"
|
|
)}
|
|
href={`${urlWithPrefilling}${firstQuestion.id}=accepted`}>
|
|
{t("emails.accept")}
|
|
</EmailButton>
|
|
</Container>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.NPS:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<Section className="w-full justify-center">
|
|
<ElementHeader headline={headline} subheader={subheader} />
|
|
<Container className="mx-0 mt-4 w-full items-center justify-center">
|
|
<Section
|
|
className={cn("w-full overflow-hidden", {
|
|
"border border-solid border-slate-200": firstQuestion.scale === "number",
|
|
})}>
|
|
<Row>
|
|
<Column className="mb-4 flex w-full justify-between gap-0">
|
|
{Array.from({ length: 11 }, (_, i) => (
|
|
<EmailButton
|
|
href={`${urlWithPrefilling}${firstQuestion.id}=${i.toString()}`}
|
|
key={i}
|
|
className={cn(
|
|
firstQuestion.isColorCodingEnabled && firstQuestion.scale === "number"
|
|
? `h-[46px] border border-t-[6px] border-t-${getNPSOptionColor(i + 1).replace("bg-", "")}`
|
|
: "h-10",
|
|
"relative m-0 w-full overflow-hidden border border-l-0 border-solid border-slate-200 p-0 text-center align-middle leading-10 text-slate-800",
|
|
{ "rounded-l-lg border-l": i === 0 },
|
|
{ "rounded-r-lg": i === 10 }
|
|
)}>
|
|
{i}
|
|
</EmailButton>
|
|
))}
|
|
</Column>
|
|
</Row>
|
|
</Section>
|
|
<Section className="text-question-color mt-2 px-1.5 text-xs leading-6">
|
|
<Row>
|
|
<Column>
|
|
<Text className="m-0 inline-block w-max p-0">
|
|
{getLocalizedValue(firstQuestion.lowerLabel, defaultLanguageCode)}
|
|
</Text>
|
|
</Column>
|
|
<Column className="text-right">
|
|
<Text className="m-0 inline-block w-max p-0 text-right">
|
|
{getLocalizedValue(firstQuestion.upperLabel, defaultLanguageCode)}
|
|
</Text>
|
|
</Column>
|
|
</Row>
|
|
</Section>
|
|
</Container>
|
|
<EmailFooter t={t} />
|
|
</Section>
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.CTA: {
|
|
const ctaElement = firstQuestion as TSurveyCTAElement;
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
{ctaElement.buttonExternal && ctaElement.ctaButtonLabel && ctaElement.buttonUrl && (
|
|
<Container className="mx-0 mt-4 flex max-w-none items-center justify-end">
|
|
<EmailButton
|
|
className="text-question-color flex items-center rounded-md border-0 bg-transparent px-3 py-3 text-base font-medium leading-4 no-underline shadow-none"
|
|
href={ctaElement.buttonUrl}>
|
|
<Text className="inline">
|
|
{getLocalizedValue(ctaElement.ctaButtonLabel, defaultLanguageCode)}{" "}
|
|
</Text>
|
|
<ExternalLinkIcon className="ml-2 inline h-4 w-4" />
|
|
</EmailButton>
|
|
</Container>
|
|
)}
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
}
|
|
case TSurveyElementTypeEnum.Rating:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<Section className="w-full">
|
|
<ElementHeader headline={headline} subheader={subheader} />
|
|
<Container className="mx-0 mt-4 w-full items-center justify-center">
|
|
<Section className="w-full overflow-hidden">
|
|
<Row>
|
|
<Column className="flex w-full justify-around">
|
|
{Array.from({ length: firstQuestion.range }, (_, i) => (
|
|
<EmailButton
|
|
className={cn(
|
|
"relative m-0 h-[48px] w-full overflow-hidden border border-l-0 border-solid border-gray-200 p-0 text-center align-middle leading-10 text-slate-800",
|
|
|
|
{ "rounded-l-lg border-l": i === 0 },
|
|
{ "rounded-r-lg": i === firstQuestion.range - 1 },
|
|
firstQuestion.isColorCodingEnabled &&
|
|
firstQuestion.scale === "number" &&
|
|
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
|
|
firstQuestion.scale === "star" && "border-transparent"
|
|
)}
|
|
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}
|
|
key={i}>
|
|
{getRatingContent(
|
|
firstQuestion.scale,
|
|
i,
|
|
firstQuestion.range,
|
|
firstQuestion.isColorCodingEnabled
|
|
)}
|
|
</EmailButton>
|
|
))}
|
|
</Column>
|
|
</Row>
|
|
</Section>
|
|
<Section className="text-question-color m-0 px-1.5 text-xs leading-6">
|
|
<Row>
|
|
<Column>
|
|
<Text className="m-0 inline-block p-0">
|
|
{getLocalizedValue(firstQuestion.lowerLabel, defaultLanguageCode)}
|
|
</Text>
|
|
</Column>
|
|
<Column className="text-right">
|
|
<Text className="m-0 inline-block p-0 text-right">
|
|
{getLocalizedValue(firstQuestion.upperLabel, defaultLanguageCode)}
|
|
</Text>
|
|
</Column>
|
|
</Row>
|
|
</Section>
|
|
</Container>
|
|
<EmailFooter t={t} />
|
|
</Section>
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.MultipleChoiceMulti:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Container className="mx-0 max-w-none">
|
|
{firstQuestion.choices.map((choice) => (
|
|
<Section
|
|
className="border-input-border-color bg-input-color text-question-color rounded-custom mt-2 block w-full border border-solid p-4"
|
|
key={choice.id}>
|
|
{getLocalizedValue(choice.label, defaultLanguageCode)}
|
|
</Section>
|
|
))}
|
|
</Container>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.Ranking:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Container className="mx-0 max-w-none">
|
|
{firstQuestion.choices.map((choice) => (
|
|
<Section
|
|
className="border-input-border-color bg-input-color text-question-color rounded-custom mt-2 block w-full border border-solid p-4"
|
|
key={choice.id}>
|
|
{getLocalizedValue(choice.label, defaultLanguageCode)}
|
|
</Section>
|
|
))}
|
|
</Container>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.MultipleChoiceSingle:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Container className="mx-0 max-w-none">
|
|
{firstQuestion.choices.map((choice) => (
|
|
<Link
|
|
className="border-input-border-color bg-input-color text-question-color rounded-custom mt-2 block border border-solid p-4"
|
|
href={`${urlWithPrefilling}${firstQuestion.id}=${getLocalizedValue(choice.label, defaultLanguageCode)}`}
|
|
key={choice.id}>
|
|
{getLocalizedValue(choice.label, defaultLanguageCode)}
|
|
</Link>
|
|
))}
|
|
</Container>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.PictureSelection:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Section className="mx-0 mt-4">
|
|
{firstQuestion.choices.map((choice) =>
|
|
firstQuestion.allowMulti ? (
|
|
<Img
|
|
className="rounded-custom mb-3 mr-3 inline-block h-[150px] w-[250px]"
|
|
key={choice.id}
|
|
src={resolveStorageUrl(choice.imageUrl)}
|
|
/>
|
|
) : (
|
|
<Link
|
|
className="rounded-custom mb-3 mr-3 inline-block h-[150px] w-[250px]"
|
|
href={`${urlWithPrefilling}${firstQuestion.id}=${choice.id}`}
|
|
key={choice.id}
|
|
target="_blank">
|
|
<Img className="rounded-custom h-full w-full" src={resolveStorageUrl(choice.imageUrl)} />
|
|
</Link>
|
|
)
|
|
)}
|
|
</Section>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.Cal:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<Container>
|
|
<ElementHeader headline={headline} subheader={subheader} />
|
|
<EmailButton
|
|
className={cn(
|
|
"bg-brand-color rounded-custom mx-auto block w-max cursor-pointer appearance-none px-6 py-3 text-sm font-medium",
|
|
isLight(brandColor) ? "text-black" : "text-white"
|
|
)}>
|
|
{t("emails.schedule_your_meeting")}
|
|
</EmailButton>
|
|
</Container>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.Date:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Section className="border-input-border-color bg-input-color rounded-custom mt-4 flex h-12 w-full items-center justify-center border border-solid">
|
|
<CalendarDaysIcon className="text-question-color inline h-4 w-4" />
|
|
<Text className="text-question-color inline text-sm font-medium">
|
|
{t("emails.select_a_date")}
|
|
</Text>
|
|
</Section>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.Matrix:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Container className="mx-0">
|
|
<Section className="w-full table-auto">
|
|
<Row>
|
|
<Column className="w-40 break-words px-4 py-2" />
|
|
{firstQuestion.columns.map((column) => {
|
|
return (
|
|
<Column
|
|
className="text-question-color max-w-40 break-words px-4 py-2 text-center"
|
|
key={column.id}>
|
|
{getLocalizedValue(column.label, "default")}
|
|
</Column>
|
|
);
|
|
})}
|
|
</Row>
|
|
{firstQuestion.rows.map((row, rowIndex) => {
|
|
return (
|
|
<Row
|
|
className={`${rowIndex % 2 === 0 ? "bg-input-color" : ""} rounded-custom`}
|
|
key={row.id}>
|
|
<Column className="w-40 break-words px-4 py-2">
|
|
{getLocalizedValue(row.label, "default")}
|
|
</Column>
|
|
{firstQuestion.columns.map((column) => {
|
|
return (
|
|
<Column className="text-question-color px-4 py-2" key={column.id}>
|
|
<Section className="bg-card-bg-color h-4 w-4 rounded-full p-2 outline" />
|
|
</Column>
|
|
);
|
|
})}
|
|
</Row>
|
|
);
|
|
})}
|
|
</Section>
|
|
</Container>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
case TSurveyElementTypeEnum.Address:
|
|
case TSurveyElementTypeEnum.ContactInfo:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
{["First Name", "Last Name", "Email", "Phone", "Company"].map((label) => (
|
|
<Section
|
|
className="border-input-border-color bg-input-color rounded-custom mt-4 block h-10 w-full border border-solid py-2 pl-2 text-slate-400"
|
|
key={label}>
|
|
{label}
|
|
</Section>
|
|
))}
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
|
|
case TSurveyElementTypeEnum.FileUpload:
|
|
return (
|
|
<EmailTemplateWrapper styling={styling} surveyUrl={url}>
|
|
<ElementHeader headline={headline} subheader={subheader} className="mr-8" />
|
|
<Section className="border-input-border-color rounded-custom mt-4 flex h-24 w-full items-center justify-center border border-dashed bg-slate-50">
|
|
<Container className="mx-auto flex items-center text-center">
|
|
<UploadIcon className="mt-6 inline h-5 w-5 text-slate-400" />
|
|
<Text className="text-slate-400">{t("emails.click_or_drag_to_upload_files")}</Text>
|
|
</Container>
|
|
</Section>
|
|
<EmailFooter t={t} />
|
|
</EmailTemplateWrapper>
|
|
);
|
|
}
|
|
}
|
|
|
|
function EmailTemplateWrapper({
|
|
children,
|
|
surveyUrl,
|
|
styling,
|
|
}: {
|
|
children: React.ReactNode;
|
|
surveyUrl: string;
|
|
styling: TSurveyStyling;
|
|
}): React.JSX.Element {
|
|
let signatureColor = "";
|
|
const colors = {
|
|
"brand-color": styling.brandColor?.light ?? COLOR_DEFAULTS.brandColor,
|
|
"card-bg-color": styling.cardBackgroundColor?.light ?? COLOR_DEFAULTS.cardBackgroundColor,
|
|
"input-color": styling.inputColor?.light ?? COLOR_DEFAULTS.inputColor,
|
|
"input-border-color": styling.inputBorderColor?.light ?? COLOR_DEFAULTS.inputBorderColor,
|
|
"card-border-color": styling.cardBorderColor?.light ?? COLOR_DEFAULTS.cardBorderColor,
|
|
"question-color": styling.questionColor?.light ?? COLOR_DEFAULTS.questionColor,
|
|
};
|
|
|
|
if (isLight(colors["question-color"])) {
|
|
signatureColor = mixColor(colors["question-color"], "#000000", 0.2);
|
|
} else {
|
|
signatureColor = mixColor(colors["question-color"], "#ffffff", 0.2);
|
|
}
|
|
|
|
return (
|
|
<Tailwind
|
|
config={{
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
...colors,
|
|
"signature-color": signatureColor,
|
|
},
|
|
borderRadius: {
|
|
custom: `${(styling.roundness ?? 8).toString()}px`,
|
|
},
|
|
},
|
|
},
|
|
}}>
|
|
<Link
|
|
className="bg-card-bg-color border-card-border-color rounded-custom mx-0 my-2 block overflow-auto border border-solid p-8 font-sans text-inherit"
|
|
href={surveyUrl}
|
|
target="_blank">
|
|
{children}
|
|
</Link>
|
|
</Tailwind>
|
|
);
|
|
}
|
|
|
|
function EmailFooter({ t }: { t: TFunction }): React.JSX.Element {
|
|
return (
|
|
<Container className="m-auto mt-8 text-center">
|
|
<Link className="text-signature-color text-xs" href="https://formbricks.com/" target="_blank">
|
|
{t("common.powered_by_formbricks")}
|
|
</Link>
|
|
</Container>
|
|
);
|
|
}
|