mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-30 02:29:45 -05:00
Merge branch 'formbricks:main' into feat/3214-unify-menu
This commit is contained in:
@@ -7,5 +7,5 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@formbricks/formbricks-com", "@formbricks/demo", "@formbricks/web"]
|
||||
"ignore": ["@formbricks/demo", "@formbricks/web"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Build Docs
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
pnpm build --filter=@formbricks/docs...
|
||||
shell: bash
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build web
|
||||
name: Build Web
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
|
||||
@@ -50,6 +50,13 @@ jobs:
|
||||
uses: ./.github/workflows/build-web.yml
|
||||
secrets: inherit
|
||||
|
||||
docs:
|
||||
name: Build Docs
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/build-docs.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-test:
|
||||
name: Run E2E Tests
|
||||
needs: [changes]
|
||||
@@ -59,7 +66,7 @@ jobs:
|
||||
|
||||
required:
|
||||
name: PR Check Summary
|
||||
needs: [lint, test, build, e2e-test]
|
||||
needs: [lint, test, build, e2e-test, docs]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -10,9 +10,6 @@ tasks:
|
||||
gp sync-await init &&
|
||||
turbo --filter "@formbricks/demo" go
|
||||
|
||||
- name: website
|
||||
command: gp sync-await init && turbo --filter "@formbricks/formbricks-com" dev
|
||||
|
||||
- name: Init Formbricks
|
||||
init: |
|
||||
cp .env.example .env &&
|
||||
|
||||
@@ -5,4 +5,5 @@ shared-workspace-shrinkwrap = true
|
||||
access = public
|
||||
enable-pre-post-scripts = true
|
||||
legacy-peer-deps=true
|
||||
node-linker=hoisted
|
||||
node-linker=hoisted
|
||||
save-exact=true
|
||||
@@ -2,7 +2,7 @@ Copyright (c) 2024 Formbricks GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under the "packages/ee/" directory of this repository, if that directory exists, is licensed under the license defined in "packages/ee/LICENSE".
|
||||
- All content that resides under the "packages/ee/" & "apps/web/app/(ee)" directories of this repository, if these directories exist, is licensed under the license defined in "packages/ee/LICENSE".
|
||||
- All content that resides under the "packages/js/", "packages/react-native/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
|
||||
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@formbricks/react-native": "workspace:*",
|
||||
"expo": "^51.0.26",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"react": "^18.2.0",
|
||||
"react-native": "^0.74.4",
|
||||
"expo": "51.0.26",
|
||||
"expo-status-bar": "1.12.1",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.74.4",
|
||||
"react-native-webview": "13.8.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "~18.2.79",
|
||||
"typescript": "^5.3.3"
|
||||
"@babel/core": "7.25.2",
|
||||
"@types/react": "18.3.11",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"lucide-react": "^0.418.0",
|
||||
"next": "14.2.5",
|
||||
"lucide-react": "0.418.0",
|
||||
"next": "14.2.10",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
|
||||
@@ -119,7 +119,7 @@ This allows us to capture and analyze partial feedback where the user is not wil
|
||||
|
||||
<MdxImage src={DocsNavi} alt="doc navigation" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
|
||||
Locate that file. We are using the [Tailwind Template “Syntax”](https://tailwindui.com/templates/syntax) for our docs. Here is our [Layout.tsx](https://github.com/formbricks/formbricks/blob/main/apps/formbricks-com/components/docs/Layout.tsx) file.
|
||||
Locate that file. We are using the [Tailwind Template “Syntax”](https://tailwindui.com/templates/syntax) for our docs.
|
||||
|
||||
3. Write the frontend code for the widget. Here is the full component (we break it down right below):
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { Search } from "@/components/Search";
|
||||
import clsx from "clsx";
|
||||
import { motion, useScroll, useTransform } from "framer-motion";
|
||||
import { type MotionStyle, motion, useScroll, useTransform } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { forwardRef } from "react";
|
||||
import { Button } from "./Button";
|
||||
@@ -46,7 +46,7 @@ export const Header = forwardRef<React.ElementRef<"div">, { className?: string }
|
||||
{
|
||||
"--bg-opacity-light": bgOpacityLight,
|
||||
"--bg-opacity-dark": bgOpacityDark,
|
||||
} as React.CSSProperties
|
||||
} as MotionStyle
|
||||
}>
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
+47
-47
@@ -12,62 +12,62 @@
|
||||
},
|
||||
"browserslist": "defaults, not ie <= 11",
|
||||
"dependencies": {
|
||||
"@algolia/autocomplete-core": "^1.17.4",
|
||||
"@calcom/embed-react": "^1.5.0",
|
||||
"@algolia/autocomplete-core": "1.17.4",
|
||||
"@calcom/embed-react": "1.5.1",
|
||||
"@docsearch/css": "3",
|
||||
"@docsearch/react": "^3.6.1",
|
||||
"@docsearch/react": "3.6.2",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@headlessui/react": "^2.1.2",
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@mapbox/rehype-prism": "^0.9.0",
|
||||
"@mdx-js/loader": "^3.0.1",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@next/mdx": "14.2.5",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"acorn": "^8.12.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"clsx": "^2.1.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"flexsearch": "^0.7.43",
|
||||
"framer-motion": "11.3.20",
|
||||
"lottie-web": "^5.12.2",
|
||||
"lucide": "^0.418.0",
|
||||
"lucide-react": "^0.418.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mdx-annotations": "^0.1.4",
|
||||
"next": "14.2.5",
|
||||
"next-plausible": "^3.12.0",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"@headlessui/react": "2.1.9",
|
||||
"@headlessui/tailwindcss": "0.2.1",
|
||||
"@mapbox/rehype-prism": "0.9.0",
|
||||
"@mdx-js/loader": "3.0.1",
|
||||
"@mdx-js/react": "3.0.1",
|
||||
"@next/mdx": "14.2.15",
|
||||
"@paralleldrive/cuid2": "2.2.2",
|
||||
"@sindresorhus/slugify": "2.2.1",
|
||||
"@tailwindcss/typography": "0.5.15",
|
||||
"acorn": "8.12.1",
|
||||
"autoprefixer": "10.4.20",
|
||||
"clsx": "2.1.1",
|
||||
"fast-glob": "3.3.2",
|
||||
"flexsearch": "0.7.43",
|
||||
"framer-motion": "11.11.4",
|
||||
"lottie-web": "5.12.2",
|
||||
"lucide": "0.451.0",
|
||||
"lucide-react": "0.451.0",
|
||||
"mdast-util-to-string": "4.0.0",
|
||||
"mdx-annotations": "0.1.4",
|
||||
"next": "14.2.10",
|
||||
"next-plausible": "3.12.2",
|
||||
"next-seo": "6.6.0",
|
||||
"next-sitemap": "4.2.3",
|
||||
"next-themes": "0.3.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"prism-react-renderer": "2.4.0",
|
||||
"prismjs": "1.29.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-highlight-words": "^0.20.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-responsive-embed": "^2.1.0",
|
||||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-mdx": "^3.0.1",
|
||||
"schema-dts": "^1.1.2",
|
||||
"sharp": "^0.33.4",
|
||||
"shiki": "^0.14.7",
|
||||
"simple-functional-loader": "^1.2.1",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"unist-util-filter": "^5.0.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zustand": "^4.5.4"
|
||||
"react-highlight-words": "0.20.0",
|
||||
"react-markdown": "9.0.1",
|
||||
"react-responsive-embed": "2.1.0",
|
||||
"remark": "15.0.1",
|
||||
"remark-gfm": "4.0.0",
|
||||
"remark-mdx": "3.0.1",
|
||||
"schema-dts": "1.1.2",
|
||||
"sharp": "0.33.5",
|
||||
"shiki": "0.14.7",
|
||||
"simple-functional-loader": "1.2.1",
|
||||
"tailwindcss": "3.4.13",
|
||||
"unist-util-filter": "5.0.1",
|
||||
"unist-util-visit": "5.0.0",
|
||||
"zustand": "4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/react-highlight-words": "^0.20.0",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/react-highlight-words": "0.20.0",
|
||||
"@formbricks/eslint-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "../../web/app/globals.css";
|
||||
import "../../../packages/ui/globals.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
|
||||
+22
-22
@@ -12,30 +12,30 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"eslint-plugin-react-refresh": "0.4.12",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.6.1",
|
||||
"@chromatic-com/storybook": "2.0.2",
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@storybook/addon-a11y": "^8.2.9",
|
||||
"@storybook/addon-essentials": "^8.2.9",
|
||||
"@storybook/addon-interactions": "^8.2.9",
|
||||
"@storybook/addon-links": "^8.2.9",
|
||||
"@storybook/addon-onboarding": "^8.2.9",
|
||||
"@storybook/blocks": "^8.2.9",
|
||||
"@storybook/react": "^8.2.9",
|
||||
"@storybook/react-vite": "^8.2.9",
|
||||
"@storybook/test": "^8.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"esbuild": "^0.23.0",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^8.2.9",
|
||||
"tsup": "^8.2.4",
|
||||
"vite": "^5.4.1"
|
||||
"@storybook/addon-a11y": "8.3.5",
|
||||
"@storybook/addon-essentials": "8.3.5",
|
||||
"@storybook/addon-interactions": "8.3.5",
|
||||
"@storybook/addon-links": "8.3.5",
|
||||
"@storybook/addon-onboarding": "8.3.5",
|
||||
"@storybook/blocks": "8.3.5",
|
||||
"@storybook/react": "8.3.5",
|
||||
"@storybook/react-vite": "8.3.5",
|
||||
"@storybook/test": "8.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.8.1",
|
||||
"@typescript-eslint/parser": "8.8.1",
|
||||
"@vitejs/plugin-react": "4.3.2",
|
||||
"esbuild": "0.24.0",
|
||||
"eslint-plugin-storybook": "0.9.0",
|
||||
"prop-types": "15.8.1",
|
||||
"storybook": "8.3.5",
|
||||
"tsup": "8.3.0",
|
||||
"vite": "5.4.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AiOutlineDiscord } from "react-icons/ai";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
@@ -230,7 +231,7 @@ export const MainNavigation = ({
|
||||
label: "Join Discord",
|
||||
href: "https://formbricks.com/discord",
|
||||
target: "_blank",
|
||||
icon: ArrowUpRightIcon,
|
||||
icon: AiOutlineDiscord,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ const Loading = () => {
|
||||
skeletonLines: [{ classes: "h-6 w-4/6 rounded-full" }],
|
||||
},
|
||||
{
|
||||
title: "How to setup",
|
||||
title: "How To Setup",
|
||||
description: "Follow these steps to setup the Formbricks widget within your app",
|
||||
skeletonLines: [
|
||||
{ classes: "h-6 w-24 rounded-full" },
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ const Page = async ({ params }) => {
|
||||
<EnvironmentIdField environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title="How to setup"
|
||||
title="How To Setup"
|
||||
description="Follow these steps to setup the Formbricks widget within your app"
|
||||
noPadding>
|
||||
<SetupInstructions environmentId={params.environmentId} webAppUrl={WEBAPP_URL} />
|
||||
|
||||
@@ -37,7 +37,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
|
||||
/>
|
||||
</PageHeader>
|
||||
<SettingsCard
|
||||
title="Multi-language surveys"
|
||||
title="Multi-Language Surveys"
|
||||
description="Add languages to create multi-language surveys.">
|
||||
<EditLanguage product={product} />
|
||||
</SettingsCard>
|
||||
|
||||
+2
-2
@@ -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 { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
@@ -16,7 +16,7 @@ export const getEmailTemplateHtml = async (surveyId: string) => {
|
||||
|
||||
const styling = getStyling(product, survey);
|
||||
const surveyUrl = WEBAPP_URL + "/s/" + survey.id;
|
||||
const html = getPreviewEmailTemplateHtml(survey, surveyUrl, styling);
|
||||
const html = await getPreviewEmailTemplateHtml(survey, surveyUrl, styling);
|
||||
const doctype =
|
||||
'<!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, "");
|
||||
|
||||
+2
-6
@@ -1,5 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getSurvey, getSurveys } from "@/app/(app)/environments/[environmentId]/surveys/lib/surveys";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
@@ -8,12 +9,7 @@ import {
|
||||
getOrganizationIdFromSurveyId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import {
|
||||
copySurveyToOtherEnvironment,
|
||||
deleteSurvey,
|
||||
getSurvey,
|
||||
getSurveys,
|
||||
} from "@formbricks/lib/survey/service";
|
||||
import { copySurveyToOtherEnvironment, deleteSurvey } from "@formbricks/lib/survey/service";
|
||||
import { generateSurveySingleUseId } from "@formbricks/lib/utils/singleUseSurveys";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZSurveyFilterCriteria } from "@formbricks/types/surveys/types";
|
||||
+13
-7
@@ -1,14 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { copySurveyToOtherEnvironmentAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import {
|
||||
TSurveyCopyFormData,
|
||||
ZSurveyCopyFormValidation,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyCopyFormData, ZSurveyCopyFormValidation } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "../../Button";
|
||||
import { Checkbox } from "../../Checkbox";
|
||||
import { FormControl, FormField, FormItem, FormProvider } from "../../Form";
|
||||
import { Label } from "../../Label";
|
||||
import { TooltipRenderer } from "../../Tooltip";
|
||||
import { copySurveyToOtherEnvironmentAction } from "../actions";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Checkbox } from "@formbricks/ui/components/Checkbox";
|
||||
import { FormControl, FormField, FormItem, FormProvider } from "@formbricks/ui/components/Form";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
import { TooltipRenderer } from "@formbricks/ui/components/Tooltip";
|
||||
|
||||
export const CopySurveyForm = ({
|
||||
defaultProducts,
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import { MousePointerClickIcon } from "lucide-react";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Modal } from "../../Modal";
|
||||
import { Modal } from "@formbricks/ui/components/Modal";
|
||||
import SurveyCopyOptions from "./SurveyCopyOptions";
|
||||
|
||||
interface CopySurveyModalProps {
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types";
|
||||
import { DropdownMenuItem } from "../../DropdownMenu";
|
||||
import { DropdownMenuItem } from "@formbricks/ui/components/DropdownMenu";
|
||||
|
||||
interface SortOptionProps {
|
||||
option: TSortOption;
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { generateSingleUseIdAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { SurveyTypeIndicator } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyTypeIndicator";
|
||||
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { convertDateString, timeSince } from "@formbricks/lib/time";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { SurveyStatusIndicator } from "@formbricks/ui/components/SurveyStatusIndicator";
|
||||
import { SurveyDropDownMenu } from "./SurveyDropdownMenu";
|
||||
|
||||
interface SurveyCardProps {
|
||||
survey: TSurvey;
|
||||
environment: TEnvironment;
|
||||
otherEnvironment: TEnvironment;
|
||||
isViewer: boolean;
|
||||
WEBAPP_URL: string;
|
||||
duplicateSurvey: (survey: TSurvey) => void;
|
||||
deleteSurvey: (surveyId: string) => void;
|
||||
}
|
||||
export const SurveyCard = ({
|
||||
survey,
|
||||
environment,
|
||||
otherEnvironment,
|
||||
isViewer,
|
||||
WEBAPP_URL,
|
||||
deleteSurvey,
|
||||
duplicateSurvey,
|
||||
}: SurveyCardProps) => {
|
||||
const isSurveyCreationDeletionDisabled = isViewer;
|
||||
|
||||
const surveyStatusLabel = useMemo(() => {
|
||||
if (survey.status === "inProgress") return "In Progress";
|
||||
else if (survey.status === "scheduled") return "Scheduled";
|
||||
else if (survey.status === "completed") return "Completed";
|
||||
else if (survey.status === "draft") return "Draft";
|
||||
else if (survey.status === "paused") return "Paused";
|
||||
}, [survey]);
|
||||
|
||||
const [singleUseId, setSingleUseId] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSingleUseId = async () => {
|
||||
if (survey.singleUse?.enabled) {
|
||||
const generateSingleUseIdResponse = await generateSingleUseIdAction({
|
||||
surveyId: survey.id,
|
||||
isEncrypted: !!survey.singleUse?.isEncrypted,
|
||||
});
|
||||
if (generateSingleUseIdResponse?.data) {
|
||||
setSingleUseId(generateSingleUseIdResponse.data);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(generateSingleUseIdResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} else {
|
||||
setSingleUseId(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSingleUseId();
|
||||
}, [survey]);
|
||||
|
||||
const linkHref = useMemo(() => {
|
||||
return survey.status === "draft"
|
||||
? `/environments/${environment.id}/surveys/${survey.id}/edit`
|
||||
: `/environments/${environment.id}/surveys/${survey.id}/summary`;
|
||||
}, [survey.status, survey.id, environment.id]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={linkHref}
|
||||
key={survey.id}
|
||||
className="relative grid w-full grid-cols-8 place-items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition-all ease-in-out hover:scale-[101%]">
|
||||
<div className="col-span-1 flex max-w-full items-center justify-self-start text-sm font-medium text-slate-900">
|
||||
<div className="w-full truncate">{survey.name}</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"col-span-1 flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
|
||||
surveyStatusLabel === "Scheduled" && "bg-slate-200",
|
||||
surveyStatusLabel === "In Progress" && "bg-emerald-50",
|
||||
surveyStatusLabel === "Completed" && "bg-slate-200",
|
||||
surveyStatusLabel === "Draft" && "bg-slate-100",
|
||||
surveyStatusLabel === "Paused" && "bg-slate-100"
|
||||
)}>
|
||||
<SurveyStatusIndicator status={survey.status} /> {surveyStatusLabel}{" "}
|
||||
</div>
|
||||
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{survey.responseCount}
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-between">
|
||||
<SurveyTypeIndicator type={survey.type} />
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{convertDateString(survey.createdAt.toString())}
|
||||
</div>
|
||||
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{timeSince(survey.updatedAt.toString())}
|
||||
</div>
|
||||
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{survey.creator ? survey.creator.name : "-"}
|
||||
</div>
|
||||
<div className="col-span-1 place-self-end">
|
||||
<SurveyDropDownMenu
|
||||
survey={survey}
|
||||
key={`surveys-${survey.id}`}
|
||||
environmentId={environment.id}
|
||||
environment={environment}
|
||||
otherEnvironment={otherEnvironment!}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
singleUseId={singleUseId}
|
||||
isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled}
|
||||
duplicateSurvey={duplicateSurvey}
|
||||
deleteSurvey={deleteSurvey}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
+4
-2
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { getProductsByEnvironmentIdAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getProductsByEnvironmentIdAction } from "../actions";
|
||||
import { CopySurveyForm } from "./CopySurveyForm";
|
||||
|
||||
interface SurveyCopyOptionsProps {
|
||||
+12
-8
@@ -1,5 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
copySurveyToOtherEnvironmentAction,
|
||||
deleteSurveyAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { getSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import { ArrowUpFromLineIcon, CopyIcon, EyeIcon, LinkIcon, SquarePenIcon, TrashIcon } from "lucide-react";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -8,16 +14,14 @@ import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { DeleteDialog } from "../../DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui/components/DeleteDialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../../DropdownMenu";
|
||||
import { copySurveyToOtherEnvironmentAction, deleteSurveyAction, getSurveyAction } from "../actions";
|
||||
} from "@formbricks/ui/components/DropdownMenu";
|
||||
import { CopySurveyModal } from "./CopySurveyModal";
|
||||
|
||||
interface SurveyDropDownMenuProps {
|
||||
@@ -49,11 +53,11 @@ export const SurveyDropDownMenu = ({
|
||||
|
||||
const surveyUrl = useMemo(() => webAppUrl + "/s/" + survey.id, [survey.id, webAppUrl]);
|
||||
|
||||
const handleDeleteSurvey = async (survey: TSurvey) => {
|
||||
const handleDeleteSurvey = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await deleteSurveyAction({ surveyId: survey.id });
|
||||
deleteSurvey(survey.id);
|
||||
await deleteSurveyAction({ surveyId });
|
||||
deleteSurvey(surveyId);
|
||||
router.refresh();
|
||||
setDeleteDialogOpen(false);
|
||||
toast.success("Survey deleted successfully.");
|
||||
@@ -206,7 +210,7 @@ export const SurveyDropDownMenu = ({
|
||||
deleteWhat="Survey"
|
||||
open={isDeleteDialogOpen}
|
||||
setOpen={setDeleteDialogOpen}
|
||||
onDelete={() => handleDeleteSurvey(survey)}
|
||||
onDelete={() => handleDeleteSurvey(survey.id)}
|
||||
text="Are you sure you want to delete this survey and all of its responses? This action cannot be undone."
|
||||
/>
|
||||
)}
|
||||
+9
-2
@@ -1,7 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { TFilterOption } from "@formbricks/types/surveys/types";
|
||||
import { Checkbox } from "../../Checkbox";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../DropdownMenu";
|
||||
import { Checkbox } from "@formbricks/ui/components/Checkbox";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@formbricks/ui/components/DropdownMenu";
|
||||
|
||||
interface SurveyFilterDropdownProps {
|
||||
title: string;
|
||||
+13
-57
@@ -1,20 +1,22 @@
|
||||
import { ChevronDownIcon, Equal, Grid2X2, X } from "lucide-react";
|
||||
"use client";
|
||||
|
||||
import { SortOption } from "@/app/(app)/environments/[environmentId]/surveys/components/SortOption";
|
||||
import { initialFilters } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyList";
|
||||
import { ChevronDownIcon, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useDebounce } from "react-use";
|
||||
import { FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { TFilterOption, TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types";
|
||||
import { initialFilters } from "..";
|
||||
import { Button } from "../../Button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "../../DropdownMenu";
|
||||
import { SearchBar } from "../../SearchBar";
|
||||
import { TooltipRenderer } from "../../Tooltip";
|
||||
import { SortOption } from "./SortOption";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@formbricks/ui/components/DropdownMenu";
|
||||
import { SearchBar } from "@formbricks/ui/components/SearchBar";
|
||||
import { SurveyFilterDropdown } from "./SurveyFilterDropdown";
|
||||
|
||||
interface SurveyFilterProps {
|
||||
orientation: string;
|
||||
setOrientation: (orientation: string) => void;
|
||||
surveyFilters: TSurveyFilters;
|
||||
setSurveyFilters: React.Dispatch<React.SetStateAction<TSurveyFilters>>;
|
||||
currentProductChannel: TProductConfigChannel;
|
||||
@@ -52,13 +54,7 @@ const sortOptions: TSortOption[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const getToolTipContent = (orientation: string) => {
|
||||
return <div>{orientation} View</div>;
|
||||
};
|
||||
|
||||
export const SurveyFilters = ({
|
||||
orientation,
|
||||
setOrientation,
|
||||
surveyFilters,
|
||||
setSurveyFilters,
|
||||
currentProductChannel,
|
||||
@@ -73,16 +69,7 @@ export const SurveyFilters = ({
|
||||
const typeOptions: TFilterOption[] = [
|
||||
{ label: "Link", value: "link" },
|
||||
{ label: "App", value: "app" },
|
||||
{ label: "Website", value: "website" },
|
||||
].filter((option) => {
|
||||
if (currentProductChannel === "website") {
|
||||
return option.value !== "app";
|
||||
} else if (currentProductChannel === "app") {
|
||||
return option.value !== "website";
|
||||
} else {
|
||||
return option;
|
||||
}
|
||||
});
|
||||
];
|
||||
|
||||
const toggleDropdown = (id: string) => {
|
||||
setDropdownOpenStates(new Map(dropdownOpenStates).set(id, !dropdownOpenStates.get(id)));
|
||||
@@ -128,11 +115,6 @@ export const SurveyFilters = ({
|
||||
setSurveyFilters((prev) => ({ ...prev, sortBy: option.value }));
|
||||
};
|
||||
|
||||
const handleOrientationChange = (value: string) => {
|
||||
setOrientation(value);
|
||||
localStorage.setItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<div className="flex space-x-2">
|
||||
@@ -193,32 +175,6 @@ export const SurveyFilters = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<TooltipRenderer
|
||||
shouldRender={true}
|
||||
tooltipContent={getToolTipContent("List")}
|
||||
className="bg-slate-900 text-white">
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg border p-1 ${
|
||||
orientation === "list" ? "bg-slate-900 text-white" : "bg-white"
|
||||
}`}
|
||||
onClick={() => handleOrientationChange("list")}>
|
||||
<Equal className="h-5 w-5" />
|
||||
</div>
|
||||
</TooltipRenderer>
|
||||
|
||||
<TooltipRenderer
|
||||
shouldRender={true}
|
||||
tooltipContent={getToolTipContent("Grid")}
|
||||
className="bg-slate-900 text-white">
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg border p-1 ${
|
||||
orientation === "grid" ? "bg-slate-900 text-white" : "bg-white"
|
||||
}`}
|
||||
onClick={() => handleOrientationChange("grid")}>
|
||||
<Grid2X2 className="h-5 w-5" />
|
||||
</div>
|
||||
</TooltipRenderer>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
+33
-67
@@ -1,20 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { getSurveysAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { getFormattedFilters } from "@/app/(app)/environments/[environmentId]/surveys/lib/utils";
|
||||
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
FORMBRICKS_SURVEYS_FILTERS_KEY_LS,
|
||||
FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS,
|
||||
} from "@formbricks/lib/localStorage";
|
||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { wrapThrows } from "@formbricks/types/error-handlers";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyFilters } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "../Button";
|
||||
import { getSurveysAction } from "./actions";
|
||||
import { SurveyCard } from "./components/SurveyCard";
|
||||
import { SurveyFilters } from "./components/SurveyFilters";
|
||||
import { SurveyLoading } from "./components/SurveyLoading";
|
||||
import { getFormattedFilters } from "./utils";
|
||||
import { TSurveyFilters } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { SurveyCard } from "./SurveyCard";
|
||||
import { SurveyFilters } from "./SurveyFilters";
|
||||
import { SurveyLoading } from "./SurveyLoading";
|
||||
|
||||
interface SurveysListProps {
|
||||
environment: TEnvironment;
|
||||
@@ -52,18 +50,8 @@ export const SurveysList = ({
|
||||
|
||||
const filters = useMemo(() => getFormattedFilters(surveyFilters, userId), [surveyFilters, userId]);
|
||||
|
||||
const [orientation, setOrientation] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const orientationFromLocalStorage = localStorage.getItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS);
|
||||
if (orientationFromLocalStorage) {
|
||||
setOrientation(orientationFromLocalStorage);
|
||||
} else {
|
||||
setOrientation("grid");
|
||||
localStorage.setItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS, "grid");
|
||||
}
|
||||
|
||||
const savedFilters = localStorage.getItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
if (savedFilters) {
|
||||
const surveyParseResult = wrapThrows(() => JSON.parse(savedFilters))();
|
||||
@@ -142,59 +130,37 @@ export const SurveysList = ({
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SurveyFilters
|
||||
orientation={orientation}
|
||||
setOrientation={setOrientation}
|
||||
surveyFilters={surveyFilters}
|
||||
setSurveyFilters={setSurveyFilters}
|
||||
currentProductChannel={currentProductChannel}
|
||||
/>
|
||||
{surveys.length > 0 ? (
|
||||
<div>
|
||||
{orientation === "list" && (
|
||||
<div className="flex-col space-y-3">
|
||||
<div className="mt-6 grid w-full grid-cols-8 place-items-center gap-3 px-6 text-sm text-slate-800">
|
||||
<div className="col-span-4 place-self-start">Name</div>
|
||||
<div className="col-span-4 grid w-full grid-cols-5 place-items-center">
|
||||
<div className="col-span-2">Created at</div>
|
||||
<div className="col-span-2">Updated at</div>
|
||||
</div>
|
||||
</div>
|
||||
{surveys.map((survey) => {
|
||||
return (
|
||||
<SurveyCard
|
||||
key={survey.id}
|
||||
survey={survey}
|
||||
environment={environment}
|
||||
otherEnvironment={otherEnvironment}
|
||||
isViewer={isViewer}
|
||||
WEBAPP_URL={WEBAPP_URL}
|
||||
orientation={orientation}
|
||||
duplicateSurvey={handleDuplicateSurvey}
|
||||
deleteSurvey={handleDeleteSurvey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="flex-col space-y-3">
|
||||
<div className="mt-6 grid w-full grid-cols-8 place-items-center gap-3 px-6 text-sm text-slate-800">
|
||||
<div className="col-span-1 place-self-start">Name</div>
|
||||
<div className="col-span-1">Status</div>
|
||||
<div className="col-span-1">Responses</div>
|
||||
<div className="col-span-1">Type</div>
|
||||
<div className="col-span-1">Created at</div>
|
||||
<div className="col-span-1">Updated at</div>
|
||||
<div className="col-span-1">Created by</div>
|
||||
</div>
|
||||
)}
|
||||
{orientation === "grid" && (
|
||||
<div className="grid grid-cols-2 place-content-stretch gap-4 lg:grid-cols-3 2xl:grid-cols-5">
|
||||
{surveys.map((survey) => {
|
||||
return (
|
||||
<SurveyCard
|
||||
key={survey.id}
|
||||
survey={survey}
|
||||
environment={environment}
|
||||
otherEnvironment={otherEnvironment}
|
||||
isViewer={isViewer}
|
||||
WEBAPP_URL={WEBAPP_URL}
|
||||
orientation={orientation}
|
||||
duplicateSurvey={handleDuplicateSurvey}
|
||||
deleteSurvey={handleDeleteSurvey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{surveys.map((survey) => {
|
||||
return (
|
||||
<SurveyCard
|
||||
key={survey.id}
|
||||
survey={survey}
|
||||
environment={environment}
|
||||
otherEnvironment={otherEnvironment}
|
||||
isViewer={isViewer}
|
||||
WEBAPP_URL={WEBAPP_URL}
|
||||
duplicateSurvey={handleDuplicateSurvey}
|
||||
deleteSurvey={handleDeleteSurvey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{hasMore && (
|
||||
<div className="flex justify-center py-5">
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Code, HelpCircle, Link2Icon } from "lucide-react";
|
||||
|
||||
interface SurveyTypeIndicatorProps {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const surveyTypeMapping = {
|
||||
app: { icon: Code, label: "App" },
|
||||
link: { icon: Link2Icon, label: "Link" },
|
||||
};
|
||||
|
||||
export const SurveyTypeIndicator = ({ type }: SurveyTypeIndicatorProps) => {
|
||||
const { icon: Icon, label } = surveyTypeMapping[type] || { icon: HelpCircle, label: "Unknown" };
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 text-sm text-slate-600">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,208 @@
|
||||
import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { responseCache } from "@formbricks/lib/response/cache";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { getInProgressSurveyCount } from "@formbricks/lib/survey/service";
|
||||
import { buildOrderByClause, buildWhereClause } from "@formbricks/lib/survey/utils";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZOptionalNumber } from "@formbricks/types/common";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { TSurveyFilterCriteria } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey } from "../types/surveys";
|
||||
|
||||
export const surveySelect: Prisma.SurveySelect = {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
type: true,
|
||||
creator: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
status: true,
|
||||
singleUse: true,
|
||||
environmentId: true,
|
||||
_count: {
|
||||
select: { responses: true },
|
||||
},
|
||||
};
|
||||
|
||||
export const getSurveys = reactCache(
|
||||
(
|
||||
environmentId: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
filterCriteria?: TSurveyFilterCriteria
|
||||
): Promise<TSurvey[]> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
|
||||
|
||||
try {
|
||||
if (filterCriteria?.sortBy === "relevance") {
|
||||
// Call the sortByRelevance function
|
||||
return await getSurveysSortedByRelevance(environmentId, limit, offset ?? 0, filterCriteria);
|
||||
}
|
||||
|
||||
// Fetch surveys normally with pagination and include response count
|
||||
const surveysPrisma = await prisma.survey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
...buildWhereClause(filterCriteria),
|
||||
},
|
||||
select: surveySelect,
|
||||
orderBy: buildOrderByClause(filterCriteria?.sortBy),
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
|
||||
return surveysPrisma.map((survey) => {
|
||||
return {
|
||||
...survey,
|
||||
responseCount: survey._count.responses,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`surveyList-getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
|
||||
{
|
||||
tags: [
|
||||
surveyCache.tag.byEnvironmentId(environmentId),
|
||||
responseCache.tag.byEnvironmentId(environmentId),
|
||||
],
|
||||
}
|
||||
)()
|
||||
);
|
||||
|
||||
export const getSurveysSortedByRelevance = reactCache(
|
||||
(
|
||||
environmentId: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
filterCriteria?: TSurveyFilterCriteria
|
||||
): Promise<TSurvey[]> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
|
||||
|
||||
try {
|
||||
let surveys: TSurvey[] = [];
|
||||
const inProgressSurveyCount = await getInProgressSurveyCount(environmentId, filterCriteria);
|
||||
|
||||
// Fetch surveys that are in progress first
|
||||
const inProgressSurveys =
|
||||
offset && offset > inProgressSurveyCount
|
||||
? []
|
||||
: await prisma.survey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
status: "inProgress",
|
||||
...buildWhereClause(filterCriteria),
|
||||
},
|
||||
select: surveySelect,
|
||||
orderBy: buildOrderByClause("updatedAt"),
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
|
||||
surveys = inProgressSurveys.map((survey) => {
|
||||
return {
|
||||
...survey,
|
||||
responseCount: survey._count.responses,
|
||||
};
|
||||
});
|
||||
|
||||
// Determine if additional surveys are needed
|
||||
if (offset !== undefined && limit && inProgressSurveys.length < limit) {
|
||||
const remainingLimit = limit - inProgressSurveys.length;
|
||||
const newOffset = Math.max(0, offset - inProgressSurveyCount);
|
||||
const additionalSurveys = await prisma.survey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
status: { not: "inProgress" },
|
||||
...buildWhereClause(filterCriteria),
|
||||
},
|
||||
select: surveySelect,
|
||||
orderBy: buildOrderByClause("updatedAt"),
|
||||
take: remainingLimit,
|
||||
skip: newOffset,
|
||||
});
|
||||
|
||||
surveys = [
|
||||
...surveys,
|
||||
...additionalSurveys.map((survey) => {
|
||||
return {
|
||||
...survey,
|
||||
responseCount: survey._count.responses,
|
||||
};
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
return surveys;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[
|
||||
`surveyList-getSurveysSortedByRelevance-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`,
|
||||
],
|
||||
{
|
||||
tags: [
|
||||
surveyCache.tag.byEnvironmentId(environmentId),
|
||||
responseCache.tag.byEnvironmentId(environmentId),
|
||||
],
|
||||
}
|
||||
)()
|
||||
);
|
||||
|
||||
export const getSurvey = reactCache(
|
||||
(surveyId: string): Promise<TSurvey | null> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([surveyId, ZId]);
|
||||
|
||||
let surveyPrisma;
|
||||
try {
|
||||
surveyPrisma = await prisma.survey.findUnique({
|
||||
where: {
|
||||
id: surveyId,
|
||||
},
|
||||
select: surveySelect,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!surveyPrisma) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { ...surveyPrisma, responseCount: surveyPrisma?._count.responses };
|
||||
},
|
||||
[`surveyList-getSurvey-${surveyId}`],
|
||||
{
|
||||
tags: [surveyCache.tag.byId(surveyId), responseCache.tag.bySurveyId(surveyId)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SurveyLoading } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyLoading";
|
||||
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/components/PageHeader";
|
||||
import { SurveyLoading } from "@formbricks/ui/components/SurveysList/components/SurveyLoading";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SurveysList } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyList";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
@@ -14,7 +15,6 @@ import { TTemplateRole } from "@formbricks/types/templates";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/components/PageHeader";
|
||||
import { SurveysList } from "@formbricks/ui/components/SurveysList";
|
||||
import { TemplateList } from "@formbricks/ui/components/TemplateList";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
import { ZSurveyStatus } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const ZSurvey = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
environmentId: z.string(),
|
||||
type: z.enum(["link", "app", "website", "web"]), //we can replace this with ZSurveyType after we remove "web" from schema
|
||||
status: ZSurveyStatus,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
responseCount: z.number(),
|
||||
creator: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
singleUse: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
isEncrypted: z.boolean(),
|
||||
})
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export type TSurvey = z.infer<typeof ZSurvey>;
|
||||
|
||||
export const ZSurveyCopyFormValidation = z.object({
|
||||
products: z.array(
|
||||
z.object({
|
||||
product: z.string(),
|
||||
environments: z.array(z.string()),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type TSurveyCopyFormData = z.infer<typeof ZSurveyCopyFormValidation>;
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import Stripe from "stripe";
|
||||
import { STRIPE_API_VERSION } from "@formbricks/lib/constants";
|
||||
import { env } from "@formbricks/lib/env";
|
||||
import { handleCheckoutSessionCompleted } from "../handlers/checkout-session-completed";
|
||||
import { handleInvoiceFinalized } from "../handlers/invoice-finalized";
|
||||
import { handleSubscriptionCreatedOrUpdated } from "../handlers/subscription-created-or-updated";
|
||||
import { handleSubscriptionDeleted } from "../handlers/subscription-deleted";
|
||||
import { handleCheckoutSessionCompleted } from "./checkoutSessionCompleted";
|
||||
import { handleInvoiceFinalized } from "./invoiceFinalized";
|
||||
import { handleSubscriptionCreatedOrUpdated } from "./subscriptionCreatedOrUpdated";
|
||||
import { handleSubscriptionDeleted } from "./subscriptionDeleted";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { headers } from "next/headers";
|
||||
import { webhookHandler } from "@formbricks/ee/billing/api/stripe-webhook";
|
||||
import { webhookHandler } from "./lib/stripeWebhook";
|
||||
|
||||
export const POST = async (request: Request) => {
|
||||
const body = await request.text();
|
||||
@@ -0,0 +1,67 @@
|
||||
import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
|
||||
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
||||
import EnvironmentStorageHandler from "@/app/(app)/environments/[environmentId]/components/EnvironmentStorageHandler";
|
||||
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
|
||||
|
||||
const EnvLayout = async ({ children, params }) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
||||
if (!hasAccess) {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
|
||||
if (!membership) return notFound();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponseFilterProvider>
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
user={user}
|
||||
environmentId={params.environmentId}
|
||||
organizationId={organization.id}
|
||||
organizationName={organization.name}
|
||||
organizationBilling={organization.billing}
|
||||
/>
|
||||
<FormbricksClient session={session} userEmail={user.email} />
|
||||
<ToasterClient />
|
||||
<EnvironmentStorageHandler environmentId={params.environmentId} />
|
||||
<EnvironmentLayout environmentId={params.environmentId} session={session}>
|
||||
{children}
|
||||
</EnvironmentLayout>
|
||||
</ResponseFilterProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvLayout;
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { createCustomerPortalSession } from "@/app/(ee)/api/billing/stripe-webhook/lib/createCustomerPortalSession";
|
||||
import { createSubscription } from "@/app/(ee)/api/billing/stripe-webhook/lib/createSubscription";
|
||||
import { isSubscriptionCancelled } from "@/app/(ee)/api/billing/stripe-webhook/lib/isSubscriptionCancelled";
|
||||
import { z } from "zod";
|
||||
import { createCustomerPortalSession } from "@formbricks/ee/billing/lib/create-customer-portal-session";
|
||||
import { createSubscription } from "@formbricks/ee/billing/lib/create-subscription";
|
||||
import { isSubscriptionCancelled } from "@formbricks/ee/billing/lib/is-subscription-cancelled";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants";
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { CLOUD_PRICING_DATA } from "@/app/(ee)/api/billing/stripe-webhook/lib/constants";
|
||||
import {
|
||||
isSubscriptionCancelledAction,
|
||||
manageSubscriptionAction,
|
||||
upgradePlanAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/settings/(organization)/billing/actions";
|
||||
} from "@/app/(ee)/environments/[environmentId]/settings/(organization)/billing/actions";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { CLOUD_PRICING_DATA } from "@formbricks/ee/billing/lib/constants";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TOrganization, TOrganizationBillingPeriod } from "@formbricks/types/organizations";
|
||||
@@ -16,7 +16,7 @@ export const replaceAttributeRecall = (survey: TSurvey, attributes: TAttributes)
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((language) => language !== null);
|
||||
.filter((language): language is string => language !== null);
|
||||
|
||||
surveyTemp.questions.forEach((question) => {
|
||||
languages.forEach((language) => {
|
||||
|
||||
@@ -19,13 +19,13 @@ export const LegalFooter = ({
|
||||
<div className="absolute bottom-0 z-[1500] h-10 w-full">
|
||||
<div className="mx-auto flex h-full max-w-lg items-center justify-center p-2 text-center text-xs text-slate-400 text-opacity-50">
|
||||
{IMPRINT_URL && (
|
||||
<Link href={IMPRINT_URL} target="_blank" className="hover:underline">
|
||||
<Link href={IMPRINT_URL} target="_blank" className="hover:underline" tabIndex={-1}>
|
||||
Imprint
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && <span className="px-2">|</span>}
|
||||
{PRIVACY_URL && (
|
||||
<Link href={PRIVACY_URL} target="_blank" className="hover:underline">
|
||||
<Link href={PRIVACY_URL} target="_blank" className="hover:underline" tabIndex={-1}>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
)}
|
||||
@@ -34,7 +34,8 @@ export const LegalFooter = ({
|
||||
<Link
|
||||
href={`https://app.formbricks.com/s/clxbivtla014iye2vfrn436xd?surveyUrl=${surveyUrl}`}
|
||||
target="_blank"
|
||||
className="hover:underline">
|
||||
className="hover:underline"
|
||||
tabIndex={-1}>
|
||||
Report Survey
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -20,8 +20,8 @@ import { RATE_LIMITING_DISABLED, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { isValidCallbackUrl } from "@formbricks/lib/utils/url";
|
||||
|
||||
export const middleware = async (request: NextRequest) => {
|
||||
// issue with next auth types & Next 15; let's review when new fixes are available
|
||||
const token = await getToken({ req: request });
|
||||
// issue with next auth types; let's review when new fixes are available
|
||||
const token = await getToken({ req: request as any });
|
||||
|
||||
if (isAuthProtectedRoute(request.nextUrl.pathname) && !token) {
|
||||
const loginUrl = `${WEBAPP_URL}/auth/login?callbackUrl=${encodeURIComponent(WEBAPP_URL + request.nextUrl.pathname + request.nextUrl.search)}`;
|
||||
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
+46
-45
@@ -12,10 +12,10 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@dnd-kit/core": "6.1.0",
|
||||
"@dnd-kit/modifiers": "7.0.0",
|
||||
"@dnd-kit/sortable": "8.0.0",
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@formbricks/api": "workspace:*",
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/ee": "workspace:*",
|
||||
@@ -26,52 +26,53 @@
|
||||
"@formbricks/surveys": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@json2csv/node": "^7.0.6",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@react-email/components": "^0.0.22",
|
||||
"@sentry/nextjs": "^8.26.0",
|
||||
"@tanstack/react-table": "^8.20.1",
|
||||
"@vercel/og": "^0.6.2",
|
||||
"@vercel/speed-insights": "^1.0.12",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"encoding": "^0.1.13",
|
||||
"file-loader": "^6.2.0",
|
||||
"framer-motion": "11.3.28",
|
||||
"googleapis": "^140.0.1",
|
||||
"jiti": "^1.21.6",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.0.0",
|
||||
"lucide-react": "^0.427.0",
|
||||
"mime": "^4.0.4",
|
||||
"next": "14.2.5",
|
||||
"next-safe-action": "^7.6.2",
|
||||
"optional": "^0.1.4",
|
||||
"otplib": "^12.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"posthog-js": "^1.155.4",
|
||||
"prismjs": "^1.29.0",
|
||||
"@hookform/resolvers": "3.9.0",
|
||||
"@json2csv/node": "7.0.6",
|
||||
"@paralleldrive/cuid2": "2.2.2",
|
||||
"@radix-ui/react-collapsible": "1.1.1",
|
||||
"@react-email/components": "0.0.25",
|
||||
"@sentry/nextjs": "8.33.1",
|
||||
"@tanstack/react-table": "8.20.5",
|
||||
"@vercel/og": "0.6.3",
|
||||
"@vercel/speed-insights": "1.0.12",
|
||||
"bcryptjs": "2.4.3",
|
||||
"dotenv": "16.4.5",
|
||||
"encoding": "0.1.13",
|
||||
"file-loader": "6.2.0",
|
||||
"framer-motion": "11.11.4",
|
||||
"googleapis": "144.0.0",
|
||||
"jiti": "2.3.3",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "11.0.1",
|
||||
"lucide-react": "0.451.0",
|
||||
"mime": "4.0.4",
|
||||
"next": "14.2.10",
|
||||
"next-safe-action": "7.9.3",
|
||||
"optional": "0.1.4",
|
||||
"otplib": "12.0.1",
|
||||
"papaparse": "5.4.1",
|
||||
"posthog-js": "1.167.0",
|
||||
"prismjs": "1.29.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"redis": "^4.7.0",
|
||||
"sharp": "^0.33.4",
|
||||
"ua-parser-js": "^1.0.38",
|
||||
"webpack": "^5.93.0",
|
||||
"xlsx": "^0.18.5"
|
||||
"react-hook-form": "7.53.0",
|
||||
"react-hot-toast": "2.4.1",
|
||||
"react-icons": "5.3.0",
|
||||
"redis": "4.7.0",
|
||||
"sharp": "0.33.5",
|
||||
"ua-parser-js": "1.0.39",
|
||||
"webpack": "5.95.0",
|
||||
"xlsx": "0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@neshca/cache-handler": "^1.5.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/qrcode": "^1.5.5"
|
||||
"@neshca/cache-handler": "1.7.3",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
"@types/lodash": "4.17.10",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/papaparse": "5.3.14",
|
||||
"@types/qrcode": "1.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -32,14 +32,14 @@
|
||||
"storybook": "turbo run storybook"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.45.3",
|
||||
"@playwright/test": "1.45.3",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"eslint": "^8.57.0",
|
||||
"husky": "^9.1.4",
|
||||
"lint-staged": "^15.2.7",
|
||||
"rimraf": "^6.0.1",
|
||||
"tsx": "^4.16.5",
|
||||
"turbo": "^2.0.11"
|
||||
"eslint": "8.57.0",
|
||||
"husky": "9.1.4",
|
||||
"lint-staged": "15.2.7",
|
||||
"rimraf": "6.0.1",
|
||||
"tsx": "4.16.5",
|
||||
"turbo": "2.0.11"
|
||||
},
|
||||
"lint-staged": {
|
||||
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [
|
||||
@@ -64,7 +64,7 @@
|
||||
"showDetails": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@changesets/cli": "^2.27.7",
|
||||
"playwright": "^1.45.3"
|
||||
"@changesets/cli": "2.27.7",
|
||||
"playwright": "1.45.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable no-console -- used for error logging */
|
||||
import { Buffer } from "node:buffer";
|
||||
import type { TUploadFileConfig, TUploadFileResponse } from "@formbricks/types/storage";
|
||||
|
||||
export class StorageAPI {
|
||||
@@ -67,18 +66,18 @@ export class StorageAPI {
|
||||
const formDataForS3 = new FormData();
|
||||
|
||||
if (presignedFields) {
|
||||
Object.keys(presignedFields).forEach((key) => {
|
||||
formDataForS3.append(key, presignedFields[key]);
|
||||
Object.entries(presignedFields).forEach(([key, value]) => {
|
||||
formDataForS3.append(key, value);
|
||||
});
|
||||
|
||||
try {
|
||||
const buffer = Buffer.from(file.base64.split(",")[1], "base64");
|
||||
const blob = new Blob([buffer], { type: file.type });
|
||||
const binaryString = atob(file.base64.split(",")[1]);
|
||||
const uint8Array = Uint8Array.from([...binaryString].map((char) => char.charCodeAt(0)));
|
||||
const blob = new Blob([uint8Array], { type: file.type });
|
||||
|
||||
formDataForS3.append("file", blob);
|
||||
} catch (buffErr) {
|
||||
console.error({ buffErr });
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw new Error("Error uploading file");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
@@ -17,13 +16,5 @@ export default defineConfig({
|
||||
fileName: "index",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({ rollupTypes: true }),
|
||||
nodePolyfills({
|
||||
include: ["buffer"],
|
||||
globals: {
|
||||
Buffer: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins: [dts({ rollupTypes: true })],
|
||||
});
|
||||
|
||||
@@ -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.20.0",
|
||||
"@prisma/extension-accelerate": "1.2.1",
|
||||
"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.20.0",
|
||||
"prisma-dbml-generator": "0.12.0",
|
||||
"prisma-json-types-generator": "3.1.1",
|
||||
"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.52.2",
|
||||
"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.10",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user