chore: add react email dev server & fix package npm deps (#3350)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
Matti Nannt
2024-10-10 11:08:46 +02:00
committed by GitHub
parent cedb0b27c0
commit cf4eec5134
55 changed files with 2052 additions and 2434 deletions

3
.npmrc
View File

@@ -5,4 +5,5 @@ shared-workspace-shrinkwrap = true
access = public access = public
enable-pre-post-scripts = true enable-pre-post-scripts = true
legacy-peer-deps=true legacy-peer-deps=true
node-linker=hoisted node-linker=hoisted
save-exact=true

View File

@@ -13,16 +13,16 @@
"dependencies": { "dependencies": {
"@formbricks/js": "workspace:*", "@formbricks/js": "workspace:*",
"@formbricks/react-native": "workspace:*", "@formbricks/react-native": "workspace:*",
"expo": "^51.0.26", "expo": "51.0.26",
"expo-status-bar": "~1.12.1", "expo-status-bar": "1.12.1",
"react": "^18.2.0", "react": "18.3.1",
"react-native": "^0.74.4", "react-native": "0.74.4",
"react-native-webview": "13.8.6" "react-native-webview": "13.8.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "7.25.2",
"@types/react": "~18.2.79", "@types/react": "18.3.11",
"typescript": "^5.3.3" "typescript": "5.3.3"
}, },
"private": true "private": true
} }

View File

@@ -13,7 +13,7 @@
"dependencies": { "dependencies": {
"@formbricks/js": "workspace:*", "@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*", "@formbricks/ui": "workspace:*",
"lucide-react": "^0.418.0", "lucide-react": "0.418.0",
"next": "14.2.5", "next": "14.2.5",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1" "react-dom": "18.3.1"

View File

@@ -12,62 +12,62 @@
}, },
"browserslist": "defaults, not ie <= 11", "browserslist": "defaults, not ie <= 11",
"dependencies": { "dependencies": {
"@algolia/autocomplete-core": "^1.17.4", "@algolia/autocomplete-core": "1.17.4",
"@calcom/embed-react": "^1.5.1", "@calcom/embed-react": "1.5.1",
"@docsearch/css": "3", "@docsearch/css": "3",
"@docsearch/react": "^3.6.2", "@docsearch/react": "3.6.2",
"@formbricks/lib": "workspace:*", "@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*", "@formbricks/ui": "workspace:*",
"@headlessui/react": "^2.1.9", "@headlessui/react": "2.1.9",
"@headlessui/tailwindcss": "^0.2.1", "@headlessui/tailwindcss": "0.2.1",
"@mapbox/rehype-prism": "^0.9.0", "@mapbox/rehype-prism": "0.9.0",
"@mdx-js/loader": "^3.0.1", "@mdx-js/loader": "3.0.1",
"@mdx-js/react": "^3.0.1", "@mdx-js/react": "3.0.1",
"@next/mdx": "14.2.15", "@next/mdx": "14.2.15",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "2.2.2",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "2.2.1",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "0.5.15",
"acorn": "^8.12.1", "acorn": "8.12.1",
"autoprefixer": "^10.4.20", "autoprefixer": "10.4.20",
"clsx": "^2.1.1", "clsx": "2.1.1",
"fast-glob": "^3.3.2", "fast-glob": "3.3.2",
"flexsearch": "^0.7.43", "flexsearch": "0.7.43",
"framer-motion": "11.11.4", "framer-motion": "11.11.4",
"lottie-web": "^5.12.2", "lottie-web": "5.12.2",
"lucide": "^0.451.0", "lucide": "0.451.0",
"lucide-react": "^0.451.0", "lucide-react": "0.451.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "4.0.0",
"mdx-annotations": "^0.1.4", "mdx-annotations": "0.1.4",
"next": "14.2.15", "next": "14.2.15",
"next-plausible": "^3.12.2", "next-plausible": "3.12.2",
"next-seo": "^6.6.0", "next-seo": "6.6.0",
"next-sitemap": "^4.2.3", "next-sitemap": "4.2.3",
"next-themes": "^0.3.0", "next-themes": "0.3.0",
"node-fetch": "^3.3.2", "node-fetch": "3.3.2",
"prism-react-renderer": "^2.4.0", "prism-react-renderer": "2.4.0",
"prismjs": "^1.29.0", "prismjs": "1.29.0",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-highlight-words": "^0.20.0", "react-highlight-words": "0.20.0",
"react-markdown": "^9.0.1", "react-markdown": "9.0.1",
"react-responsive-embed": "^2.1.0", "react-responsive-embed": "2.1.0",
"remark": "^15.0.1", "remark": "15.0.1",
"remark-gfm": "^4.0.0", "remark-gfm": "4.0.0",
"remark-mdx": "^3.0.1", "remark-mdx": "3.0.1",
"schema-dts": "^1.1.2", "schema-dts": "1.1.2",
"sharp": "^0.33.5", "sharp": "0.33.5",
"shiki": "^0.14.7", "shiki": "0.14.7",
"simple-functional-loader": "^1.2.1", "simple-functional-loader": "1.2.1",
"tailwindcss": "^3.4.13", "tailwindcss": "3.4.13",
"unist-util-filter": "^5.0.1", "unist-util-filter": "5.0.1",
"unist-util-visit": "^5.0.0", "unist-util-visit": "5.0.0",
"zustand": "^4.5.5" "zustand": "4.5.5"
}, },
"devDependencies": { "devDependencies": {
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@types/dompurify": "^3.0.5", "@types/dompurify": "3.0.5",
"@types/react-highlight-words": "^0.20.0", "@types/react-highlight-words": "0.20.0",
"@formbricks/eslint-config": "workspace:*" "@formbricks/eslint-config": "workspace:*"
} }
} }

View File

@@ -12,30 +12,30 @@
}, },
"dependencies": { "dependencies": {
"@formbricks/ui": "workspace:*", "@formbricks/ui": "workspace:*",
"eslint-plugin-react-refresh": "^0.4.12", "eslint-plugin-react-refresh": "0.4.12",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1" "react-dom": "18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^2.0.2", "@chromatic-com/storybook": "2.0.2",
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@storybook/addon-a11y": "^8.3.5", "@storybook/addon-a11y": "8.3.5",
"@storybook/addon-essentials": "^8.3.5", "@storybook/addon-essentials": "8.3.5",
"@storybook/addon-interactions": "^8.3.5", "@storybook/addon-interactions": "8.3.5",
"@storybook/addon-links": "^8.3.5", "@storybook/addon-links": "8.3.5",
"@storybook/addon-onboarding": "^8.3.5", "@storybook/addon-onboarding": "8.3.5",
"@storybook/blocks": "^8.3.5", "@storybook/blocks": "8.3.5",
"@storybook/react": "^8.3.5", "@storybook/react": "8.3.5",
"@storybook/react-vite": "^8.3.5", "@storybook/react-vite": "8.3.5",
"@storybook/test": "^8.3.5", "@storybook/test": "8.3.5",
"@typescript-eslint/eslint-plugin": "^8.8.1", "@typescript-eslint/eslint-plugin": "8.8.1",
"@typescript-eslint/parser": "^8.8.1", "@typescript-eslint/parser": "8.8.1",
"@vitejs/plugin-react": "^4.3.2", "@vitejs/plugin-react": "4.3.2",
"esbuild": "^0.24.0", "esbuild": "0.24.0",
"eslint-plugin-storybook": "^0.9.0", "eslint-plugin-storybook": "0.9.0",
"prop-types": "^15.8.1", "prop-types": "15.8.1",
"storybook": "^8.3.5", "storybook": "8.3.5",
"tsup": "^8.3.0", "tsup": "8.3.0",
"vite": "^5.4.8" "vite": "5.4.8"
} }
} }

View File

@@ -1,4 +1,4 @@
import { getPreviewEmailTemplateHtml } from "@formbricks/email/components/survey/preview-email-template"; import { getPreviewEmailTemplateHtml } from "@formbricks/email/components/preview-email-template";
import { WEBAPP_URL } from "@formbricks/lib/constants"; import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSurvey } from "@formbricks/lib/survey/service"; import { getSurvey } from "@formbricks/lib/survey/service";
@@ -16,7 +16,7 @@ export const getEmailTemplateHtml = async (surveyId: string) => {
const styling = getStyling(product, survey); const styling = getStyling(product, survey);
const surveyUrl = WEBAPP_URL + "/s/" + survey.id; const surveyUrl = WEBAPP_URL + "/s/" + survey.id;
const html = getPreviewEmailTemplateHtml(survey, surveyUrl, styling); const html = await getPreviewEmailTemplateHtml(survey, surveyUrl, styling);
const doctype = const doctype =
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
const htmlCleaned = html.toString().replace(doctype, ""); const htmlCleaned = html.toString().replace(doctype, "");

View File

@@ -12,10 +12,10 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "^7.0.0", "@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "^8.0.0", "@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "3.2.2",
"@formbricks/api": "workspace:*", "@formbricks/api": "workspace:*",
"@formbricks/database": "workspace:*", "@formbricks/database": "workspace:*",
"@formbricks/ee": "workspace:*", "@formbricks/ee": "workspace:*",
@@ -26,52 +26,52 @@
"@formbricks/surveys": "workspace:*", "@formbricks/surveys": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*", "@formbricks/ui": "workspace:*",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "3.9.0",
"@json2csv/node": "^7.0.6", "@json2csv/node": "7.0.6",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "2.2.2",
"@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-collapsible": "1.1.1",
"@react-email/components": "^0.0.25", "@react-email/components": "0.0.25",
"@sentry/nextjs": "^8.33.1", "@sentry/nextjs": "8.33.1",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "8.20.5",
"@vercel/og": "^0.6.3", "@vercel/og": "0.6.3",
"@vercel/speed-insights": "^1.0.12", "@vercel/speed-insights": "1.0.12",
"bcryptjs": "^2.4.3", "bcryptjs": "2.4.3",
"dotenv": "^16.4.5", "dotenv": "16.4.5",
"encoding": "^0.1.13", "encoding": "0.1.13",
"file-loader": "^6.2.0", "file-loader": "6.2.0",
"framer-motion": "11.11.4", "framer-motion": "11.11.4",
"googleapis": "^144.0.0", "googleapis": "144.0.0",
"jiti": "^2.3.3", "jiti": "2.3.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "9.0.2",
"lodash": "^4.17.21", "lodash": "4.17.21",
"lru-cache": "^11.0.1", "lru-cache": "11.0.1",
"lucide-react": "^0.451.0", "lucide-react": "0.451.0",
"mime": "^4.0.4", "mime": "4.0.4",
"next": "14.2.15", "next": "14.2.15",
"next-safe-action": "^7.9.3", "next-safe-action": "7.9.3",
"optional": "^0.1.4", "optional": "0.1.4",
"otplib": "^12.0.1", "otplib": "12.0.1",
"papaparse": "^5.4.1", "papaparse": "5.4.1",
"posthog-js": "^1.167.0", "posthog-js": "1.167.0",
"prismjs": "^1.29.0", "prismjs": "1.29.0",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-hook-form": "^7.53.0", "react-hook-form": "7.53.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "2.4.1",
"redis": "^4.7.0", "redis": "4.7.0",
"sharp": "^0.33.5", "sharp": "0.33.5",
"ua-parser-js": "^1.0.39", "ua-parser-js": "1.0.39",
"webpack": "^5.95.0", "webpack": "5.95.0",
"xlsx": "^0.18.5" "xlsx": "0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"@neshca/cache-handler": "^1.7.3", "@neshca/cache-handler": "1.7.3",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "2.4.6",
"@types/lodash": "^4.17.10", "@types/lodash": "4.17.10",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "14.1.2",
"@types/papaparse": "^5.3.14", "@types/papaparse": "5.3.14",
"@types/qrcode": "^1.5.5" "@types/qrcode": "1.5.5"
} }
} }

View File

@@ -32,14 +32,14 @@
"storybook": "turbo run storybook" "storybook": "turbo run storybook"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.45.3", "@playwright/test": "1.45.3",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"eslint": "^8.57.0", "eslint": "8.57.0",
"husky": "^9.1.4", "husky": "9.1.4",
"lint-staged": "^15.2.7", "lint-staged": "15.2.7",
"rimraf": "^6.0.1", "rimraf": "6.0.1",
"tsx": "^4.16.5", "tsx": "4.16.5",
"turbo": "^2.0.11" "turbo": "2.0.11"
}, },
"lint-staged": { "lint-staged": {
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [ "(apps|packages)/**/*.{js,ts,jsx,tsx}": [
@@ -64,7 +64,7 @@
"showDetails": true "showDetails": true
}, },
"dependencies": { "dependencies": {
"@changesets/cli": "^2.27.7", "@changesets/cli": "2.27.7",
"playwright": "^1.45.3" "playwright": "1.45.3"
} }
} }

View File

@@ -37,11 +37,11 @@
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-inject": "5.0.5",
"buffer": "^6.0.3", "buffer": "6.0.3",
"terser": "^5.31.6", "terser": "5.31.6",
"vite": "^5.4.1", "vite": "5.4.8",
"vite-plugin-dts": "^3.9.1", "vite-plugin-dts": "3.9.1",
"vite-plugin-node-polyfills": "^0.22.0" "vite-plugin-node-polyfills": "0.22.0"
} }
} }

View File

@@ -3,15 +3,15 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@next/eslint-plugin-next": "^14.2.5", "@next/eslint-plugin-next": "14.2.5",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "8.0.0",
"@vercel/style-guide": "^6.0.0", "@vercel/style-guide": "6.0.0",
"eslint-config-next": "^14.2.5", "eslint-config-next": "14.2.5",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-config-turbo": "^2.0.14", "eslint-config-turbo": "2.0.14",
"eslint-plugin-react": "7.35.0", "eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-react-refresh": "^0.4.9" "eslint-plugin-react-refresh": "0.4.9"
} }
} }

View File

@@ -7,8 +7,8 @@
"clean": "rimraf node_modules .turbo" "clean": "rimraf node_modules .turbo"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "4.3.0",
"prettier": "^3.3.3", "prettier": "3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6" "prettier-plugin-tailwindcss": "0.6.6"
} }
} }

View File

@@ -8,7 +8,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "22.3.0", "@types/node": "22.3.0",
"@types/react": "18.3.3", "@types/react": "18.3.11",
"@types/react-dom": "18.3.0", "@types/react-dom": "18.3.0",
"typescript": "5.4.5" "typescript": "5.4.5"
} }

View File

@@ -55,20 +55,20 @@
"data-migration:migrate-survey-types": "ts-node ./data-migrations/20241002123456_migrate_survey_types/data-migration.ts" "data-migration:migrate-survey-types": "ts-node ./data-migrations/20241002123456_migrate_survey_types/data-migration.ts"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^5.18.0", "@prisma/client": "5.18.0",
"@prisma/extension-accelerate": "^1.1.0", "@prisma/extension-accelerate": "1.1.0",
"dotenv-cli": "^7.4.2" "dotenv-cli": "7.4.2"
}, },
"devDependencies": { "devDependencies": {
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "2.2.2",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"prisma": "^5.18.0", "prisma": "5.18.0",
"prisma-dbml-generator": "^0.12.0", "prisma-dbml-generator": "0.12.0",
"prisma-json-types-generator": "^3.0.4", "prisma-json-types-generator": "3.0.4",
"ts-node": "^10.9.2", "ts-node": "10.9.2",
"zod": "^3.23.8", "zod": "3.23.8",
"zod-prisma": "^0.5.4" "zod-prisma": "0.5.4"
} }
} }

View File

@@ -11,28 +11,28 @@
"lint": "eslint --ext .ts,.tsx --fix ." "lint": "eslint --ext .ts,.tsx --fix ."
}, },
"devDependencies": { "devDependencies": {
"@formbricks/config-typescript": "*", "@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"@formbricks/lib": "*", "@formbricks/lib": "workspace:*",
"@formbricks/types": "*", "@formbricks/types": "workspace:*",
"@formbricks/ui": "*", "@formbricks/ui": "workspace:*",
"@types/dompurify": "^3.0.5", "@types/dompurify": "3.0.5",
"@types/react": "18.3.3" "@types/react": "18.3.11"
}, },
"dependencies": { "dependencies": {
"@formbricks/database": "workspace:*", "@formbricks/database": "workspace:*",
"@formbricks/lib": "workspace:*", "@formbricks/lib": "workspace:*",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "2.2.2",
"@radix-ui/react-collapsible": "^1.1.0", "@radix-ui/react-collapsible": "1.1.0",
"https-proxy-agent": "^7.0.5", "https-proxy-agent": "7.0.5",
"lucide-react": "^0.427.0", "lucide-react": "0.427.0",
"next": "^14.2.5", "next": "14.2.5",
"next-auth": "^4.24.7", "next-auth": "4.24.7",
"node-fetch": "^3.3.2", "node-fetch": "3.3.2",
"react-hook-form": "^7.53.0", "react-hook-form": "7.53.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "2.4.1",
"server-only": "^0.0.1", "server-only": "0.0.1",
"stripe": "^16.7.0", "stripe": "16.7.0",
"zod": "^3.23.8" "zod": "3.23.8"
} }
} }

View File

@@ -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&apos;t request this, please ignore this email.</Text>
<EmailFooter />
</Container>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -13,3 +13,5 @@ export function EmailButton({ label, href }: EmailButtonProps): React.JSX.Elemen
</Button> </Button>
); );
} }
export default EmailButton;

View File

@@ -8,3 +8,5 @@ export function EmailFooter(): React.JSX.Element {
</Text> </Text>
); );
} }
export default EmailFooter;

View File

@@ -1,10 +1,6 @@
import { Body, Column, Container, Html, Img, Link, Row, Section, Tailwind } from "@react-email/components"; import { Body, Column, Container, Html, Img, Link, Row, Section, Tailwind } from "@react-email/components";
interface EmailTemplateProps { export function EmailTemplate({ children }): React.JSX.Element {
content: JSX.Element;
}
export function EmailTemplate({ content }: EmailTemplateProps): React.JSX.Element {
return ( return (
<Html> <Html>
<Tailwind> <Tailwind>
@@ -22,7 +18,7 @@ export function EmailTemplate({ content }: EmailTemplateProps): React.JSX.Elemen
/> />
</Link> </Link>
</Section> </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> <Section>
<Row> <Row>

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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}&apos;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>
);
}

View File

@@ -18,7 +18,7 @@ import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { isLight, mixColor } from "@formbricks/lib/utils/colors"; import { isLight, mixColor } from "@formbricks/lib/utils/colors";
import { type TSurvey, TSurveyQuestionTypeEnum, type TSurveyStyling } from "@formbricks/types/surveys/types"; import { type TSurvey, TSurveyQuestionTypeEnum, type TSurveyStyling } from "@formbricks/types/surveys/types";
import { RatingSmiley } from "@formbricks/ui/components/RatingSmiley"; import { RatingSmiley } from "@formbricks/ui/components/RatingSmiley";
import { getNPSOptionColor, getRatingNumberOptionColor } from "../../lib/utils"; import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils";
interface PreviewEmailTemplateProps { interface PreviewEmailTemplateProps {
survey: TSurvey; survey: TSurvey;
@@ -26,11 +26,11 @@ interface PreviewEmailTemplateProps {
styling: TSurveyStyling; styling: TSurveyStyling;
} }
export const getPreviewEmailTemplateHtml = ( export const getPreviewEmailTemplateHtml = async (
survey: TSurvey, survey: TSurvey,
surveyUrl: string, surveyUrl: string,
styling: TSurveyStyling styling: TSurveyStyling
): string => { ): Promise<string> => {
return render(<PreviewEmailTemplate styling={styling} survey={survey} surveyUrl={surveyUrl} />, { return render(<PreviewEmailTemplate styling={styling} survey={survey} surveyUrl={surveyUrl} />, {
pretty: true, pretty: true,
}); });

View File

@@ -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&apos;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>
);
}

View File

@@ -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>
);
}

View File

@@ -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&apos;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>
);
}

View File

@@ -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&apos;t request this, please ignore this email.</Text>
<EmailFooter />
</Container>
</EmailTemplate>
);
}
export default ForgotPasswordEmail;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}&apos;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;

View File

@@ -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&apos;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;

View File

@@ -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;

View File

@@ -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&apos;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>
);
}

View File

@@ -2,7 +2,7 @@ import { Container, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { WEBAPP_URL } from "@formbricks/lib/constants"; import { WEBAPP_URL } from "@formbricks/lib/constants";
import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary"; 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"; import { NotificationFooter } from "./notification-footer";
interface CreateReminderNotificationBodyProps { interface CreateReminderNotificationBodyProps {

View File

@@ -6,7 +6,7 @@ import type {
TWeeklySummaryNotificationDataSurvey, TWeeklySummaryNotificationDataSurvey,
TWeeklySummarySurveyResponseData, TWeeklySummarySurveyResponseData,
} from "@formbricks/types/weekly-summary"; } from "@formbricks/types/weekly-summary";
import { EmailButton } from "../general/email-button"; import { EmailButton } from "../../components/email-button";
import { renderEmailResponseValue } from "../survey/response-finished-email"; import { renderEmailResponseValue } from "../survey/response-finished-email";
const getButtonLabel = (count: number): string => { const getButtonLabel = (count: number): string => {

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary"; import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary";
import { EmailTemplate } from "../../components/email-template";
import { LiveSurveyNotification } from "./live-survey-notification"; import { LiveSurveyNotification } from "./live-survey-notification";
import { NotificationFooter } from "./notification-footer"; import { NotificationFooter } from "./notification-footer";
import { NotificationHeader } from "./notification-header"; import { NotificationHeader } from "./notification-header";
@@ -21,7 +22,7 @@ export function WeeklySummaryNotificationEmail({
endYear, endYear,
}: WeeklySummaryNotificationEmailProps): React.JSX.Element { }: WeeklySummaryNotificationEmailProps): React.JSX.Element {
return ( return (
<div> <EmailTemplate>
<NotificationHeader <NotificationHeader
endDate={endDate} endDate={endDate}
endYear={endYear} endYear={endYear}
@@ -35,6 +36,6 @@ export function WeeklySummaryNotificationEmail({
surveys={notificationData.surveys} surveys={notificationData.surveys}
/> />
<NotificationFooter environmentId={notificationData.environmentId} /> <NotificationFooter environmentId={notificationData.environmentId} />
</div> </EmailTemplate>
); );
} }

View File

@@ -18,18 +18,17 @@ import type { TLinkSurveyEmailData } from "@formbricks/types/email";
import type { TResponse } from "@formbricks/types/responses"; import type { TResponse } from "@formbricks/types/responses";
import type { TSurvey } from "@formbricks/types/surveys/types"; import type { TSurvey } from "@formbricks/types/surveys/types";
import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary"; import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary";
import { ForgotPasswordEmail } from "./components/auth/forgot-password-email"; import { ForgotPasswordEmail } from "./emails/auth/forgot-password-email";
import { PasswordResetNotifyEmail } from "./components/auth/password-reset-notify-email"; import { PasswordResetNotifyEmail } from "./emails/auth/password-reset-notify-email";
import { VerificationEmail } from "./components/auth/verification-email"; import { VerificationEmail } from "./emails/auth/verification-email";
import { EmailTemplate } from "./components/general/email-template"; import { InviteAcceptedEmail } from "./emails/invite/invite-accepted-email";
import { InviteAcceptedEmail } from "./components/invite/invite-accepted-email"; import { InviteEmail } from "./emails/invite/invite-email";
import { InviteEmail } from "./components/invite/invite-email"; import { OnboardingInviteEmail } from "./emails/invite/onboarding-invite-email";
import { OnboardingInviteEmail } from "./components/invite/onboarding-invite-email"; import { EmbedSurveyPreviewEmail } from "./emails/survey/embed-survey-preview-email";
import { EmbedSurveyPreviewEmail } from "./components/survey/embed-survey-preview-email"; import { LinkSurveyEmail } from "./emails/survey/link-survey-email";
import { LinkSurveyEmail } from "./components/survey/link-survey-email"; import { ResponseFinishedEmail } from "./emails/survey/response-finished-email";
import { ResponseFinishedEmail } from "./components/survey/response-finished-email"; import { NoLiveSurveyNotificationEmail } from "./emails/weekly-summary/no-live-survey-notification-email";
import { NoLiveSurveyNotificationEmail } from "./components/weekly-summary/no-live-survey-notification-email"; import { WeeklySummaryNotificationEmail } from "./emails/weekly-summary/weekly-summary-notification-email";
import { WeeklySummaryNotificationEmail } from "./components/weekly-summary/weekly-summary-notification-email";
export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT); 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( const verificationRequestLink = `${WEBAPP_URL}/auth/verification-requested?email=${encodeURIComponent(
user.email user.email
)}`; )}`;
const html = await render(VerificationEmail({ verificationRequestLink, verifyLink }));
await sendEmail({ await sendEmail({
to: user.email, to: user.email,
subject: "Please verify your email to use Formbricks", 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", expiresIn: "1d",
}); });
const verifyLink = `${WEBAPP_URL}/auth/forgot-password/reset?token=${encodeURIComponent(token)}`; const verifyLink = `${WEBAPP_URL}/auth/forgot-password/reset?token=${encodeURIComponent(token)}`;
const html = await render(ForgotPasswordEmail({ verifyLink }));
await sendEmail({ await sendEmail({
to: user.email, to: user.email,
subject: "Reset your Formbricks password", subject: "Reset your Formbricks password",
html: render(EmailTemplate({ content: ForgotPasswordEmail({ verifyLink }) })), html,
}); });
}; };
export const sendPasswordResetNotifyEmail = async (user: TEmailUser): Promise<void> => { export const sendPasswordResetNotifyEmail = async (user: TEmailUser): Promise<void> => {
const html = await render(PasswordResetNotifyEmail());
await sendEmail({ await sendEmail({
to: user.email, to: user.email,
subject: "Your Formbricks password has been changed", 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)}`; const verifyLink = `${WEBAPP_URL}/invite?token=${encodeURIComponent(token)}`;
if (isOnboardingInvite && inviteMessage) { if (isOnboardingInvite && inviteMessage) {
const html = await render(OnboardingInviteEmail({ verifyLink, inviteMessage, inviterName }));
await sendEmail({ await sendEmail({
to: email, to: email,
subject: `${inviterName} needs a hand setting up Formbricks. Can you help out?`, subject: `${inviterName} needs a hand setting up Formbricks. Can you help out?`,
html: render( html,
EmailTemplate({ content: OnboardingInviteEmail({ verifyLink, inviteMessage, inviterName }) })
),
}); });
} else { } else {
const html = await render(InviteEmail({ inviteeName, inviterName, verifyLink }));
await sendEmail({ await sendEmail({
to: email, to: email,
subject: `You're invited to collaborate on Formbricks!`, 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, inviteeName: string,
email: string email: string
): Promise<void> => { ): Promise<void> => {
const html = await render(InviteAcceptedEmail({ inviteeName, inviterName }));
await sendEmail({ await sendEmail({
to: email, to: email,
subject: `You've got a new organization member!`, 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"); throw new Error("Organization not found");
} }
const html = await render(
ResponseFinishedEmail({
survey,
responseCount,
response,
WEBAPP_URL,
environmentId,
organization,
})
);
await sendEmail({ await sendEmail({
to: email, to: email,
subject: personEmail subject: personEmail
? `${personEmail} just completed your ${survey.name} survey ✅` ? `${personEmail} just completed your ${survey.name} survey ✅`
: `A response for ${survey.name} was completed ✅`, : `A response for ${survey.name} was completed ✅`,
replyTo: personEmail?.toString() ?? MAIL_FROM, replyTo: personEmail?.toString() ?? MAIL_FROM,
html: render( html,
EmailTemplate({
content: ResponseFinishedEmail({
survey,
responseCount,
response,
WEBAPP_URL,
environmentId,
organization,
}),
})
),
}); });
}; };
export const sendEmbedSurveyPreviewEmail = async ( export const sendEmbedSurveyPreviewEmail = async (
to: string, to: string,
subject: string, subject: string,
html: string, innerHtml: string,
environmentId: string environmentId: string
): Promise<void> => { ): Promise<void> => {
const html = await render(EmbedSurveyPreviewEmail({ html: innerHtml, environmentId }));
await sendEmail({ await sendEmail({
to, to,
subject, 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)}`; return `${WEBAPP_URL}/s/${surveyId}?verify=${encodeURIComponent(token)}`;
}; };
const surveyLink = getSurveyLink();
const html = await render(LinkSurveyEmail({ surveyName, surveyLink }));
await sendEmail({ await sendEmail({
to: data.email, to: data.email,
subject: "Your survey is ready to be filled out.", 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 startYear = notificationData.lastWeekDate.getFullYear();
const endYear = notificationData.currentDate.getFullYear(); const endYear = notificationData.currentDate.getFullYear();
const html = await render(
WeeklySummaryNotificationEmail({
notificationData,
startDate,
endDate,
startYear,
endYear,
})
);
await sendEmail({ await sendEmail({
to: email, to: email,
subject: getEmailSubject(notificationData.productName), subject: getEmailSubject(notificationData.productName),
html: render( html,
EmailTemplate({
content: WeeklySummaryNotificationEmail({
notificationData,
startDate,
endDate,
startYear,
endYear,
}),
})
),
}); });
}; };
@@ -263,19 +269,18 @@ export const sendNoLiveSurveyNotificationEmail = async (
)}`; )}`;
const startYear = notificationData.lastWeekDate.getFullYear(); const startYear = notificationData.lastWeekDate.getFullYear();
const endYear = notificationData.currentDate.getFullYear(); const endYear = notificationData.currentDate.getFullYear();
const html = await render(
NoLiveSurveyNotificationEmail({
notificationData,
startDate,
endDate,
startYear,
endYear,
})
);
await sendEmail({ await sendEmail({
to: email, to: email,
subject: getEmailSubject(notificationData.productName), subject: getEmailSubject(notificationData.productName),
html: render( html,
EmailTemplate({
content: NoLiveSurveyNotificationEmail({
notificationData,
startDate,
endDate,
startYear,
endYear,
}),
})
),
}); });
}; };

View File

@@ -4,6 +4,7 @@
"description": "Email package", "description": "Email package",
"main": "./index.tsx", "main": "./index.tsx",
"scripts": { "scripts": {
"dev": "email dev --port 3003",
"clean": "rimraf .turbo node_modules dist", "clean": "rimraf .turbo node_modules dist",
"lint": "eslint --ext .ts,.tsx --fix ." "lint": "eslint --ext .ts,.tsx --fix ."
}, },
@@ -12,14 +13,14 @@
"@formbricks/lib": "workspace:*", "@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*", "@formbricks/ui": "workspace:*",
"@react-email/components": "^0.0.22", "@react-email/components": "0.0.25",
"@react-email/render": "^0.0.17", "@react-email/render": "1.0.1",
"lucide-react": "^0.427.0", "lucide-react": "0.451.0",
"nodemailer": "^6.9.14", "nodemailer": "6.9.15"
"react-email": "^2.1.6"
}, },
"devDependencies": { "devDependencies": {
"@types/nodemailer": "^6.4.15", "@types/nodemailer": "6.4.16",
"@types/react": "18.3.3" "@types/react": "18.3.11",
"react-email": "2.1.6"
} }
} }

View File

@@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["/*"] "@/*": ["./*"]
}, },
"resolveJsonModule": true, "resolveJsonModule": true,
"strictNullChecks": true "strictNullChecks": true

View File

@@ -46,8 +46,8 @@
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"terser": "^5.31.6", "terser": "5.31.6",
"vite": "^5.4.1", "vite": "5.4.8",
"vite-plugin-dts": "^3.9.1" "vite-plugin-dts": "3.9.1"
} }
} }

View File

@@ -44,9 +44,9 @@
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"terser": "^5.31.6", "terser": "5.31.6",
"vite": "^5.4.1", "vite": "5.4.8",
"vite-plugin-dts": "^3.9.1" "vite-plugin-dts": "3.9.1"
}, },
"peerDependencies": { "peerDependencies": {
"zod": "3.x" "zod": "3.x"

View File

@@ -20,31 +20,31 @@
"@formbricks/api": "*", "@formbricks/api": "*",
"@formbricks/database": "*", "@formbricks/database": "*",
"@formbricks/types": "*", "@formbricks/types": "*",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "2.2.2",
"@t3-oss/env-nextjs": "^0.11.0", "@t3-oss/env-nextjs": "0.11.0",
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "1.2.0",
"aws-crt": "^1.21.3", "aws-crt": "1.21.3",
"date-fns": "^3.6.0", "date-fns": "3.6.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "9.0.2",
"markdown-it": "^14.1.0", "markdown-it": "14.1.0",
"mime-types": "^2.1.35", "mime-types": "2.1.35",
"nanoid": "^5.0.7", "nanoid": "5.0.7",
"next-auth": "^4.24.7", "next-auth": "4.24.7",
"posthog-node": "^4.1.0", "posthog-node": "4.1.0",
"qrcode": "^1.5.4", "qrcode": "1.5.4",
"server-only": "^0.0.1", "server-only": "0.0.1",
"superjson": "^2.2.1", "superjson": "2.2.1",
"tailwind-merge": "^2.5.2" "tailwind-merge": "2.5.2"
}, },
"devDependencies": { "devDependencies": {
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "9.0.6",
"@types/mime-types": "^2.1.4", "@types/mime-types": "2.1.4",
"@types/ungap__structured-clone": "^1.2.0", "@types/ungap__structured-clone": "1.2.0",
"dotenv": "^16.4.5", "dotenv": "16.4.5",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"ts-node": "^10.9.2", "ts-node": "10.9.2",
"vitest": "^2.0.5", "vitest": "2.0.5",
"vitest-mock-extended": "^2.0.0" "vitest-mock-extended": "2.0.0"
} }
} }

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"react-select": "^5.8.0"
}
}

View File

@@ -44,12 +44,12 @@
"@formbricks/lib": "workspace:*", "@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@types/react": "^18.2.79", "@types/react": "18.3.11",
"react": "18.2.0", "react": "18.3.1",
"react-native": "^0.74.5", "react-native": "0.74.5",
"terser": "^5.31.3", "terser": "5.31.3",
"vite": "^5.3.5", "vite": "5.4.8",
"vite-plugin-dts": "^3.9.1" "vite-plugin-dts": "3.9.1"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16.8.0", "react": ">=16.8.0",

View File

@@ -41,21 +41,21 @@
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"@formbricks/lib": "workspace:*", "@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"@preact/preset-vite": "^2.9.0", "@preact/preset-vite": "2.9.0",
"autoprefixer": "^10.4.20", "autoprefixer": "10.4.20",
"concurrently": "8.2.2", "concurrently": "8.2.2",
"isomorphic-dompurify": "^2.14.0", "isomorphic-dompurify": "2.14.0",
"postcss": "^8.4.41", "postcss": "8.4.41",
"preact": "^10.23.2", "preact": "10.23.2",
"react-date-picker": "^11.0.0", "react-date-picker": "11.0.0",
"serve": "14.2.3", "serve": "14.2.3",
"tailwindcss": "^3.4.10", "tailwindcss": "3.4.10",
"terser": "^5.31.6", "terser": "5.31.6",
"vite": "^5.4.1", "vite": "5.4.8",
"vite-plugin-dts": "^3.9.1", "vite-plugin-dts": "3.9.1",
"vite-tsconfig-paths": "^5.0.1" "vite-tsconfig-paths": "5.0.1"
}, },
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^0.8.2" "@formkit/auto-animate": "0.8.2"
} }
} }

View File

@@ -12,6 +12,6 @@
"@formbricks/config-typescript": "workspace:*" "@formbricks/config-typescript": "workspace:*"
}, },
"dependencies": { "dependencies": {
"zod": "^3.23.8" "zod": "3.23.8"
} }
} }

View File

@@ -12,50 +12,50 @@
"@formbricks/config-typescript": "workspace:*", "@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*", "@formbricks/eslint-config": "workspace:*",
"@formbricks/types": "workspace:*", "@formbricks/types": "workspace:*",
"concurrently": "^8.2.2", "concurrently": "8.2.2",
"postcss": "^8.4.41", "postcss": "8.4.41",
"react": "18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@formbricks/database": "workspace:*", "@formbricks/database": "workspace:*",
"@formbricks/lib": "workspace:*", "@formbricks/lib": "workspace:*",
"@formbricks/surveys": "workspace:*", "@formbricks/surveys": "workspace:*",
"@lexical/code": "^0.17.0", "@lexical/code": "0.17.0",
"@lexical/link": "^0.17.0", "@lexical/link": "0.17.0",
"@lexical/list": "^0.17.0", "@lexical/list": "0.17.0",
"@lexical/markdown": "^0.17.0", "@lexical/markdown": "0.17.0",
"@lexical/react": "^0.17.0", "@lexical/react": "0.17.0",
"@lexical/rich-text": "^0.17.0", "@lexical/rich-text": "0.17.0",
"@lexical/table": "^0.17.0", "@lexical/table": "0.17.0",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "1.2.0",
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "1.1.1",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "2.1.1",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "2.1.0",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "1.1.1",
"@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-radio-group": "1.2.0",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "2.1.1",
"@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-slider": "1.2.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "1.1.2",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "0.5.9",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "0.5.13",
"autoprefixer": "^10.4.20", "autoprefixer": "10.4.20",
"boring-avatars": "^1.10.2", "boring-avatars": "1.10.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "0.7.0",
"clsx": "^2.1.1", "clsx": "2.1.1",
"cmdk": "^1.0.0", "cmdk": "1.0.0",
"lexical": "^0.17.0", "lexical": "0.17.0",
"lucide-react": "^0.427.0", "lucide-react": "0.427.0",
"mime": "^4.0.4", "mime": "4.0.4",
"react-colorful": "^5.6.1", "react-colorful": "5.6.1",
"react-confetti": "^6.1.0", "react-confetti": "6.1.0",
"react-day-picker": "^9.0.8", "react-day-picker": "9.0.8",
"react-hot-toast": "^2.4.1", "react-hot-toast": "2.4.1",
"react-radio-group": "^3.0.3", "react-radio-group": "3.0.3",
"react-use": "^17.5.1", "react-use": "17.5.1",
"tailwind-merge": "^2.5.2", "tailwind-merge": "2.5.2",
"tailwindcss": "^3.4.13" "tailwindcss": "3.4.13"
} }
} }

2918
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff