mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-24 06:28:49 -05:00
chore: add react email dev server & fix package npm deps (#3350)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -37,11 +37,11 @@
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"buffer": "^6.0.3",
|
||||
"terser": "^5.31.6",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-dts": "^3.9.1",
|
||||
"vite-plugin-node-polyfills": "^0.22.0"
|
||||
"@rollup/plugin-inject": "5.0.5",
|
||||
"buffer": "6.0.3",
|
||||
"terser": "5.31.6",
|
||||
"vite": "5.4.8",
|
||||
"vite-plugin-dts": "3.9.1",
|
||||
"vite-plugin-node-polyfills": "0.22.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^14.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"eslint-config-next": "^14.2.5",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "^2.0.14",
|
||||
"@next/eslint-plugin-next": "14.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.0.0",
|
||||
"@typescript-eslint/parser": "8.0.0",
|
||||
"@vercel/style-guide": "6.0.0",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-config-turbo": "2.0.14",
|
||||
"eslint-plugin-react": "7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.9"
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"clean": "rimraf node_modules .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6"
|
||||
"@trivago/prettier-plugin-sort-imports": "4.3.0",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-tailwindcss": "0.6.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.3.0",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react": "18.3.11",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
|
||||
@@ -55,20 +55,20 @@
|
||||
"data-migration:migrate-survey-types": "ts-node ./data-migrations/20241002123456_migrate_survey_types/data-migration.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.18.0",
|
||||
"@prisma/extension-accelerate": "^1.1.0",
|
||||
"dotenv-cli": "^7.4.2"
|
||||
"@prisma/client": "5.18.0",
|
||||
"@prisma/extension-accelerate": "1.1.0",
|
||||
"dotenv-cli": "7.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@paralleldrive/cuid2": "2.2.2",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"prisma": "^5.18.0",
|
||||
"prisma-dbml-generator": "^0.12.0",
|
||||
"prisma-json-types-generator": "^3.0.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^3.23.8",
|
||||
"zod-prisma": "^0.5.4"
|
||||
"prisma": "5.18.0",
|
||||
"prisma-dbml-generator": "0.12.0",
|
||||
"prisma-json-types-generator": "3.0.4",
|
||||
"ts-node": "10.9.2",
|
||||
"zod": "3.23.8",
|
||||
"zod-prisma": "0.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -11,28 +11,28 @@
|
||||
"lint": "eslint --ext .ts,.tsx --fix ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "*",
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@formbricks/lib": "*",
|
||||
"@formbricks/types": "*",
|
||||
"@formbricks/ui": "*",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/react": "18.3.3"
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/react": "18.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"lucide-react": "^0.427.0",
|
||||
"next": "^14.2.5",
|
||||
"next-auth": "^4.24.7",
|
||||
"node-fetch": "^3.3.2",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"server-only": "^0.0.1",
|
||||
"stripe": "^16.7.0",
|
||||
"zod": "^3.23.8"
|
||||
"@paralleldrive/cuid2": "2.2.2",
|
||||
"@radix-ui/react-collapsible": "1.1.0",
|
||||
"https-proxy-agent": "7.0.5",
|
||||
"lucide-react": "0.427.0",
|
||||
"next": "14.2.5",
|
||||
"next-auth": "4.24.7",
|
||||
"node-fetch": "3.3.2",
|
||||
"react-hook-form": "7.53.0",
|
||||
"react-hot-toast": "2.4.1",
|
||||
"server-only": "0.0.1",
|
||||
"stripe": "16.7.0",
|
||||
"zod": "3.23.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailButton } from "../general/email-button";
|
||||
import { EmailFooter } from "../general/email-footer";
|
||||
|
||||
interface ForgotPasswordEmailProps {
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export function ForgotPasswordEmail({ verifyLink }: ForgotPasswordEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<Heading>Change password</Heading>
|
||||
<Text>
|
||||
You have requested a link to change your password. You can do this by clicking the link below:
|
||||
</Text>
|
||||
<EmailButton href={verifyLink} label="Change password" />
|
||||
<Text className="font-bold">The link is valid for 24 hours.</Text>
|
||||
<Text className="mb-0">If you didn't request this, please ignore this email.</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailFooter } from "../general/email-footer";
|
||||
|
||||
export function PasswordResetNotifyEmail(): React.JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<Heading>Password changed</Heading>
|
||||
<Text>Your password has been changed successfully.</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailButton } from "../general/email-button";
|
||||
import { EmailFooter } from "../general/email-footer";
|
||||
|
||||
interface VerificationEmailProps {
|
||||
verifyLink: string;
|
||||
verificationRequestLink: string;
|
||||
}
|
||||
|
||||
export function VerificationEmail({
|
||||
verifyLink,
|
||||
verificationRequestLink,
|
||||
}: VerificationEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<Heading>Almost there!</Heading>
|
||||
<Text>To start using Formbricks please verify your email below:</Text>
|
||||
<EmailButton href={verifyLink} label="Verify email" />
|
||||
<Text>You can also click on this link:</Text>
|
||||
<Link className="break-all text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="font-bold">The link is valid for 24h.</Text>
|
||||
<Text>
|
||||
If it has expired please request a new token here:{" "}
|
||||
<Link className="text-black underline" href={verificationRequestLink}>
|
||||
Request new verification
|
||||
</Link>
|
||||
</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+2
@@ -13,3 +13,5 @@ export function EmailButton({ label, href }: EmailButtonProps): React.JSX.Elemen
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailButton;
|
||||
+2
@@ -8,3 +8,5 @@ export function EmailFooter(): React.JSX.Element {
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailFooter;
|
||||
+2
-6
@@ -1,10 +1,6 @@
|
||||
import { Body, Column, Container, Html, Img, Link, Row, Section, Tailwind } from "@react-email/components";
|
||||
|
||||
interface EmailTemplateProps {
|
||||
content: JSX.Element;
|
||||
}
|
||||
|
||||
export function EmailTemplate({ content }: EmailTemplateProps): React.JSX.Element {
|
||||
export function EmailTemplate({ children }): React.JSX.Element {
|
||||
return (
|
||||
<Html>
|
||||
<Tailwind>
|
||||
@@ -22,7 +18,7 @@ export function EmailTemplate({ content }: EmailTemplateProps): React.JSX.Elemen
|
||||
/>
|
||||
</Link>
|
||||
</Section>
|
||||
<Container className="mx-auto my-8 max-w-xl bg-white p-4 text-left">{content}</Container>
|
||||
<Container className="mx-auto my-8 max-w-xl bg-white p-4 text-left">{children}</Container>
|
||||
|
||||
<Section>
|
||||
<Row>
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Container, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailFooter } from "../general/email-footer";
|
||||
|
||||
interface InviteAcceptedEmailProps {
|
||||
inviterName: string;
|
||||
inviteeName: string;
|
||||
}
|
||||
|
||||
export function InviteAcceptedEmail({
|
||||
inviterName,
|
||||
inviteeName,
|
||||
}: InviteAcceptedEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<Text>Hey {inviterName},</Text>
|
||||
<Text>Just letting you know that {inviteeName} accepted your invitation. Have fun collaborating! </Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Container, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailButton } from "../general/email-button";
|
||||
import { EmailFooter } from "../general/email-footer";
|
||||
|
||||
interface InviteEmailProps {
|
||||
inviteeName: string;
|
||||
inviterName: string;
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export function InviteEmail({ inviteeName, inviterName, verifyLink }: InviteEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<Text>Hey {inviteeName},</Text>
|
||||
<Text>
|
||||
Your colleague {inviterName} invited you to join them at Formbricks. To accept the invitation, please
|
||||
click the link below:
|
||||
</Text>
|
||||
<EmailButton href={verifyLink} label="Join organization" />
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailButton } from "../general/email-button";
|
||||
import { EmailFooter } from "../general/email-footer";
|
||||
|
||||
interface OnboardingInviteEmailProps {
|
||||
inviteMessage: string;
|
||||
inviterName: string;
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export function OnboardingInviteEmail({
|
||||
inviteMessage,
|
||||
inviterName,
|
||||
verifyLink,
|
||||
}: OnboardingInviteEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<Heading>Hey 👋</Heading>
|
||||
<Text>{inviteMessage}</Text>
|
||||
<Text className="font-medium">Get Started in Minutes</Text>
|
||||
<ol>
|
||||
<li>Create an account to join {inviterName}'s organization.</li>
|
||||
<li>Connect Formbricks to your app or website via HTML Snippet or NPM in just a few minutes.</li>
|
||||
<li>Done ✅</li>
|
||||
</ol>
|
||||
<EmailButton href={verifyLink} label={`Join ${inviterName}'s organization`} />
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -18,7 +18,7 @@ import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { isLight, mixColor } from "@formbricks/lib/utils/colors";
|
||||
import { type TSurvey, TSurveyQuestionTypeEnum, type TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { RatingSmiley } from "@formbricks/ui/components/RatingSmiley";
|
||||
import { getNPSOptionColor, getRatingNumberOptionColor } from "../../lib/utils";
|
||||
import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils";
|
||||
|
||||
interface PreviewEmailTemplateProps {
|
||||
survey: TSurvey;
|
||||
@@ -26,11 +26,11 @@ interface PreviewEmailTemplateProps {
|
||||
styling: TSurveyStyling;
|
||||
}
|
||||
|
||||
export const getPreviewEmailTemplateHtml = (
|
||||
export const getPreviewEmailTemplateHtml = async (
|
||||
survey: TSurvey,
|
||||
surveyUrl: string,
|
||||
styling: TSurveyStyling
|
||||
): string => {
|
||||
): Promise<string> => {
|
||||
return render(<PreviewEmailTemplate styling={styling} survey={survey} surveyUrl={surveyUrl} />, {
|
||||
pretty: true,
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
interface EmbedSurveyPreviewEmailProps {
|
||||
html: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export function EmbedSurveyPreviewEmail({
|
||||
html,
|
||||
environmentId,
|
||||
}: EmbedSurveyPreviewEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<Heading>Preview Email Embed</Heading>
|
||||
<Text>This is how the code snippet looks embedded into an email:</Text>
|
||||
<Text className="text-sm">
|
||||
<b>Didn't request this?</b> Help us fight spam and forward this mail to hola@formbricks.com
|
||||
</Text>
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<Text className="text-center text-sm text-slate-700">Environment ID: {environmentId}</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailButton } from "../general/email-button";
|
||||
import { EmailFooter } from "../general/email-footer";
|
||||
|
||||
interface LinkSurveyEmailProps {
|
||||
surveyName: string;
|
||||
getSurveyLink: () => string;
|
||||
}
|
||||
|
||||
export function LinkSurveyEmail({ surveyName, getSurveyLink }: LinkSurveyEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<Heading>Hey 👋</Heading>
|
||||
<Text>Thanks for validating your email!</Text>
|
||||
<Text>To fill out the survey please click on the button below:</Text>
|
||||
<EmailButton href={getSurveyLink()} label="Take survey" />
|
||||
<Text className="text-xs text-slate-400">Survey name: {surveyName}</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import { Column, Container, Hr, Img, Link, Row, Section, Text } from "@react-email/components";
|
||||
import { FileDigitIcon, FileType2Icon } from "lucide-react";
|
||||
import { getQuestionResponseMapping } from "@formbricks/lib/responses";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import type { TOrganization } from "@formbricks/types/organizations";
|
||||
import type { TResponse } from "@formbricks/types/responses";
|
||||
import {
|
||||
type TSurvey,
|
||||
type TSurveyQuestionType,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { EmailButton } from "../general/email-button";
|
||||
|
||||
export const renderEmailResponseValue = (
|
||||
response: string | string[],
|
||||
questionType: TSurveyQuestionType
|
||||
): React.JSX.Element => {
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionTypeEnum.FileUpload:
|
||||
return (
|
||||
<Container>
|
||||
{Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Link
|
||||
className="mt-2 flex flex-col items-center justify-center rounded-lg bg-gray-200 p-2 text-black shadow-sm"
|
||||
href={responseItem}
|
||||
key={responseItem}>
|
||||
<FileIcon />
|
||||
<Text className="mx-auto mb-0 truncate">{getOriginalFileNameFromUrl(responseItem)}</Text>
|
||||
</Link>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
|
||||
case TSurveyQuestionTypeEnum.PictureSelection:
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
{Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Column key={responseItem}>
|
||||
<Img alt={responseItem.split("/").pop()} className="m-2 h-28" src={responseItem} />
|
||||
</Column>
|
||||
))}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
return (
|
||||
<Container>
|
||||
<Row className="my-1 font-semibold text-slate-700" dir="auto">
|
||||
{Array.isArray(response) &&
|
||||
response.map(
|
||||
(item, index) =>
|
||||
item && (
|
||||
<Row key={index} className="mb-1 flex items-center">
|
||||
<Column className="w-6 text-gray-400">#{index + 1}</Column>
|
||||
<Column className="rounded bg-gray-100 px-2 py-1">{item}</Column>
|
||||
</Row>
|
||||
)
|
||||
)}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
default:
|
||||
return <Text className="mt-0 whitespace-pre-wrap break-words font-bold">{response}</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
interface ResponseFinishedEmailProps {
|
||||
survey: TSurvey;
|
||||
responseCount: number;
|
||||
response: TResponse;
|
||||
WEBAPP_URL: string;
|
||||
environmentId: string;
|
||||
organization: TOrganization;
|
||||
}
|
||||
|
||||
export function ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
}: ResponseFinishedEmailProps): React.JSX.Element {
|
||||
const questions = getQuestionResponseMapping(survey, response);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="mb-4 text-3xl font-bold">Hey 👋</Text>
|
||||
<Text className="mb-4">
|
||||
Congrats, you received a new response to your survey! Someone just completed your survey{" "}
|
||||
<strong>{survey.name}</strong>:
|
||||
</Text>
|
||||
<Hr />
|
||||
{questions.map((question) => {
|
||||
if (!question.response) return;
|
||||
return (
|
||||
<Row key={question.question}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 font-medium">{question.question}</Text>
|
||||
{renderEmailResponseValue(question.response, question.type)}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
{survey.variables.map((variable) => {
|
||||
const variableResponse = response.variables[variable.id];
|
||||
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">
|
||||
{variable.type === "number" ? (
|
||||
<FileDigitIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<FileType2Icon className="h-4 w-4" />
|
||||
)}
|
||||
{variable.name}
|
||||
</Text>
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words font-bold">{variableResponse}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
{survey.hiddenFields.fieldIds?.map((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = response.data[hiddenFieldId];
|
||||
if (hiddenFieldResponse && typeof hiddenFieldResponse === "string") {
|
||||
return (
|
||||
<Row key={hiddenFieldId}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 flex items-center gap-2 font-medium">
|
||||
{hiddenFieldId} <EyeOffIcon />
|
||||
</Text>
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words font-bold">
|
||||
{hiddenFieldResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
<EmailButton
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=email_notification&utm_medium=email&utm_content=view_responses_CTA`}
|
||||
label={
|
||||
responseCount > 1
|
||||
? `View ${String(responseCount - 1).toString()} more ${responseCount === 2 ? "response" : "responses"}`
|
||||
: `View survey summary`
|
||||
}
|
||||
/>
|
||||
<Hr />
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
<Text className="font-bold">Don't want to get these notifications?</Text>
|
||||
<Text className="mb-0">
|
||||
Turn off notifications for{" "}
|
||||
<Link
|
||||
className="text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=alert&elementId=${survey.id}`}>
|
||||
this form
|
||||
</Link>
|
||||
</Text>
|
||||
<Text className="mt-0">
|
||||
Turn off notifications for{" "}
|
||||
<Link
|
||||
className="text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=unsubscribedOrganizationIds&elementId=${organization.id}`}>
|
||||
all newly created forms{" "}
|
||||
</Link>
|
||||
</Text>
|
||||
</Section>
|
||||
</Column>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function FileIcon(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
className="lucide lucide-file"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
||||
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function EyeOffIcon(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-eye-off h-4 w-4 rounded-lg bg-slate-200 p-1">
|
||||
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" />
|
||||
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" />
|
||||
<path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" />
|
||||
<line x1="2" x2="22" y1="2" y2="22" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface ForgotPasswordEmailProps {
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export function ForgotPasswordEmail({ verifyLink }: ForgotPasswordEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Heading>Change password</Heading>
|
||||
<Text>
|
||||
You have requested a link to change your password. You can do this by clicking the link below:
|
||||
</Text>
|
||||
<EmailButton href={verifyLink} label="Change password" />
|
||||
<Text className="font-bold">The link is valid for 24 hours.</Text>
|
||||
<Text className="mb-0">If you didn't request this, please ignore this email.</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default ForgotPasswordEmail;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
export function PasswordResetNotifyEmail(): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Heading>Password changed</Heading>
|
||||
<Text>Your password has been changed successfully.</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default PasswordResetNotifyEmail;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface VerificationEmailProps {
|
||||
verifyLink: string;
|
||||
verificationRequestLink: string;
|
||||
}
|
||||
|
||||
export function VerificationEmail({
|
||||
verifyLink,
|
||||
verificationRequestLink,
|
||||
}: VerificationEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Heading>Almost there!</Heading>
|
||||
<Text>To start using Formbricks please verify your email below:</Text>
|
||||
<EmailButton href={verifyLink} label="Verify email" />
|
||||
<Text>You can also click on this link:</Text>
|
||||
<Link className="break-all text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="font-bold">The link is valid for 24h.</Text>
|
||||
<Text>
|
||||
If it has expired please request a new token here:{" "}
|
||||
<Link className="text-black underline" href={verificationRequestLink}>
|
||||
Request new verification
|
||||
</Link>
|
||||
</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default VerificationEmail;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Container, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface InviteAcceptedEmailProps {
|
||||
inviterName: string;
|
||||
inviteeName: string;
|
||||
}
|
||||
|
||||
export function InviteAcceptedEmail({
|
||||
inviterName,
|
||||
inviteeName,
|
||||
}: InviteAcceptedEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Text>Hey {inviterName},</Text>
|
||||
<Text>
|
||||
Just letting you know that {inviteeName} accepted your invitation. Have fun collaborating!{" "}
|
||||
</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteAcceptedEmail;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Container, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
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 function InviteEmail({ inviteeName, inviterName, verifyLink }: InviteEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Text>Hey {inviteeName},</Text>
|
||||
<Text>
|
||||
Your colleague {inviterName} invited you to join them at Formbricks. To accept the invitation,
|
||||
please click the link below:
|
||||
</Text>
|
||||
<EmailButton href={verifyLink} label="Join organization" />
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteEmail;
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export function OnboardingInviteEmail({
|
||||
inviteMessage,
|
||||
inviterName,
|
||||
verifyLink,
|
||||
}: OnboardingInviteEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Heading>Hey 👋</Heading>
|
||||
<Text>{inviteMessage}</Text>
|
||||
<Text className="font-medium">Get Started in Minutes</Text>
|
||||
<ol>
|
||||
<li>Create an account to join {inviterName}'s organization.</li>
|
||||
<li>Connect Formbricks to your app or website via HTML Snippet or NPM in just a few minutes.</li>
|
||||
<li>Done ✅</li>
|
||||
</ol>
|
||||
<EmailButton href={verifyLink} label={`Join ${inviterName}'s organization`} />
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default OnboardingInviteEmail;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface EmbedSurveyPreviewEmailProps {
|
||||
html: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export function EmbedSurveyPreviewEmail({
|
||||
html,
|
||||
environmentId,
|
||||
}: EmbedSurveyPreviewEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Heading>Preview Email Embed</Heading>
|
||||
<Text>This is how the code snippet looks embedded into an email:</Text>
|
||||
<Text className="text-sm">
|
||||
<b>Didn't request this?</b> Help us fight spam and forward this mail to hola@formbricks.com
|
||||
</Text>
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<Text className="text-center text-sm text-slate-700">Environment ID: {environmentId}</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmbedSurveyPreviewEmail;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface LinkSurveyEmailProps {
|
||||
surveyName: string;
|
||||
surveyLink: string;
|
||||
}
|
||||
|
||||
export function LinkSurveyEmail({ surveyName, surveyLink }: LinkSurveyEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Heading>Hey 👋</Heading>
|
||||
<Text>Thanks for validating your email!</Text>
|
||||
<Text>To fill out the survey please click on the button below:</Text>
|
||||
<EmailButton href={surveyLink} label="Take survey" />
|
||||
<Text className="text-xs text-slate-400">Survey name: {surveyName}</Text>
|
||||
<EmailFooter />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkSurveyEmail;
|
||||
@@ -0,0 +1,228 @@
|
||||
import { Column, Container, Hr, Img, Link, Row, Section, Text } from "@react-email/components";
|
||||
import { FileDigitIcon, FileType2Icon } from "lucide-react";
|
||||
import { getQuestionResponseMapping } from "@formbricks/lib/responses";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import type { TOrganization } from "@formbricks/types/organizations";
|
||||
import type { TResponse } from "@formbricks/types/responses";
|
||||
import {
|
||||
type TSurvey,
|
||||
type TSurveyQuestionType,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
export const renderEmailResponseValue = (
|
||||
response: string | string[],
|
||||
questionType: TSurveyQuestionType
|
||||
): React.JSX.Element => {
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionTypeEnum.FileUpload:
|
||||
return (
|
||||
<Container>
|
||||
{Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Link
|
||||
className="mt-2 flex flex-col items-center justify-center rounded-lg bg-gray-200 p-2 text-black shadow-sm"
|
||||
href={responseItem}
|
||||
key={responseItem}>
|
||||
<FileIcon />
|
||||
<Text className="mx-auto mb-0 truncate">{getOriginalFileNameFromUrl(responseItem)}</Text>
|
||||
</Link>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
|
||||
case TSurveyQuestionTypeEnum.PictureSelection:
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
{Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Column key={responseItem}>
|
||||
<Img alt={responseItem.split("/").pop()} className="m-2 h-28" src={responseItem} />
|
||||
</Column>
|
||||
))}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
return (
|
||||
<Container>
|
||||
<Row className="my-1 font-semibold text-slate-700" dir="auto">
|
||||
{Array.isArray(response) &&
|
||||
response.map(
|
||||
(item, index) =>
|
||||
item && (
|
||||
<Row key={item} className="mb-1 flex items-center">
|
||||
<Column className="w-6 text-gray-400">#{index + 1}</Column>
|
||||
<Column className="rounded bg-gray-100 px-2 py-1">{item}</Column>
|
||||
</Row>
|
||||
)
|
||||
)}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
default:
|
||||
return <Text className="mt-0 whitespace-pre-wrap break-words font-bold">{response}</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
interface ResponseFinishedEmailProps {
|
||||
survey: TSurvey;
|
||||
responseCount: number;
|
||||
response: TResponse;
|
||||
WEBAPP_URL: string;
|
||||
environmentId: string;
|
||||
organization: TOrganization;
|
||||
}
|
||||
|
||||
export function ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
}: ResponseFinishedEmailProps): React.JSX.Element {
|
||||
const questions = getQuestionResponseMapping(survey, response);
|
||||
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Container>
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="mb-4 text-3xl font-bold">Hey 👋</Text>
|
||||
<Text className="mb-4">
|
||||
Congrats, you received a new response to your survey! Someone just completed your survey{" "}
|
||||
<strong>{survey.name}</strong>:
|
||||
</Text>
|
||||
<Hr />
|
||||
{questions.map((question) => {
|
||||
if (!question.response) return;
|
||||
return (
|
||||
<Row key={question.question}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 font-medium">{question.question}</Text>
|
||||
{renderEmailResponseValue(question.response, question.type)}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
{survey.variables.map((variable) => {
|
||||
const variableResponse = response.variables[variable.id];
|
||||
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">
|
||||
{variable.type === "number" ? (
|
||||
<FileDigitIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<FileType2Icon className="h-4 w-4" />
|
||||
)}
|
||||
{variable.name}
|
||||
</Text>
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words font-bold">
|
||||
{variableResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
{survey.hiddenFields.fieldIds?.map((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = response.data[hiddenFieldId];
|
||||
if (hiddenFieldResponse && typeof hiddenFieldResponse === "string") {
|
||||
return (
|
||||
<Row key={hiddenFieldId}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 flex items-center gap-2 font-medium">
|
||||
{hiddenFieldId} <EyeOffIcon />
|
||||
</Text>
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words font-bold">
|
||||
{hiddenFieldResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
<EmailButton
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=email_notification&utm_medium=email&utm_content=view_responses_CTA`}
|
||||
label={
|
||||
responseCount > 1
|
||||
? `View ${String(responseCount - 1).toString()} more ${responseCount === 2 ? "response" : "responses"}`
|
||||
: `View survey summary`
|
||||
}
|
||||
/>
|
||||
<Hr />
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
<Text className="font-bold">Don't want to get these notifications?</Text>
|
||||
<Text className="mb-0">
|
||||
Turn off notifications for{" "}
|
||||
<Link
|
||||
className="text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=alert&elementId=${survey.id}`}>
|
||||
this form
|
||||
</Link>
|
||||
</Text>
|
||||
<Text className="mt-0">
|
||||
Turn off notifications for{" "}
|
||||
<Link
|
||||
className="text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=unsubscribedOrganizationIds&elementId=${organization.id}`}>
|
||||
all newly created forms{" "}
|
||||
</Link>
|
||||
</Text>
|
||||
</Section>
|
||||
</Column>
|
||||
</Row>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function FileIcon(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
className="lucide lucide-file"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
||||
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function EyeOffIcon(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-eye-off h-4 w-4 rounded-lg bg-slate-200 p-1">
|
||||
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" />
|
||||
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" />
|
||||
<path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" />
|
||||
<line x1="2" x2="22" y1="2" y2="22" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Container, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary";
|
||||
import { EmailButton } from "../general/email-button";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { NotificationFooter } from "./notification-footer";
|
||||
|
||||
interface CreateReminderNotificationBodyProps {
|
||||
+1
-1
@@ -6,7 +6,7 @@ import type {
|
||||
TWeeklySummaryNotificationDataSurvey,
|
||||
TWeeklySummarySurveyResponseData,
|
||||
} from "@formbricks/types/weekly-summary";
|
||||
import { EmailButton } from "../general/email-button";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { renderEmailResponseValue } from "../survey/response-finished-email";
|
||||
|
||||
const getButtonLabel = (count: number): string => {
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
import { LiveSurveyNotification } from "./live-survey-notification";
|
||||
import { NotificationFooter } from "./notification-footer";
|
||||
import { NotificationHeader } from "./notification-header";
|
||||
@@ -21,7 +22,7 @@ export function WeeklySummaryNotificationEmail({
|
||||
endYear,
|
||||
}: WeeklySummaryNotificationEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<EmailTemplate>
|
||||
<NotificationHeader
|
||||
endDate={endDate}
|
||||
endYear={endYear}
|
||||
@@ -35,6 +36,6 @@ export function WeeklySummaryNotificationEmail({
|
||||
surveys={notificationData.surveys}
|
||||
/>
|
||||
<NotificationFooter environmentId={notificationData.environmentId} />
|
||||
</div>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
+62
-57
@@ -18,18 +18,17 @@ import type { TLinkSurveyEmailData } from "@formbricks/types/email";
|
||||
import type { TResponse } from "@formbricks/types/responses";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary";
|
||||
import { ForgotPasswordEmail } from "./components/auth/forgot-password-email";
|
||||
import { PasswordResetNotifyEmail } from "./components/auth/password-reset-notify-email";
|
||||
import { VerificationEmail } from "./components/auth/verification-email";
|
||||
import { EmailTemplate } from "./components/general/email-template";
|
||||
import { InviteAcceptedEmail } from "./components/invite/invite-accepted-email";
|
||||
import { InviteEmail } from "./components/invite/invite-email";
|
||||
import { OnboardingInviteEmail } from "./components/invite/onboarding-invite-email";
|
||||
import { EmbedSurveyPreviewEmail } from "./components/survey/embed-survey-preview-email";
|
||||
import { LinkSurveyEmail } from "./components/survey/link-survey-email";
|
||||
import { ResponseFinishedEmail } from "./components/survey/response-finished-email";
|
||||
import { NoLiveSurveyNotificationEmail } from "./components/weekly-summary/no-live-survey-notification-email";
|
||||
import { WeeklySummaryNotificationEmail } from "./components/weekly-summary/weekly-summary-notification-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 { 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";
|
||||
import { NoLiveSurveyNotificationEmail } from "./emails/weekly-summary/no-live-survey-notification-email";
|
||||
import { WeeklySummaryNotificationEmail } from "./emails/weekly-summary/weekly-summary-notification-email";
|
||||
|
||||
export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT);
|
||||
|
||||
@@ -81,10 +80,11 @@ export const sendVerificationEmail = async (user: TEmailUser): Promise<void> =>
|
||||
const verificationRequestLink = `${WEBAPP_URL}/auth/verification-requested?email=${encodeURIComponent(
|
||||
user.email
|
||||
)}`;
|
||||
const html = await render(VerificationEmail({ verificationRequestLink, verifyLink }));
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Please verify your email to use Formbricks",
|
||||
html: render(EmailTemplate({ content: VerificationEmail({ verificationRequestLink, verifyLink }) })),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -93,18 +93,20 @@ export const sendForgotPasswordEmail = async (user: TEmailUser): Promise<void> =
|
||||
expiresIn: "1d",
|
||||
});
|
||||
const verifyLink = `${WEBAPP_URL}/auth/forgot-password/reset?token=${encodeURIComponent(token)}`;
|
||||
const html = await render(ForgotPasswordEmail({ verifyLink }));
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Reset your Formbricks password",
|
||||
html: render(EmailTemplate({ content: ForgotPasswordEmail({ verifyLink }) })),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendPasswordResetNotifyEmail = async (user: TEmailUser): Promise<void> => {
|
||||
const html = await render(PasswordResetNotifyEmail());
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Your Formbricks password has been changed",
|
||||
html: render(EmailTemplate({ content: PasswordResetNotifyEmail() })),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -123,18 +125,18 @@ export const sendInviteMemberEmail = async (
|
||||
const verifyLink = `${WEBAPP_URL}/invite?token=${encodeURIComponent(token)}`;
|
||||
|
||||
if (isOnboardingInvite && inviteMessage) {
|
||||
const html = await render(OnboardingInviteEmail({ verifyLink, inviteMessage, inviterName }));
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: `${inviterName} needs a hand setting up Formbricks. Can you help out?`,
|
||||
html: render(
|
||||
EmailTemplate({ content: OnboardingInviteEmail({ verifyLink, inviteMessage, inviterName }) })
|
||||
),
|
||||
html,
|
||||
});
|
||||
} else {
|
||||
const html = await render(InviteEmail({ inviteeName, inviterName, verifyLink }));
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: `You're invited to collaborate on Formbricks!`,
|
||||
html: render(EmailTemplate({ content: InviteEmail({ inviteeName, inviterName, verifyLink }) })),
|
||||
html,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -144,10 +146,11 @@ export const sendInviteAcceptedEmail = async (
|
||||
inviteeName: string,
|
||||
email: string
|
||||
): Promise<void> => {
|
||||
const html = await render(InviteAcceptedEmail({ inviteeName, inviterName }));
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: `You've got a new organization member!`,
|
||||
html: render(EmailTemplate({ content: InviteAcceptedEmail({ inviteeName, inviterName }) })),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -165,37 +168,38 @@ export const sendResponseFinishedEmail = async (
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const html = await render(
|
||||
ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
})
|
||||
);
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: personEmail
|
||||
? `${personEmail} just completed your ${survey.name} survey ✅`
|
||||
: `A response for ${survey.name} was completed ✅`,
|
||||
replyTo: personEmail?.toString() ?? MAIL_FROM,
|
||||
html: render(
|
||||
EmailTemplate({
|
||||
content: ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
}),
|
||||
})
|
||||
),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendEmbedSurveyPreviewEmail = async (
|
||||
to: string,
|
||||
subject: string,
|
||||
html: string,
|
||||
innerHtml: string,
|
||||
environmentId: string
|
||||
): Promise<void> => {
|
||||
const html = await render(EmbedSurveyPreviewEmail({ html: innerHtml, environmentId }));
|
||||
await sendEmail({
|
||||
to,
|
||||
subject,
|
||||
html: render(EmailTemplate({ content: EmbedSurveyPreviewEmail({ html, environmentId }) })),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -211,10 +215,13 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
|
||||
}
|
||||
return `${WEBAPP_URL}/s/${surveyId}?verify=${encodeURIComponent(token)}`;
|
||||
};
|
||||
const surveyLink = getSurveyLink();
|
||||
|
||||
const html = await render(LinkSurveyEmail({ surveyName, surveyLink }));
|
||||
await sendEmail({
|
||||
to: data.email,
|
||||
subject: "Your survey is ready to be filled out.",
|
||||
html: render(EmailTemplate({ content: LinkSurveyEmail({ surveyName, getSurveyLink }) })),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -232,20 +239,19 @@ export const sendWeeklySummaryNotificationEmail = async (
|
||||
)}`;
|
||||
const startYear = notificationData.lastWeekDate.getFullYear();
|
||||
const endYear = notificationData.currentDate.getFullYear();
|
||||
const html = await render(
|
||||
WeeklySummaryNotificationEmail({
|
||||
notificationData,
|
||||
startDate,
|
||||
endDate,
|
||||
startYear,
|
||||
endYear,
|
||||
})
|
||||
);
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: getEmailSubject(notificationData.productName),
|
||||
html: render(
|
||||
EmailTemplate({
|
||||
content: WeeklySummaryNotificationEmail({
|
||||
notificationData,
|
||||
startDate,
|
||||
endDate,
|
||||
startYear,
|
||||
endYear,
|
||||
}),
|
||||
})
|
||||
),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -263,19 +269,18 @@ export const sendNoLiveSurveyNotificationEmail = async (
|
||||
)}`;
|
||||
const startYear = notificationData.lastWeekDate.getFullYear();
|
||||
const endYear = notificationData.currentDate.getFullYear();
|
||||
const html = await render(
|
||||
NoLiveSurveyNotificationEmail({
|
||||
notificationData,
|
||||
startDate,
|
||||
endDate,
|
||||
startYear,
|
||||
endYear,
|
||||
})
|
||||
);
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: getEmailSubject(notificationData.productName),
|
||||
html: render(
|
||||
EmailTemplate({
|
||||
content: NoLiveSurveyNotificationEmail({
|
||||
notificationData,
|
||||
startDate,
|
||||
endDate,
|
||||
startYear,
|
||||
endYear,
|
||||
}),
|
||||
})
|
||||
),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"description": "Email package",
|
||||
"main": "./index.tsx",
|
||||
"scripts": {
|
||||
"dev": "email dev --port 3003",
|
||||
"clean": "rimraf .turbo node_modules dist",
|
||||
"lint": "eslint --ext .ts,.tsx --fix ."
|
||||
},
|
||||
@@ -12,14 +13,14 @@
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@react-email/components": "^0.0.22",
|
||||
"@react-email/render": "^0.0.17",
|
||||
"lucide-react": "^0.427.0",
|
||||
"nodemailer": "^6.9.14",
|
||||
"react-email": "^2.1.6"
|
||||
"@react-email/components": "0.0.25",
|
||||
"@react-email/render": "1.0.1",
|
||||
"lucide-react": "0.451.0",
|
||||
"nodemailer": "6.9.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/react": "18.3.3"
|
||||
"@types/nodemailer": "6.4.16",
|
||||
"@types/react": "18.3.11",
|
||||
"react-email": "2.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["/*"]
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"strictNullChecks": true
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"terser": "^5.31.6",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-dts": "^3.9.1"
|
||||
"terser": "5.31.6",
|
||||
"vite": "5.4.8",
|
||||
"vite-plugin-dts": "3.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"terser": "^5.31.6",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-dts": "^3.9.1"
|
||||
"terser": "5.31.6",
|
||||
"vite": "5.4.8",
|
||||
"vite-plugin-dts": "3.9.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "3.x"
|
||||
|
||||
+22
-22
@@ -20,31 +20,31 @@
|
||||
"@formbricks/api": "*",
|
||||
"@formbricks/database": "*",
|
||||
"@formbricks/types": "*",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@t3-oss/env-nextjs": "^0.11.0",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
"aws-crt": "^1.21.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.7",
|
||||
"next-auth": "^4.24.7",
|
||||
"posthog-node": "^4.1.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"server-only": "^0.0.1",
|
||||
"superjson": "^2.2.1",
|
||||
"tailwind-merge": "^2.5.2"
|
||||
"@paralleldrive/cuid2": "2.2.2",
|
||||
"@t3-oss/env-nextjs": "0.11.0",
|
||||
"@ungap/structured-clone": "1.2.0",
|
||||
"aws-crt": "1.21.3",
|
||||
"date-fns": "3.6.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"markdown-it": "14.1.0",
|
||||
"mime-types": "2.1.35",
|
||||
"nanoid": "5.0.7",
|
||||
"next-auth": "4.24.7",
|
||||
"posthog-node": "4.1.0",
|
||||
"qrcode": "1.5.4",
|
||||
"server-only": "0.0.1",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/ungap__structured-clone": "^1.2.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"@types/jsonwebtoken": "9.0.6",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ungap__structured-clone": "1.2.0",
|
||||
"dotenv": "16.4.5",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"ts-node": "^10.9.2",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest-mock-extended": "^2.0.0"
|
||||
"ts-node": "10.9.2",
|
||||
"vitest": "2.0.5",
|
||||
"vitest-mock-extended": "2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"react-select": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -44,12 +44,12 @@
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@types/react": "^18.2.79",
|
||||
"react": "18.2.0",
|
||||
"react-native": "^0.74.5",
|
||||
"terser": "^5.31.3",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-dts": "^3.9.1"
|
||||
"@types/react": "18.3.11",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.74.5",
|
||||
"terser": "5.31.3",
|
||||
"vite": "5.4.8",
|
||||
"vite-plugin-dts": "3.9.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
|
||||
@@ -41,21 +41,21 @@
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@preact/preset-vite": "^2.9.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"@preact/preset-vite": "2.9.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"concurrently": "8.2.2",
|
||||
"isomorphic-dompurify": "^2.14.0",
|
||||
"postcss": "^8.4.41",
|
||||
"preact": "^10.23.2",
|
||||
"react-date-picker": "^11.0.0",
|
||||
"isomorphic-dompurify": "2.14.0",
|
||||
"postcss": "8.4.41",
|
||||
"preact": "10.23.2",
|
||||
"react-date-picker": "11.0.0",
|
||||
"serve": "14.2.3",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"terser": "^5.31.6",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-dts": "^3.9.1",
|
||||
"vite-tsconfig-paths": "^5.0.1"
|
||||
"tailwindcss": "3.4.10",
|
||||
"terser": "5.31.6",
|
||||
"vite": "5.4.8",
|
||||
"vite-plugin-dts": "3.9.1",
|
||||
"vite-tsconfig-paths": "5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2"
|
||||
"@formkit/auto-animate": "0.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"@formbricks/config-typescript": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8"
|
||||
"zod": "3.23.8"
|
||||
}
|
||||
}
|
||||
|
||||
+39
-39
@@ -12,50 +12,50 @@
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"concurrently": "^8.2.2",
|
||||
"postcss": "^8.4.41",
|
||||
"concurrently": "8.2.2",
|
||||
"postcss": "8.4.41",
|
||||
"react": "18.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/surveys": "workspace:*",
|
||||
"@lexical/code": "^0.17.0",
|
||||
"@lexical/link": "^0.17.0",
|
||||
"@lexical/list": "^0.17.0",
|
||||
"@lexical/markdown": "^0.17.0",
|
||||
"@lexical/react": "^0.17.0",
|
||||
"@lexical/rich-text": "^0.17.0",
|
||||
"@lexical/table": "^0.17.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-radio-group": "^1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slider": "^1.2.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"boring-avatars": "^1.10.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"lexical": "^0.17.0",
|
||||
"lucide-react": "^0.427.0",
|
||||
"mime": "^4.0.4",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-day-picker": "^9.0.8",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-radio-group": "^3.0.3",
|
||||
"react-use": "^17.5.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss": "^3.4.13"
|
||||
"@lexical/code": "0.17.0",
|
||||
"@lexical/link": "0.17.0",
|
||||
"@lexical/list": "0.17.0",
|
||||
"@lexical/markdown": "0.17.0",
|
||||
"@lexical/react": "0.17.0",
|
||||
"@lexical/rich-text": "0.17.0",
|
||||
"@lexical/table": "0.17.0",
|
||||
"@radix-ui/react-accordion": "1.2.0",
|
||||
"@radix-ui/react-checkbox": "1.1.1",
|
||||
"@radix-ui/react-dialog": "1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.1",
|
||||
"@radix-ui/react-label": "2.1.0",
|
||||
"@radix-ui/react-popover": "1.1.1",
|
||||
"@radix-ui/react-radio-group": "1.2.0",
|
||||
"@radix-ui/react-select": "2.1.1",
|
||||
"@radix-ui/react-slider": "1.2.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-switch": "1.1.0",
|
||||
"@radix-ui/react-tooltip": "1.1.2",
|
||||
"@tailwindcss/forms": "0.5.9",
|
||||
"@tailwindcss/typography": "0.5.13",
|
||||
"autoprefixer": "10.4.20",
|
||||
"boring-avatars": "1.10.2",
|
||||
"class-variance-authority": "0.7.0",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"lexical": "0.17.0",
|
||||
"lucide-react": "0.427.0",
|
||||
"mime": "4.0.4",
|
||||
"react-colorful": "5.6.1",
|
||||
"react-confetti": "6.1.0",
|
||||
"react-day-picker": "9.0.8",
|
||||
"react-hot-toast": "2.4.1",
|
||||
"react-radio-group": "3.0.3",
|
||||
"react-use": "17.5.1",
|
||||
"tailwind-merge": "2.5.2",
|
||||
"tailwindcss": "3.4.13"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user