mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-01 11:50:43 -05:00
385 lines
14 KiB
TypeScript
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";
|