diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 5ec2fa7ced..301aed0751 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -12,8 +12,8 @@
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
- "extensions": ["dbaeumer.vscode-eslint"]
- }
+ "extensions": ["dbaeumer.vscode-eslint"],
+ },
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
@@ -25,5 +25,5 @@
"postAttachCommand": "pnpm dev --filter=web... --filter=demo...",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
- "remoteUser": "node"
+ "remoteUser": "node",
}
diff --git a/apps/formbricks-com/app/docs/getting-started/framework-guides/page.mdx b/apps/formbricks-com/app/docs/getting-started/framework-guides/page.mdx
index c97313981e..9c37b29742 100644
--- a/apps/formbricks-com/app/docs/getting-started/framework-guides/page.mdx
+++ b/apps/formbricks-com/app/docs/getting-started/framework-guides/page.mdx
@@ -52,7 +52,7 @@ All you need to do is copy a `
+`;
+
+ return (
+
+
+
+ {tabs.map((tab) => (
+ setActiveId(tab.id)}
+ className={cn(
+ tab.id === activeTab
+ ? " bg-slate-100 font-semibold text-slate-900"
+ : "text-slate-500 transition-all duration-300 hover:bg-slate-50 hover:text-slate-700",
+ "flex h-full w-full items-center justify-center rounded-md px-3 py-2 text-center text-sm font-medium"
+ )}
+ aria-current={tab.id === activeTab ? "page" : undefined}>
+ {tab.icon && {tab.icon}
}
+ {tab.label}
+
+ ))}
+
+
+
+ {activeTab === "npm" ? (
+
+
+ npm install @formbricks/js --save
+
+
+ Import Formbricks and initialize the widget in your Component (e.g. App.tsx):
+
+
{`import formbricks from "@formbricks/js";
+
+if (typeof window !== "undefined") {
+ formbricks.init({
+ environmentId: "${environmentId}",
+ apiHost: "${webAppUrl}",
+ });
+}`}
+
+ Read docs
+
+
+ ) : activeTab === "html" ? (
+
+
+ Insert this code into the <head> tag of your website:
+
+
+ {htmlSnippet}
+
+
+ {
+ navigator.clipboard.writeText(htmlSnippet);
+ toast.success("Copied to clipboard");
+ }}>
+ Copy code
+
+
+ Step by step manual
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/web/app/(app)/onboarding/components/inapp/SurveyObjective.tsx b/apps/web/app/(app)/onboarding/components/inapp/SurveyObjective.tsx
new file mode 100644
index 0000000000..4ce13c8b7c
--- /dev/null
+++ b/apps/web/app/(app)/onboarding/components/inapp/SurveyObjective.tsx
@@ -0,0 +1,164 @@
+"use client";
+
+import { updateUserAction } from "@/app/(app)/onboarding/actions";
+import OnboardingTitle from "@/app/(app)/onboarding/components/OnboardingTitle";
+import { handleTabNavigation } from "@/app/(app)/onboarding/utils";
+import { formbricksEnabled, updateResponse } from "@/app/lib/formbricks";
+import { useEffect, useRef, useState } from "react";
+import { toast } from "react-hot-toast";
+
+import { cn } from "@formbricks/lib/cn";
+import { env } from "@formbricks/lib/env";
+import { TUser, TUserObjective } from "@formbricks/types/user";
+import { Button } from "@formbricks/ui/Button";
+import { Input } from "@formbricks/ui/Input";
+
+type ObjectiveProps = {
+ formbricksResponseId?: string;
+ user: TUser;
+ setCurrentStep: (currentStep: number) => void;
+};
+
+type ObjectiveChoice = {
+ label: string;
+ id: TUserObjective;
+};
+
+export const Objective: React.FC = ({ formbricksResponseId, user, setCurrentStep }) => {
+ const objectives: Array = [
+ { label: "Increase conversion", id: "increase_conversion" },
+ { label: "Improve user retention", id: "improve_user_retention" },
+ { label: "Increase user adoption", id: "increase_user_adoption" },
+ { label: "Sharpen marketing messaging", id: "sharpen_marketing_messaging" },
+ { label: "Support sales", id: "support_sales" },
+ { label: "Other", id: "other" },
+ ];
+
+ const [selectedChoice, setSelectedChoice] = useState(null);
+ const [isProfileUpdating, setIsProfileUpdating] = useState(false);
+ const [otherValue, setOtherValue] = useState("");
+
+ const fieldsetRef = useRef(null);
+
+ useEffect(() => {
+ const onKeyDown = handleTabNavigation(fieldsetRef, setSelectedChoice);
+ window.addEventListener("keydown", onKeyDown);
+ return () => {
+ window.removeEventListener("keydown", onKeyDown);
+ };
+ }, [fieldsetRef, setSelectedChoice]);
+
+ const next = () => {
+ setCurrentStep(4);
+ localStorage.setItem("onboardingCurrentStep", "4");
+ };
+
+ const handleNextClick = async () => {
+ if (selectedChoice === "Other" && otherValue.trim() === "") {
+ toast.error("Other value missing");
+ return;
+ }
+ if (selectedChoice) {
+ const selectedObjective = objectives.find((objective) => objective.label === selectedChoice);
+ if (selectedObjective) {
+ try {
+ setIsProfileUpdating(true);
+ await updateUserAction({
+ objective: selectedObjective.id,
+ name: user.name ?? undefined,
+ });
+ setIsProfileUpdating(false);
+ } catch (e) {
+ setIsProfileUpdating(false);
+ console.error(e);
+ toast.error("An error occured saving your settings");
+ }
+ if (formbricksEnabled && env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID && formbricksResponseId) {
+ const res = await updateResponse(
+ formbricksResponseId,
+ {
+ objective: selectedObjective.id === "other" ? otherValue : selectedObjective.label,
+ },
+ true
+ );
+ if (!res.ok) {
+ console.error("Error updating response", res.error);
+ }
+ }
+ next();
+ }
+ }
+ };
+
+ return (
+
+
+
+ Choices
+
+
+
+
+
+ Skip
+
+
+ Next
+
+
+
+ );
+};
diff --git a/apps/web/app/(app)/onboarding/components/inapp/SurveyRole.tsx b/apps/web/app/(app)/onboarding/components/inapp/SurveyRole.tsx
new file mode 100644
index 0000000000..f7077cba4e
--- /dev/null
+++ b/apps/web/app/(app)/onboarding/components/inapp/SurveyRole.tsx
@@ -0,0 +1,160 @@
+"use client";
+
+import { updateUserAction } from "@/app/(app)/onboarding/actions";
+import OnboardingTitle from "@/app/(app)/onboarding/components/OnboardingTitle";
+import { handleTabNavigation } from "@/app/(app)/onboarding/utils";
+import { createResponse, formbricksEnabled } from "@/app/lib/formbricks";
+import { Session } from "next-auth";
+import { useEffect, useRef, useState } from "react";
+import { toast } from "react-hot-toast";
+
+import { cn } from "@formbricks/lib/cn";
+import { env } from "@formbricks/lib/env";
+import { Button } from "@formbricks/ui/Button";
+import { Input } from "@formbricks/ui/Input";
+
+type RoleProps = {
+ setFormbricksResponseId: (id: string) => void;
+ session: Session;
+ setCurrentStep: (currentStep: number) => void;
+};
+
+type RoleChoice = {
+ label: string;
+ id: "project_manager" | "engineer" | "founder" | "marketing_specialist" | "other";
+};
+
+export const Role: React.FC = ({ setFormbricksResponseId, session, setCurrentStep }) => {
+ const [selectedChoice, setSelectedChoice] = useState(null);
+ const [isUpdating, setIsUpdating] = useState(false);
+ const fieldsetRef = useRef(null);
+ const [otherValue, setOtherValue] = useState("");
+
+ useEffect(() => {
+ const onKeyDown = handleTabNavigation(fieldsetRef, setSelectedChoice);
+ window.addEventListener("keydown", onKeyDown);
+ return () => {
+ window.removeEventListener("keydown", onKeyDown);
+ };
+ }, [fieldsetRef, setSelectedChoice]);
+
+ const roles: Array = [
+ { label: "Project Manager", id: "project_manager" },
+ { label: "Engineer", id: "engineer" },
+ { label: "Founder", id: "founder" },
+ { label: "Marketing Specialist", id: "marketing_specialist" },
+ { label: "Other", id: "other" },
+ ];
+
+ const next = () => {
+ setCurrentStep(3);
+ localStorage.setItem("onboardingCurrentStep", "3");
+ };
+
+ const handleNextClick = async () => {
+ if (selectedChoice === "Other" && otherValue.trim() === "") {
+ toast.error("Other value missing");
+ return;
+ }
+ if (selectedChoice) {
+ const selectedRole = roles.find((role) => role.label === selectedChoice);
+ if (selectedRole) {
+ try {
+ setIsUpdating(true);
+ await updateUserAction({
+ role: selectedRole.id,
+ });
+ setIsUpdating(false);
+ } catch (e) {
+ setIsUpdating(false);
+ toast.error("An error occured saving your settings");
+ console.error(e);
+ }
+ if (formbricksEnabled && env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID) {
+ const res = await createResponse(env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID, session.user.id, {
+ role: selectedRole.id === "other" ? otherValue : selectedRole.label,
+ });
+ if (res.ok) {
+ const response = res.data;
+ setFormbricksResponseId(response.id);
+ } else {
+ console.error("Error sending response to Formbricks", res.error);
+ }
+ }
+ next();
+ }
+ }
+ };
+
+ return (
+
+
+
+ Choices
+
+
+
+
+ Skip
+
+
+ Next
+
+
+
+ );
+};
diff --git a/apps/web/app/(app)/onboarding/components/link/CreateFirstSurvey.tsx b/apps/web/app/(app)/onboarding/components/link/CreateFirstSurvey.tsx
new file mode 100644
index 0000000000..cc4cd8f2ed
--- /dev/null
+++ b/apps/web/app/(app)/onboarding/components/link/CreateFirstSurvey.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import {
+ customSurvey,
+ templates,
+} from "@/app/(app)/environments/[environmentId]/surveys/templates/templates";
+import OnboardingTitle from "@/app/(app)/onboarding/components/OnboardingTitle";
+import ChurnImage from "@/images/onboarding-churn.png";
+import FeedbackImage from "@/images/onboarding-collect-feedback.png";
+import NPSImage from "@/images/onboarding-nps.png";
+import { ArrowRight } from "lucide-react";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { toast } from "react-hot-toast";
+
+import { TTemplate } from "@formbricks/types/templates";
+import { Button } from "@formbricks/ui/Button";
+import { OptionCard } from "@formbricks/ui/OptionCard";
+
+import { createSurveyFromTemplate, finishOnboardingAction } from "../../actions";
+
+interface CreateFirstSurveyProps {
+ environmentId: string;
+}
+
+export function CreateFirstSurvey({ environmentId }: CreateFirstSurveyProps) {
+ const router = useRouter();
+ const [loadingTemplate, setLoadingTemplate] = useState(null);
+ const templateOrder = ["Collect Feedback", "Net Promoter Score (NPS)", "Churn Survey"];
+ const templateImages = {
+ "Collect Feedback": FeedbackImage,
+ "Net Promoter Score (NPS)": NPSImage,
+ "Churn Survey": ChurnImage,
+ };
+
+ const filteredTemplates = templates
+ .filter((template) => templateOrder.includes(template.name))
+ .sort((a, b) => templateOrder.indexOf(a.name) - templateOrder.indexOf(b.name));
+
+ const newSurveyFromTemplate = async (template: TTemplate) => {
+ setLoadingTemplate(template.name);
+ if (typeof localStorage !== undefined) {
+ localStorage.removeItem("onboardingPathway");
+ localStorage.removeItem("onboardingCurrentStep");
+ }
+ await finishOnboardingAction();
+ try {
+ const survey = await createSurveyFromTemplate(template, environmentId);
+ router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
+ } catch (e) {
+ toast.error("An error occurred creating a new survey");
+ }
+ };
+
+ return (
+
+
+
+ {filteredTemplates.map((template) => {
+ const TemplateImage = templateImages[template.name];
+ return (
+ newSurveyFromTemplate(template)}
+ loading={loadingTemplate === template.name}>
+
+
+ );
+ })}
+
+
{
+ newSurveyFromTemplate(customSurvey);
+ }}>
+ Start from scratch
+
+
+ );
+}
diff --git a/apps/web/app/(app)/onboarding/components/onboarding.tsx b/apps/web/app/(app)/onboarding/components/onboarding.tsx
new file mode 100644
index 0000000000..72673f04be
--- /dev/null
+++ b/apps/web/app/(app)/onboarding/components/onboarding.tsx
@@ -0,0 +1,166 @@
+"use client";
+
+import jsPackageJson from "@/../../packages/js/package.json";
+import { ConnectWithFormbricks } from "@/app/(app)/onboarding/components/inapp/ConnectWithFormbricks";
+import { InviteTeamMate } from "@/app/(app)/onboarding/components/inapp/InviteTeamMate";
+import { Objective } from "@/app/(app)/onboarding/components/inapp/SurveyObjective";
+import { Role } from "@/app/(app)/onboarding/components/inapp/SurveyRole";
+import { CreateFirstSurvey } from "@/app/(app)/onboarding/components/link/CreateFirstSurvey";
+import { Session } from "next-auth";
+import { useEffect, useState } from "react";
+
+import { TEnvironment } from "@formbricks/types/environment";
+import { TTeam } from "@formbricks/types/teams";
+import { TUser } from "@formbricks/types/user";
+
+import PathwaySelect from "./PathwaySelect";
+import { OnboardingHeader } from "./ProgressBar";
+
+interface OnboardingProps {
+ isFormbricksCloud: boolean;
+ session: Session;
+ environment: TEnvironment;
+ user: TUser;
+ team: TTeam;
+ webAppUrl: string;
+}
+
+export function Onboarding({
+ isFormbricksCloud,
+ session,
+ environment,
+ user,
+ team,
+ webAppUrl,
+}: OnboardingProps) {
+ const [selectedPathway, setSelectedPathway] = useState(null);
+ const [progress, setProgress] = useState(16);
+ const [formbricksResponseId, setFormbricksResponseId] = useState();
+ const [currentStep, setCurrentStep] = useState(null);
+ const [iframeLoaded, setIframeLoaded] = useState(false);
+ const [iframeVisible, setIframeVisible] = useState(false);
+ const [fade, setFade] = useState(false);
+
+ useEffect(() => {
+ if (currentStep === 2 && selectedPathway === "link") {
+ setIframeVisible(true);
+ } else {
+ setIframeVisible(false);
+ }
+ }, [currentStep, iframeLoaded, selectedPathway]);
+
+ useEffect(() => {
+ if (iframeVisible) {
+ setFade(true);
+
+ const handleSurveyCompletion = () => {
+ setFade(false);
+
+ setTimeout(() => {
+ setIframeVisible(false); // Hide the iframe after fade-out effect is complete
+ setCurrentStep(5); // Assuming you want to move to the next step after survey completion
+ }, 1000); // Adjust timeout duration based on your fade-out CSS transition
+ };
+
+ window.addEventListener("formbricksSurveyCompleted", handleSurveyCompletion);
+
+ // Cleanup function to remove the event listener
+ return () => {
+ window.removeEventListener("formbricksSurveyCompleted", handleSurveyCompletion);
+ };
+ }
+ }, [iframeVisible, currentStep]); // Depend on iframeVisible and currentStep to re-evaluate when needed
+
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ // Access localStorage only when window is available
+ const pathwayValueFromLocalStorage = localStorage.getItem("onboardingPathway");
+ const currentStepValueFromLocalStorage = parseInt(localStorage.getItem("onboardingCurrentStep") ?? "1");
+
+ setSelectedPathway(pathwayValueFromLocalStorage);
+ setCurrentStep(currentStepValueFromLocalStorage);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (currentStep) {
+ const stepProgressMap = { 1: 16, 2: 50, 3: 65, 4: 75, 5: 90 };
+ const newProgress = stepProgressMap[currentStep] || 16;
+ setProgress(newProgress);
+ localStorage.setItem("onboardingCurrentStep", currentStep.toString());
+ }
+ }, [currentStep]);
+
+ // Function to render current onboarding step
+ const renderOnboardingStep = () => {
+ switch (currentStep) {
+ case 1:
+ return (
+
+ );
+ case 2:
+ return (
+ selectedPathway !== "link" && (
+
+ )
+ );
+ case 3:
+ return (
+
+ );
+ case 4:
+ return (
+
+ );
+ case 5:
+ return selectedPathway === "link" ? (
+
+ ) : (
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+
+ {renderOnboardingStep()}
+ {iframeVisible && isFormbricksCloud && (
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/app/(app)/onboarding/layout.tsx b/apps/web/app/(app)/onboarding/layout.tsx
new file mode 100644
index 0000000000..f967ef2400
--- /dev/null
+++ b/apps/web/app/(app)/onboarding/layout.tsx
@@ -0,0 +1,19 @@
+import { getServerSession } from "next-auth";
+import { redirect } from "next/navigation";
+
+import { authOptions } from "@formbricks/lib/authOptions";
+import ToasterClient from "@formbricks/ui/ToasterClient";
+
+export default async function EnvironmentLayout({ children }) {
+ const session = await getServerSession(authOptions);
+ if (!session || !session.user) {
+ return redirect(`/auth/login`);
+ }
+
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/apps/web/app/(app)/onboarding/loading.tsx b/apps/web/app/(app)/onboarding/loading.tsx
deleted file mode 100644
index e1f44d88f3..0000000000
--- a/apps/web/app/(app)/onboarding/loading.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function Loading() {
- return (
-
- );
-}
diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx
index 47b56013ce..c343b428ca 100644
--- a/apps/web/app/(app)/onboarding/page.tsx
+++ b/apps/web/app/(app)/onboarding/page.tsx
@@ -1,31 +1,44 @@
+import { Onboarding } from "@/app/(app)/onboarding/components/onboarding";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
+import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
import { getFirstEnvironmentByUserId } from "@formbricks/lib/environment/service";
-import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
+import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getUser } from "@formbricks/lib/user/service";
-import Onboarding from "./components/Onboarding";
-
export default async function OnboardingPage() {
const session = await getServerSession(authOptions);
+
+ // Redirect to login if not authenticated
if (!session) {
- redirect("/auth/login");
+ return redirect("/auth/login");
}
- const userId = session?.user.id;
+
+ // Redirect to home if onboarding is completed
+ if (session.user.onboardingCompleted) {
+ return redirect("/");
+ }
+
+ const userId = session.user.id;
const environment = await getFirstEnvironmentByUserId(userId);
-
- if (!environment) {
- throw new Error("No environment found for user");
- }
-
const user = await getUser(userId);
- const product = await getProductByEnvironmentId(environment?.id!);
+ const team = environment ? await getTeamByEnvironmentId(environment.id) : null;
- if (!environment || !user || !product) {
- throw new Error("Failed to get environment, user, or product");
+ // Ensure all necessary data is available
+ if (!environment || !user || !team) {
+ throw new Error("Failed to get necessary user, environment, or team information");
}
- return ;
+ return (
+
+ );
}
diff --git a/apps/web/app/(app)/onboarding/utils.ts b/apps/web/app/(app)/onboarding/utils.ts
index ef29344037..20ed0b1dfe 100644
--- a/apps/web/app/(app)/onboarding/utils.ts
+++ b/apps/web/app/(app)/onboarding/utils.ts
@@ -1,4 +1,3 @@
-// util.js
export const handleTabNavigation = (fieldsetRef, setSelectedChoice) => (event) => {
if (event.key !== "Tab") {
return;
diff --git a/apps/web/app/api/v1/client/[environmentId]/in-app/sync/route.ts b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/route.ts
index 7e3aaa194a..0146050d30 100644
--- a/apps/web/app/api/v1/client/[environmentId]/in-app/sync/route.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/route.ts
@@ -1,13 +1,18 @@
+import { getFirstSurvey } from "@/app/(app)/environments/[environmentId]/surveys/templates/templates";
import { sendFreeLimitReachedEventToPosthogBiWeekly } from "@/app/api/v1/client/[environmentId]/in-app/sync/lib/posthog";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { NextRequest } from "next/server";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
-import { IS_FORMBRICKS_CLOUD, PRICING_APPSURVEYS_FREE_RESPONSES } from "@formbricks/lib/constants";
+import {
+ IS_FORMBRICKS_CLOUD,
+ PRICING_APPSURVEYS_FREE_RESPONSES,
+ WEBAPP_URL,
+} from "@formbricks/lib/constants";
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
-import { getSurveys } from "@formbricks/lib/survey/service";
+import { createSurvey, getSurveys } from "@formbricks/lib/survey/service";
import { getMonthlyTeamResponseCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TJsStateSync, ZJsPublicSyncInput } from "@formbricks/types/js";
@@ -63,6 +68,8 @@ export async function GET(
}
if (!environment?.widgetSetupCompleted) {
+ const firstSurvey = getFirstSurvey(WEBAPP_URL);
+ await createSurvey(environmentId, firstSurvey);
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
}
diff --git a/apps/web/app/lib/questions.ts b/apps/web/app/lib/questions.ts
index 2e5acf7e15..6d58339aa0 100644
--- a/apps/web/app/lib/questions.ts
+++ b/apps/web/app/lib/questions.ts
@@ -36,6 +36,7 @@ export const questionTypes: TSurveyQuestionType[] = [
subheader: "Who? Who? Who?",
placeholder: "Type your answer here...",
longAnswer: true,
+ inputType: "text",
},
},
{
diff --git a/apps/web/app/s/[surveyId]/components/MediaBackground.tsx b/apps/web/app/s/[surveyId]/components/MediaBackground.tsx
index 94a32a76a9..bfef1368c4 100644
--- a/apps/web/app/s/[surveyId]/components/MediaBackground.tsx
+++ b/apps/web/app/s/[surveyId]/components/MediaBackground.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useEffect, useRef } from "react";
+import React, { useEffect, useRef, useState } from "react";
import { TSurvey } from "@formbricks/types/surveys";
@@ -20,15 +20,30 @@ export const MediaBackground: React.FC = ({
ContentRef,
}) => {
const animatedBackgroundRef = useRef(null);
+ const [backgroundLoaded, setBackgroundLoaded] = useState(false);
useEffect(() => {
- if (survey.styling?.background?.bgType === "animation") {
- if (animatedBackgroundRef.current && survey.styling?.background?.bg) {
- animatedBackgroundRef.current.src = survey.styling?.background?.bg;
- animatedBackgroundRef.current.play();
- }
+ if (survey.styling?.background?.bgType === "animation" && animatedBackgroundRef.current) {
+ const video = animatedBackgroundRef.current;
+ const onCanPlayThrough = () => setBackgroundLoaded(true);
+ video.addEventListener("canplaythrough", onCanPlayThrough);
+ video.src = survey.styling?.background?.bg || "";
+
+ // Cleanup
+ return () => video.removeEventListener("canplaythrough", onCanPlayThrough);
+ } else if (survey.styling?.background?.bgType === "image" && survey.styling?.background?.bg) {
+ // For images, we create a new Image object to listen for the 'load' event
+ const img = new Image();
+ img.onload = () => setBackgroundLoaded(true);
+ img.src = survey.styling?.background?.bg;
+ } else {
+ // For colors or any other types, set to loaded immediately
+ setBackgroundLoaded(true);
}
- }, [survey.styling?.background?.bg, survey.styling?.background?.bgType]);
+ }, [survey.styling?.background]);
+
+ const baseClasses = "absolute inset-0 h-full w-full transition-opacity duration-500";
+ const loadedClass = backgroundLoaded ? "opacity-100" : "opacity-0";
const getFilterStyle = () => {
return survey.styling?.background?.brightness
@@ -38,13 +53,12 @@ export const MediaBackground: React.FC = ({
const renderBackground = () => {
const filterStyle = getFilterStyle();
- const baseClasses = "absolute inset-0 h-full w-full";
switch (survey.styling?.background?.bgType) {
case "color":
return (
);
@@ -55,23 +69,20 @@ export const MediaBackground: React.FC = ({
muted
loop
autoPlay
- className={`${baseClasses} object-cover`}
+ className={`${baseClasses} ${loadedClass} object-cover`}
style={{ filter: `${filterStyle}` }}>
-
+
);
case "image":
return (
);
default:
- return
;
+ return
;
}
};
diff --git a/apps/web/images/onboarding-churn.png b/apps/web/images/onboarding-churn.png
new file mode 100644
index 0000000000..f5ee19f673
Binary files /dev/null and b/apps/web/images/onboarding-churn.png differ
diff --git a/apps/web/images/onboarding-collect-feedback.png b/apps/web/images/onboarding-collect-feedback.png
new file mode 100644
index 0000000000..92cf31e63d
Binary files /dev/null and b/apps/web/images/onboarding-collect-feedback.png differ
diff --git a/apps/web/images/onboarding-dance.gif b/apps/web/images/onboarding-dance.gif
new file mode 100644
index 0000000000..688a20613d
Binary files /dev/null and b/apps/web/images/onboarding-dance.gif differ
diff --git a/apps/web/images/onboarding-in-app-survey.png b/apps/web/images/onboarding-in-app-survey.png
new file mode 100644
index 0000000000..f7529d2b5c
Binary files /dev/null and b/apps/web/images/onboarding-in-app-survey.png differ
diff --git a/apps/web/images/onboarding-link-survey.webp b/apps/web/images/onboarding-link-survey.webp
new file mode 100644
index 0000000000..a52183331f
Binary files /dev/null and b/apps/web/images/onboarding-link-survey.webp differ
diff --git a/apps/web/images/onboarding-lost.gif b/apps/web/images/onboarding-lost.gif
new file mode 100644
index 0000000000..937fe99126
Binary files /dev/null and b/apps/web/images/onboarding-lost.gif differ
diff --git a/apps/web/images/onboarding-nps.png b/apps/web/images/onboarding-nps.png
new file mode 100644
index 0000000000..35ebb5e0bf
Binary files /dev/null and b/apps/web/images/onboarding-nps.png differ
diff --git a/apps/web/playwright/action.spec.ts b/apps/web/playwright/action.spec.ts
index 0c50abfecc..b32fedefa3 100644
--- a/apps/web/playwright/action.spec.ts
+++ b/apps/web/playwright/action.spec.ts
@@ -1,7 +1,7 @@
import { actions, users } from "@/playwright/utils/mock";
import { Page, expect, test } from "@playwright/test";
-import { login, signUpAndLogin, skipOnboarding } from "./utils/helper";
+import { finishOnboarding, login, signUpAndLogin } from "./utils/helper";
const createNoCodeActionByCSSSelector = async (
page: Page,
@@ -13,7 +13,7 @@ const createNoCodeActionByCSSSelector = async (
selector: string
) => {
await signUpAndLogin(page, username, email, password);
- await skipOnboarding(page);
+ await finishOnboarding(page);
await page.getByRole("link", { name: "Actions & Attributes" }).click();
await page.waitForURL(/\/environments\/[^/]+\/actions/);
@@ -55,7 +55,7 @@ const createNoCodeActionByPageURL = async (
testURL: string
) => {
await signUpAndLogin(page, username, email, password);
- await skipOnboarding(page);
+ await finishOnboarding(page);
await page.getByRole("link", { name: "Actions & Attributes" }).click();
await page.waitForURL(/\/environments\/[^/]+\/actions/);
@@ -104,7 +104,7 @@ const createNoCodeActionByInnerText = async (
innerText: string
) => {
await signUpAndLogin(page, username, email, password);
- await skipOnboarding(page);
+ await finishOnboarding(page);
await page.getByRole("link", { name: "Actions & Attributes" }).click();
await page.waitForURL(/\/environments\/[^/]+\/actions/);
@@ -276,7 +276,7 @@ test.describe("Create and Edit Code Action", async () => {
test("Create Code Action", async ({ page }) => {
await signUpAndLogin(page, username, email, password);
- await skipOnboarding(page);
+ await finishOnboarding(page);
await page.getByRole("link", { name: "Actions & Attributes" }).click();
await page.waitForURL(/\/environments\/[^/]+\/actions/);
diff --git a/apps/web/playwright/js.spec.ts b/apps/web/playwright/js.spec.ts
index 69b381063b..2272520733 100644
--- a/apps/web/playwright/js.spec.ts
+++ b/apps/web/playwright/js.spec.ts
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
-import { login, replaceEnvironmentIdInHtml, signUpAndLogin, skipOnboarding } from "./utils/helper";
+import { finishOnboarding, login, replaceEnvironmentIdInHtml, signUpAndLogin } from "./utils/helper";
import { users } from "./utils/mock";
test.describe("JS Package Test", async () => {
@@ -10,7 +10,7 @@ test.describe("JS Package Test", async () => {
test("Admin creates an In-App Survey", async ({ page }) => {
await signUpAndLogin(page, name, email, password);
- await skipOnboarding(page);
+ await finishOnboarding(page);
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
diff --git a/apps/web/playwright/onboarding.spec.ts b/apps/web/playwright/onboarding.spec.ts
index a26a0fa686..98a6e64ff3 100644
--- a/apps/web/playwright/onboarding.spec.ts
+++ b/apps/web/playwright/onboarding.spec.ts
@@ -3,48 +3,32 @@ import { expect, test } from "@playwright/test";
import { signUpAndLogin } from "./utils/helper";
import { teams, users } from "./utils/mock";
-const { role, productName, useCase } = teams.onboarding[0];
+const { productName } = teams.onboarding[0];
test.describe("Onboarding Flow Test", async () => {
- test("Step by Step", async ({ page }) => {
+ test("link survey", async ({ page }) => {
const { name, email, password } = users.onboarding[0];
await signUpAndLogin(page, name, email, password);
await page.waitForURL("/onboarding");
await expect(page).toHaveURL("/onboarding");
- await page.getByRole("button", { name: "Begin (1 min)" }).click();
- await page.getByLabel(role).check();
- await page.getByRole("button", { name: "Next" }).click();
-
- await expect(page.getByLabel(useCase)).toBeVisible();
- await page.getByLabel(useCase).check();
- await page.getByRole("button", { name: "Next" }).click();
-
- await expect(page.getByPlaceholder("e.g. Formbricks")).toBeVisible();
- await page.getByPlaceholder("e.g. Formbricks").fill(productName);
-
- await page.locator("#color-picker").click();
- await page.getByLabel("Hue").click();
-
- await page.locator("div").filter({ hasText: "Create your team's product." }).nth(1).click();
- await page.getByRole("button", { name: "Done" }).click();
-
- await page.waitForURL(/\/environments\/[^/]+\/surveys/);
- await expect(page).toHaveURL(/\/environments\/[^/]+\/surveys/);
- await expect(page.getByText(productName)).toBeVisible();
+ await page.getByRole("button", { name: "Link Surveys Create a new" }).click();
+ await page.getByRole("button", { name: "Collect Feedback Collect" }).click();
+ await page.getByRole("button", { name: "Back", exact: true }).click();
+ await page.getByRole("button", { name: "Save" }).click();
});
- test("Skip", async ({ page }) => {
+ test("In app survey", async ({ page }) => {
const { name, email, password } = users.onboarding[1];
await signUpAndLogin(page, name, email, password);
await page.waitForURL("/onboarding");
await expect(page).toHaveURL("/onboarding");
-
- await page.getByRole("button", { name: "I'll do it later" }).click();
- await page.getByRole("button", { name: "I'll do it later" }).click();
-
+ await page.getByRole("button", { name: "In-app Surveys Run a survey" }).click();
+ await page.getByRole("button", { name: "I am not sure how to do this" }).click();
+ await page.locator("input").click();
+ await page.locator("input").fill("test@gmail.com");
+ await page.getByRole("button", { name: "Invite" }).click();
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
- await expect(page).toHaveURL(/\/environments\/[^/]+\/surveys/);
- await expect(page.getByText("My Product")).toBeVisible();
+ await expect(page.getByText(productName)).toBeVisible();
});
});
diff --git a/apps/web/playwright/team.spec.ts b/apps/web/playwright/team.spec.ts
index e47854c428..c69ac40e77 100644
--- a/apps/web/playwright/team.spec.ts
+++ b/apps/web/playwright/team.spec.ts
@@ -1,6 +1,6 @@
import { expect, test } from "playwright/test";
-import { login, signUpAndLogin, signupUsingInviteToken, skipOnboarding } from "./utils/helper";
+import { finishOnboarding, login, signUpAndLogin, signupUsingInviteToken } from "./utils/helper";
import { invites, users } from "./utils/mock";
test.describe("Invite, accept and remove team member", async () => {
@@ -10,7 +10,7 @@ test.describe("Invite, accept and remove team member", async () => {
test("Invite team member", async ({ page }) => {
await signUpAndLogin(page, name, email, password);
- await skipOnboarding(page);
+ await finishOnboarding(page);
const dropdownTrigger = page.locator("#userDropdownTrigger");
await expect(dropdownTrigger).toBeVisible();
@@ -84,7 +84,7 @@ test.describe("Invite, accept and remove team member", async () => {
await page.getByRole("link", { name: "Create account" }).click();
await signupUsingInviteToken(page, name, email, password);
- await skipOnboarding(page);
+ await finishOnboarding(page);
});
test("Remove member", async ({ page }) => {
diff --git a/apps/web/playwright/utils/helper.ts b/apps/web/playwright/utils/helper.ts
index a1885833d6..81b1459e23 100644
--- a/apps/web/playwright/utils/helper.ts
+++ b/apps/web/playwright/utils/helper.ts
@@ -37,14 +37,15 @@ export const login = async (page: Page, email: string, password: string): Promis
await page.getByRole("button", { name: "Login with Email" }).click();
};
-export const skipOnboarding = async (page: Page): Promise => {
+export const finishOnboarding = async (page: Page): Promise => {
await page.waitForURL("/onboarding");
await expect(page).toHaveURL("/onboarding");
- await page.getByRole("button", { name: "I'll do it later" }).click();
- await page.waitForTimeout(500);
- await page.getByRole("button", { name: "I'll do it later" }).click();
+ await page.getByRole("button", { name: "In-app Surveys Run a survey" }).click();
+ await page.getByRole("button", { name: "I am not sure how to do this" }).click();
+ await page.locator("input").click();
+ await page.locator("input").fill("test@gmail.com");
+ await page.getByRole("button", { name: "Invite" }).click();
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
- await expect(page).toHaveURL(/\/environments\/[^/]+\/surveys/);
await expect(page.getByText("My Product")).toBeVisible();
};
@@ -87,7 +88,7 @@ export const createSurvey = async (
const addQuestion = "Add QuestionAdd a new question to your survey";
await signUpAndLogin(page, name, email, password);
- await skipOnboarding(page);
+ await finishOnboarding(page);
await page.getByRole("heading", { name: "Start from Scratch" }).click();
diff --git a/apps/web/playwright/utils/mock.ts b/apps/web/playwright/utils/mock.ts
index d70bd0b14e..7a8fc56404 100644
--- a/apps/web/playwright/utils/mock.ts
+++ b/apps/web/playwright/utils/mock.ts
@@ -21,7 +21,7 @@ export const users = {
survey: [
{
name: "Survey User 1",
- email: "survey1@formbricks.com",
+ email: "survey3@formbricks.com",
password: "Y1I*EpURUSb32j5XijP",
},
{
@@ -83,7 +83,7 @@ export const teams = {
{
role: "Founder",
useCase: "Increase conversion",
- productName: "Formbricks E2E Test Suite",
+ productName: "My Product",
},
],
};
diff --git a/apps/web/public/onboarding/meme.png b/apps/web/public/onboarding/meme.png
new file mode 100644
index 0000000000..4c12b965f3
Binary files /dev/null and b/apps/web/public/onboarding/meme.png differ
diff --git a/apps/web/vercel.json b/apps/web/vercel.json
index 6ad55b7606..7d90f55144 100644
--- a/apps/web/vercel.json
+++ b/apps/web/vercel.json
@@ -7,7 +7,7 @@
"maxDuration": 10,
"memory": 300
},
- "app/**/*.ts": {
+ "**/*.ts": {
"maxDuration": 10,
"memory": 512
}
diff --git a/packages/lib/emails/emails.ts b/packages/lib/emails/emails.ts
index 667b4d05ec..13d7d2ad7b 100644
--- a/packages/lib/emails/emails.ts
+++ b/packages/lib/emails/emails.ts
@@ -126,7 +126,9 @@ export const sendInviteMemberEmail = async (
inviteId: string,
email: string,
inviterName: string | null,
- inviteeName: string | null
+ inviteeName: string | null,
+ isOnboardingInvite?: boolean,
+ inviteMessage?: string
) => {
const token = createInviteToken(inviteId, email, {
expiresIn: "7d",
@@ -134,16 +136,35 @@ export const sendInviteMemberEmail = async (
const verifyLink = `${WEBAPP_URL}/invite?token=${encodeURIComponent(token)}`;
- await sendEmail({
- to: email,
- subject: `You're invited to collaborate on Formbricks!`,
- html: withEmailTemplate(`Hey ${inviteeName},
- Your colleague ${inviterName} invited you to join them at Formbricks. To accept the invitation, please click the link below:
- Join team
-
- Have a great day!
- The Formbricks Team!`),
- });
+ if (isOnboardingInvite && inviteMessage) {
+ await sendEmail({
+ to: email,
+ subject: `${inviterName} needs a hand setting up Formbricks. Can you help out?`,
+ html: withEmailTemplate(`Hey π,
+ ${inviteMessage}
+ Get Started in Minutes
+
+ Create an account to join ${inviterName}'s team.
+ Connect Formbricks to your app or website via HTML Snippet or NPM in just a few minutes.
+ Done β
+
+ Join ${inviterName}'s team
+
+ Have a great day!
+ The Formbricks Team!`),
+ });
+ } else {
+ await sendEmail({
+ to: email,
+ subject: `You're invited to collaborate on Formbricks!`,
+ html: withEmailTemplate(`Hey ${inviteeName},
+ Your colleague ${inviterName} invited you to join them at Formbricks. To accept the invitation, please click the link below:
+ Join team
+
+ Have a great day!
+ The Formbricks Team!`),
+ });
+ }
};
export const sendInviteAcceptedEmail = async (inviterName: string, inviteeName: string, email: string) => {
diff --git a/packages/lib/invite/service.ts b/packages/lib/invite/service.ts
index 1c7124c40a..c942a93e54 100644
--- a/packages/lib/invite/service.ts
+++ b/packages/lib/invite/service.ts
@@ -194,10 +194,14 @@ export const inviteUser = async ({
currentUser,
invitee,
teamId,
+ isOnboardingInvite,
+ inviteMessage,
}: {
teamId: string;
invitee: TInvitee;
currentUser: TCurrentUser;
+ isOnboardingInvite?: boolean;
+ inviteMessage?: string;
}): Promise => {
validateInputs([teamId, ZString], [invitee, ZInvitee], [currentUser, ZCurrentUser]);
@@ -239,6 +243,6 @@ export const inviteUser = async ({
teamId: invite.teamId,
});
- await sendInviteMemberEmail(invite.id, email, currentUserName, name);
+ await sendInviteMemberEmail(invite.id, email, currentUserName, name, isOnboardingInvite, inviteMessage);
return invite;
};
diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts
index a6eec2112a..932b8aa6e1 100644
--- a/packages/lib/survey/service.ts
+++ b/packages/lib/survey/service.ts
@@ -31,6 +31,15 @@ import { validateInputs } from "../utils/validate";
import { surveyCache } from "./cache";
import { anySurveyHasFilters } from "./util";
+interface TriggerUpdate {
+ create?: Array<{ actionClassId: string }>;
+ deleteMany?: {
+ actionClassId: {
+ in: string[];
+ };
+ };
+}
+
export const selectSurvey = {
id: true,
createdAt: true,
@@ -100,6 +109,47 @@ const revalidateSurveyByActionClassId = (actionClasses: TActionClass[], actionCl
}
};
+const processTriggerUpdates = (
+ triggers: string[],
+ currentSurveyTriggers: string[],
+ actionClasses: TActionClass[]
+) => {
+ const newTriggers: string[] = [];
+ const removedTriggers: string[] = [];
+
+ // find added triggers
+ for (const trigger of triggers) {
+ if (!trigger || currentSurveyTriggers.includes(trigger)) {
+ continue;
+ }
+ newTriggers.push(trigger);
+ }
+
+ // find removed triggers
+ for (const trigger of currentSurveyTriggers) {
+ if (!triggers.includes(trigger)) {
+ removedTriggers.push(getActionClassIdFromName(actionClasses, trigger));
+ }
+ }
+
+ // Construct the triggers update object
+ const triggersUpdate: TriggerUpdate = {};
+
+ if (newTriggers.length > 0) {
+ triggersUpdate.create = newTriggers.map((trigger) => ({
+ actionClassId: getActionClassIdFromName(actionClasses, trigger),
+ }));
+ }
+
+ if (removedTriggers.length > 0) {
+ triggersUpdate.deleteMany = {
+ actionClassId: { in: removedTriggers },
+ };
+ }
+ revalidateSurveyByActionClassId(actionClasses, [...newTriggers, ...removedTriggers]);
+ return triggersUpdate;
+};
+
export const getSurvey = async (surveyId: string): Promise => {
const survey = await unstable_cache(
async () => {
@@ -281,52 +331,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise =>
const { triggers, environmentId, segment, ...surveyData } = updatedSurvey;
if (triggers) {
- const newTriggers: string[] = [];
- const removedTriggers: string[] = [];
-
- // find added triggers
- for (const trigger of triggers) {
- if (!trigger) {
- continue;
- }
- if (currentSurvey.triggers.find((t) => t === trigger)) {
- continue;
- } else {
- newTriggers.push(trigger);
- }
- }
-
- // find removed triggers
- for (const trigger of currentSurvey.triggers) {
- if (triggers.find((t: any) => t === trigger)) {
- continue;
- } else {
- removedTriggers.push(trigger);
- }
- }
- // create new triggers
- if (newTriggers.length > 0) {
- data.triggers = {
- ...(data.triggers || []),
- create: newTriggers.map((trigger) => ({
- actionClassId: getActionClassIdFromName(actionClasses, trigger),
- })),
- };
- }
- // delete removed triggers
- if (removedTriggers.length > 0) {
- data.triggers = {
- ...(data.triggers || []),
- deleteMany: {
- actionClassId: {
- in: removedTriggers.map((trigger) => getActionClassIdFromName(actionClasses, trigger)),
- },
- },
- };
- }
-
- // Revalidation for newly added/removed actionClassId
- revalidateSurveyByActionClassId(actionClasses, [...newTriggers, ...removedTriggers]);
+ data.triggers = processTriggerUpdates(triggers, currentSurvey.triggers, actionClasses);
}
if (segment) {
@@ -441,8 +446,10 @@ export const createSurvey = async (environmentId: string, surveyBody: TSurveyInp
const data: Omit = {
...surveyBody,
- // TODO: Create with triggers & attributeFilters
- triggers: undefined,
+ // TODO: Create with attributeFilters
+ triggers: surveyBody.triggers
+ ? processTriggerUpdates(surveyBody.triggers, [], await getActionClasses(environmentId))
+ : undefined,
attributeFilters: undefined,
};
diff --git a/packages/lib/survey/tests/survey.test.ts b/packages/lib/survey/tests/survey.test.ts
index f15725b305..d3d37d63ba 100644
--- a/packages/lib/survey/tests/survey.test.ts
+++ b/packages/lib/survey/tests/survey.test.ts
@@ -3,7 +3,7 @@ import { prisma } from "../../__mocks__/database";
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, it } from "vitest";
-import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
+import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { testInputValidation } from "../../vitestSetup";
import {
@@ -204,6 +204,7 @@ describe("Tests for createSurvey", () => {
it("Creates a survey successfully", async () => {
prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput);
prisma.team.findFirst.mockResolvedValueOnce(mockTeamOutput);
+ prisma.actionClass.findMany.mockResolvedValue([mockActionClass]);
prisma.user.findMany.mockResolvedValueOnce([
{
...mockUser,
diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json
index b14ee541ef..fa378c95b1 100644
--- a/packages/lib/tsconfig.json
+++ b/packages/lib/tsconfig.json
@@ -6,13 +6,13 @@
"downlevelIteration": true,
"baseUrl": ".",
"paths": {
- "@prisma/client/*": ["@formbricks/database/client/*"]
+ "@prisma/client/*": ["@formbricks/database/client/*"],
},
"plugins": [
{
- "name": "next"
- }
+ "name": "next",
+ },
],
- "strictNullChecks": true
- }
+ "strictNullChecks": true,
+ },
}
diff --git a/packages/lib/user/service.ts b/packages/lib/user/service.ts
index 51e729114a..d3d40ff7ce 100644
--- a/packages/lib/user/service.ts
+++ b/packages/lib/user/service.ts
@@ -168,8 +168,6 @@ export const createUser = async (data: TUserCreateInput): Promise => {
select: responseSelection,
});
- console.log("user", user);
-
userCache.revalidate({
email: user.email,
id: user.id,
@@ -210,6 +208,7 @@ export const deleteUser = async (id: string): Promise => {
const teamHasAtLeastOneAdmin = teamAdminMemberships.length > 0;
const teamHasOnlyOneMember = teamMemberships.length === 1;
const currentUserIsTeamOwner = role === "owner";
+ await deleteMembership(id, teamId);
if (teamHasOnlyOneMember) {
await deleteTeam(teamId);
@@ -219,8 +218,6 @@ export const deleteUser = async (id: string): Promise => {
} else if (currentUserIsTeamOwner) {
await deleteTeam(teamId);
}
-
- await deleteMembership(id, teamId);
}
const deletedUser = await deleteUserById(id);
diff --git a/packages/surveys/src/components/general/Survey.tsx b/packages/surveys/src/components/general/Survey.tsx
index e9b207b7bb..d240ce4f93 100644
--- a/packages/surveys/src/components/general/Survey.tsx
+++ b/packages/surveys/src/components/general/Survey.tsx
@@ -150,6 +150,9 @@ export function Survey({
const finished = nextQuestionId === "end";
onResponse({ data: responseData, ttc, finished });
if (finished) {
+ // Dispatching a custom event when the survey is completed
+ const event = new CustomEvent("formbricksSurveyCompleted", { detail: { surveyId: survey.id } });
+ window.top?.dispatchEvent(event);
onFinished();
}
setQuestionId(nextQuestionId);
diff --git a/packages/surveys/src/components/general/ThankYouCard.tsx b/packages/surveys/src/components/general/ThankYouCard.tsx
index 6bcb1acc71..f92031c91c 100644
--- a/packages/surveys/src/components/general/ThankYouCard.tsx
+++ b/packages/surveys/src/components/general/ThankYouCard.tsx
@@ -75,7 +75,7 @@ export default function ThankYouCard({
isLastQuestion={false}
onClick={() => {
if (!buttonLink) return;
- window.location.href = buttonLink;
+ window.location.replace(buttonLink);
}}
/>
Press Enter β΅
diff --git a/packages/tailwind-config/tailwind.config.js b/packages/tailwind-config/tailwind.config.js
index a98dfc8765..2e6df32452 100644
--- a/packages/tailwind-config/tailwind.config.js
+++ b/packages/tailwind-config/tailwind.config.js
@@ -21,6 +21,11 @@ module.exports = {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
},
+ boxShadow: {
+ "card-sm": "0px 0.5px 12px -5px rgba(30,41,59,0.20)",
+ "card-md": "0px 1px 25px -10px rgba(30,41,59,0.30)",
+ "card-lg": "0px 2px 51px -19px rgba(30,41,59,0.40)",
+ },
colors: {
brand: {
DEFAULT: "#00E6CA",
diff --git a/packages/types/surveys.ts b/packages/types/surveys.ts
index 80c59a6d1a..4bd6cb0047 100644
--- a/packages/types/surveys.ts
+++ b/packages/types/surveys.ts
@@ -391,7 +391,7 @@ export const ZSurveyQuestions = z.array(ZSurveyQuestion);
export type TSurveyQuestions = z.infer;
-const ZSurveyDisplayOption = z.enum(["displayOnce", "displayMultiple", "respondMultiple"]);
+export const ZSurveyDisplayOption = z.enum(["displayOnce", "displayMultiple", "respondMultiple"]);
export type TSurveyDisplayOption = z.infer;
diff --git a/packages/ui/CodeBlock/index.tsx b/packages/ui/CodeBlock/index.tsx
index a4c2c869c0..1a6383dac9 100644
--- a/packages/ui/CodeBlock/index.tsx
+++ b/packages/ui/CodeBlock/index.tsx
@@ -8,16 +8,20 @@ import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
+import "./style.css";
+
interface CodeBlockProps {
children: React.ReactNode;
language: string;
customCodeClass?: string;
+ customEditorClass?: string;
showCopyToClipboard?: boolean;
}
const CodeBlock: React.FC = ({
children,
language,
+ customEditorClass = "",
customCodeClass = "",
showCopyToClipboard = true,
}) => {
@@ -28,16 +32,18 @@ const CodeBlock: React.FC = ({
return (
{showCopyToClipboard && (
-
{
- const childText = children?.toString() || "";
- navigator.clipboard.writeText(childText);
- toast.success("Copied to clipboard");
- }}
- />
+
+ {
+ const childText = children?.toString() || "";
+ navigator.clipboard.writeText(childText);
+ toast.success("Copied to clipboard");
+ }}
+ />
+
)}
-
+
{children}
diff --git a/packages/ui/CodeBlock/style.css b/packages/ui/CodeBlock/style.css
new file mode 100644
index 0000000000..a688902d9c
--- /dev/null
+++ b/packages/ui/CodeBlock/style.css
@@ -0,0 +1,21 @@
+pre {
+ scrollbar-width: thin;
+ scrollbar-color: #e2e8f0 #ffffff;
+ }
+
+ pre::-webkit-scrollbar {
+ width: 4px !important;
+ border-radius: 99px;
+ }
+
+ pre::-webkit-scrollbar-track {
+ background: #e2e8f0;
+ border-radius: 99px;
+ }
+
+pre::-webkit-scrollbar-thumb {
+ background-color: #cbd5e1;
+ border: 3px solid #cbd5e1;
+ border-radius: 99px;
+ }
+
\ No newline at end of file
diff --git a/packages/ui/OptionCard/index.tsx b/packages/ui/OptionCard/index.tsx
new file mode 100644
index 0000000000..c05081999f
--- /dev/null
+++ b/packages/ui/OptionCard/index.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+
+import LoadingSpinner from "../LoadingSpinner";
+
+interface PathwayOptionProps {
+ size: "sm" | "md" | "lg";
+ title: string;
+ description: string;
+ loading?: boolean;
+ onSelect: () => void;
+ children?: React.ReactNode;
+}
+
+const sizeClasses = {
+ sm: "rounded-lg border border-slate-200 shadow-card-sm transition-all duration-150",
+ md: "rounded-xl border border-slate-200 shadow-card-md transition-all duration-300",
+ lg: "rounded-2xl border border-slate-200 shadow-card-lg transition-all duration-500",
+};
+
+export const OptionCard: React.FC = ({
+ size,
+ title,
+ description,
+ children,
+ onSelect,
+ loading,
+}) => (
+
+
+
+ {children}
+
+
{title}
+
{description}
+
+
+
+ {loading && (
+
+
+
+ )}
+
+);
diff --git a/packages/ui/ProgressBar/index.tsx b/packages/ui/ProgressBar/index.tsx
index ba86a242d1..a0e83e18e5 100644
--- a/packages/ui/ProgressBar/index.tsx
+++ b/packages/ui/ProgressBar/index.tsx
@@ -13,7 +13,7 @@ export const ProgressBar: React.FC = ({ progress, barColor, he
+ style={{ width: `${Math.floor(progress * 100)}%`, transition: "width 0.5s ease-out" }}>
);
};
diff --git a/packages/ui/SurveysList/index.tsx b/packages/ui/SurveysList/index.tsx
index 01b20fafa2..adc31071a0 100644
--- a/packages/ui/SurveysList/index.tsx
+++ b/packages/ui/SurveysList/index.tsx
@@ -30,14 +30,12 @@ export default function SurveysList({
const [filteredSurveys, setFilteredSurveys] = useState(surveys);
// Initialize orientation state with a function that checks if window is defined
const [orientation, setOrientation] = useState(() =>
- typeof window !== "undefined" ? localStorage.getItem("surveyOrientation") || "grid" : "grid"
+ typeof localStorage !== "undefined" ? localStorage.getItem("surveyOrientation") || "grid" : "grid"
);
// Save orientation to localStorage
useEffect(() => {
- if (typeof window !== "undefined") {
- localStorage.setItem("surveyOrientation", orientation);
- }
+ localStorage.setItem("surveyOrientation", orientation);
}, [orientation]);
return (
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
index 757a8dd6e7..cea88f2605 100644
--- a/packages/ui/tsconfig.json
+++ b/packages/ui/tsconfig.json
@@ -3,6 +3,6 @@
"include": [".", "../types/*.d.ts"],
"exclude": ["build", "node_modules"],
"compilerOptions": {
- "lib": ["ES2021.String"]
- }
+ "lib": ["ES2021.String"],
+ },
}