Files
formbricks/apps/web/modules/ui/components/preview-survey/index.tsx
T
2025-11-14 11:50:55 +05:30

385 lines
14 KiB
TypeScript

"use client";
import { motion } from "framer-motion";
import { ExpandIcon, MonitorIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Environment, Project } from "@formbricks/database/generated/browser";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurvey, TSurveyQuestionId, TSurveyStyling } from "@formbricks/types/surveys/types";
import { cn } from "@/lib/cn";
import { ClientLogo } from "@/modules/ui/components/client-logo";
import { MediaBackground } from "@/modules/ui/components/media-background";
import { ResetProgressButton } from "@/modules/ui/components/reset-progress-button";
import { SurveyInline } from "@/modules/ui/components/survey";
import { Modal } from "./components/modal";
import { TabOption } from "./components/tab-option";
type TPreviewType = "modal" | "fullwidth" | "email";
interface PreviewSurveyProps {
survey: TSurvey;
questionId?: string | null;
previewType?: TPreviewType;
project: Project;
environment: Pick<Environment, "id" | "appSetupCompleted">;
languageCode: string;
isSpamProtectionAllowed: boolean;
}
let surveyNameTemp: string;
let setQuestionId = (_: string) => {};
export const PreviewSurvey = ({
questionId,
survey,
previewType,
project,
environment,
languageCode,
isSpamProtectionAllowed,
}: PreviewSurveyProps) => {
const [isModalOpen, setIsModalOpen] = useState(true);
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
const { t } = useTranslation();
const [appSetupCompleted, setAppSetupCompleted] = useState(false);
const [previewMode, setPreviewMode] = useState("desktop");
const ContentRef = useRef<HTMLDivElement | null>(null);
const { projectOverwrites } = survey || {};
const { placement: surveyPlacement } = projectOverwrites || {};
const { darkOverlay: surveyDarkOverlay } = projectOverwrites || {};
const { clickOutsideClose: surveyClickOutsideClose } = projectOverwrites || {};
const placement = surveyPlacement || project.placement;
const darkOverlay = surveyDarkOverlay ?? project.darkOverlay;
const clickOutsideClose = surveyClickOutsideClose ?? project.clickOutsideClose;
const styling: TSurveyStyling | TProjectStyling = useMemo(() => {
// allow style overwrite is disabled from the project
if (!project.styling.allowStyleOverwrite) {
return project.styling;
}
// allow style overwrite is enabled from the project
if (project.styling.allowStyleOverwrite) {
// survey style overwrite is disabled
if (!survey.styling?.overwriteThemeStyling) {
return project.styling;
}
// survey style overwrite is enabled
return survey.styling;
}
return project.styling;
}, [project.styling, survey.styling]);
const updateQuestionId = useCallback(
(newQuestionId: TSurveyQuestionId) => {
if (
!newQuestionId ||
newQuestionId === "hidden" ||
newQuestionId === "multiLanguage" ||
newQuestionId.includes("fb-variables-")
)
return;
if (newQuestionId === "start" && !survey.welcomeCard.enabled) return;
setQuestionId(newQuestionId);
},
[survey.welcomeCard.enabled]
);
useEffect(() => {
if (questionId) {
updateQuestionId(questionId);
}
}, [questionId, updateQuestionId]);
const onFinished = () => {
// close modal if there are no questions left
if (survey.type === "app" && survey.endings.length === 0) {
setIsModalOpen(false);
setTimeout(() => {
setQuestionId(survey.questions[0]?.id);
setIsModalOpen(true);
}, 500);
}
};
// this useEffect is for refreshing the survey preview only if user is switching between templates on survey templates page and hence we are checking for survey.id === "someUniqeId1" which is a common Id for all templates
useEffect(() => {
if (survey.name !== surveyNameTemp && survey.id === "someUniqueId1") {
resetQuestionProgress();
surveyNameTemp = survey.name;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [survey]);
const resetQuestionProgress = () => {
let storePreviewMode = previewMode;
setPreviewMode("null");
setTimeout(() => {
setPreviewMode(storePreviewMode);
}, 10);
setQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
};
useEffect(() => {
if (environment) {
setAppSetupCompleted(environment.appSetupCompleted);
}
}, [environment]);
const isSpamProtectionEnabled = useMemo(() => {
return isSpamProtectionAllowed && survey.recaptcha?.enabled;
}, [survey.recaptcha?.enabled, isSpamProtectionAllowed]);
const handlePreviewModalClose = () => {
setIsModalOpen(false);
setTimeout(() => {
setIsModalOpen(true);
resetQuestionProgress();
}, 1000);
};
if (!previewType) {
previewType = appSetupCompleted ? "modal" : "fullwidth";
if (!questionId) {
return <></>;
}
}
const handlePreviewModeChange = (mode: "mobile" | "desktop") => {
setPreviewMode(mode);
requestAnimationFrame(() => {
if (questionId) {
setQuestionId(questionId);
}
});
};
return (
<div
className="flex h-full w-full flex-col items-center justify-items-center p-2 py-4"
id="survey-preview">
<motion.div
className={cn(
"z-50 flex h-full w-fit items-center justify-center",
isFullScreenPreview && "h-full w-full bg-zinc-500/50 backdrop-blur-md"
)}
style={{
position: isFullScreenPreview ? "fixed" : "absolute",
zIndex: 50,
left: isFullScreenPreview ? 0 : undefined,
top: isFullScreenPreview ? 0 : undefined,
}}
transition={{
ease: "easeInOut",
delay: 1.5,
}}
/>
<motion.div
layout
style={{
left: isFullScreenPreview ? "2.5%" : undefined,
top: isFullScreenPreview ? 0 : undefined,
}}
transition={{
duration: 0.8,
ease: "easeInOut",
type: "spring",
}}
className={cn(
"z-50 flex h-[95%] w-full items-center justify-center overflow-hidden rounded-lg border border-slate-300",
isFullScreenPreview && "absolute z-50 h-[95%] w-[95%]"
)}>
{previewMode === "mobile" && (
<>
<p className="absolute left-0 top-0 m-2 rounded bg-slate-100 px-2 py-1 text-xs text-slate-400">
Preview
</p>
<div className="absolute right-0 top-0 m-2">
<ResetProgressButton onClick={resetQuestionProgress} />
</div>
<MediaBackground
surveyType={survey.type}
styling={styling}
ContentRef={ContentRef as React.RefObject<HTMLDivElement>}
isMobilePreview>
{previewType === "modal" ? (
<Modal
isOpen={isModalOpen}
placement={placement}
previewMode="mobile"
darkOverlay={darkOverlay}
clickOutsideClose={clickOutsideClose}
borderRadius={styling?.roundness ?? 8}
background={styling?.cardBackgroundColor?.light}>
<SurveyInline
isPreviewMode={true}
survey={survey}
isBrandingEnabled={project.inAppSurveyBranding}
isRedirectDisabled={true}
languageCode={languageCode}
styling={styling}
isCardBorderVisible={!styling.highlightBorderColor?.light}
onClose={handlePreviewModalClose}
getSetQuestionId={(f: (value: string) => void) => {
setQuestionId = f;
}}
onFinished={onFinished}
isSpamProtectionEnabled={isSpamProtectionEnabled}
/>
</Modal>
) : (
<div className="flex h-full w-full flex-col justify-center px-1">
<div className="absolute left-5 top-5">
{!styling.isLogoHidden && (
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
)}
</div>
<div className="z-10 w-full rounded-lg border border-transparent">
<SurveyInline
isPreviewMode={true}
survey={{ ...survey, type: "link" }}
isBrandingEnabled={project.linkSurveyBranding}
languageCode={languageCode}
responseCount={42}
styling={styling}
getSetQuestionId={(f: (value: string) => void) => {
setQuestionId = f;
}}
isSpamProtectionEnabled={isSpamProtectionEnabled}
/>
</div>
</div>
)}
</MediaBackground>
</>
)}
{previewMode === "desktop" && (
<div className="flex h-full flex-1 flex-col">
<div className="flex h-8 w-full items-center rounded-t-lg bg-slate-100">
<div className="ml-6 flex space-x-2">
<div className="h-3 w-3 rounded-full bg-red-500"></div>
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
<button
className="h-3 w-3 cursor-pointer rounded-full bg-emerald-500"
onClick={() => {
if (isFullScreenPreview) {
setIsFullScreenPreview(false);
} else {
setIsFullScreenPreview(true);
}
}}
aria-label={isFullScreenPreview ? "Shrink Preview" : "Expand Preview"}></button>
</div>
<div className="ml-4 flex w-full justify-between font-mono text-sm text-slate-400">
<p>
{previewType === "modal"
? t("environments.surveys.edit.your_web_app")
: t("common.preview")}
</p>
<div className="flex items-center">
{isFullScreenPreview ? (
<ShrinkIcon
className="mr-1 h-[22px] w-[22px] cursor-pointer rounded-md bg-white p-1 text-slate-500 hover:text-slate-700"
onClick={() => {
setIsFullScreenPreview(false);
}}
/>
) : (
<ExpandIcon
className="mr-1 h-[22px] w-[22px] cursor-pointer rounded-md bg-white p-1 text-slate-500 hover:text-slate-700"
onClick={() => {
setIsFullScreenPreview(true);
}}
/>
)}
<ResetProgressButton onClick={resetQuestionProgress} />
</div>
</div>
</div>
{previewType === "modal" ? (
<Modal
isOpen={isModalOpen}
placement={placement}
clickOutsideClose={clickOutsideClose}
darkOverlay={darkOverlay}
previewMode="desktop"
borderRadius={styling.roundness ?? 8}
background={styling.cardBackgroundColor?.light}>
<SurveyInline
isPreviewMode={true}
survey={survey}
isBrandingEnabled={project.inAppSurveyBranding}
isRedirectDisabled={true}
languageCode={languageCode}
styling={styling}
isCardBorderVisible={!styling.highlightBorderColor?.light}
onClose={handlePreviewModalClose}
getSetQuestionId={(f: (value: string) => void) => {
setQuestionId = f;
}}
onFinished={onFinished}
isSpamProtectionEnabled={isSpamProtectionEnabled}
/>
</Modal>
) : (
<MediaBackground
surveyType={survey.type}
styling={styling}
ContentRef={ContentRef as React.RefObject<HTMLDivElement>}
isEditorView>
<div className="absolute left-5 top-5">
{!styling.isLogoHidden && (
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
)}
</div>
<div className="z-0 w-full max-w-4xl rounded-lg border-transparent">
<SurveyInline
isPreviewMode={true}
survey={{ ...survey, type: "link" }}
isBrandingEnabled={project.linkSurveyBranding}
isRedirectDisabled={true}
languageCode={languageCode}
responseCount={42}
styling={styling}
getSetQuestionId={(f: (value: string) => void) => {
setQuestionId = f;
}}
isSpamProtectionEnabled={isSpamProtectionEnabled}
/>
</div>
</MediaBackground>
)}
</div>
)}
</motion.div>
{/* for toggling between mobile and desktop mode */}
<div className="mt-2 flex rounded-full border-2 border-slate-300 p-1">
<TabOption
active={previewMode === "mobile"}
icon={<SmartphoneIcon className="mx-4 my-2 h-4 w-4 text-slate-700" />}
onClick={() => handlePreviewModeChange("mobile")}
/>
<TabOption
active={previewMode === "desktop"}
icon={<MonitorIcon className="mx-4 my-2 h-4 w-4 text-slate-700" />}
onClick={() => handlePreviewModeChange("desktop")}
/>
</div>
</div>
);
};
export { getPlacementStyle } from "./lib/utils";