mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-19 02:10:33 -05:00
fix: product settings
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import PreviewSurvey from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey";
|
||||
import UnifiedStylingPreviewSurvey from "@/app/(app)/environments/[environmentId]/settings/lookandfeel/components/UnifiedStylingPreviewSurvey";
|
||||
import { RotateCcwIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -390,16 +389,13 @@ const UnifiedStyling = ({ product }: UnifiedStylingProps) => {
|
||||
|
||||
{/* Survey Preview */}
|
||||
|
||||
<div className="w-1/2 bg-slate-100">
|
||||
<div className="max-h-96">
|
||||
<PreviewSurvey
|
||||
<div className="w-1/2 bg-slate-100 pt-4">
|
||||
<div className="h-full max-h-[800px]">
|
||||
<UnifiedStylingPreviewSurvey
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
survey={previewSurvey as TSurvey}
|
||||
environment={{ widgetSetupCompleted: true } as TEnvironment}
|
||||
product={product}
|
||||
previewType={previewSurvey.type === "web" ? "modal" : "fullwidth"}
|
||||
onFileUpload={async (file) => file.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal";
|
||||
import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground";
|
||||
import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
|
||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/24/solid";
|
||||
import { Variants, motion } from "framer-motion";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { TProduct } from "@formbricks/types/product";
|
||||
import { TStyling } from "@formbricks/types/styling";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
|
||||
interface UnifiedStylingPreviewSurveyProps {
|
||||
survey: TSurvey;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId?: string | null;
|
||||
product: TProduct;
|
||||
}
|
||||
|
||||
let surveyNameTemp;
|
||||
|
||||
const previewParentContainerVariant: Variants = {
|
||||
expanded: {
|
||||
position: "fixed",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
backdropFilter: "blur(15px)",
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 1040,
|
||||
transition: {
|
||||
ease: "easeIn",
|
||||
duration: 0.001,
|
||||
},
|
||||
},
|
||||
shrink: {
|
||||
display: "none",
|
||||
position: "fixed",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.0)",
|
||||
backdropFilter: "blur(0px)",
|
||||
transition: {
|
||||
duration: 0,
|
||||
},
|
||||
zIndex: -1,
|
||||
},
|
||||
};
|
||||
|
||||
export default function UnifiedStylingPreviewSurvey({
|
||||
setActiveQuestionId,
|
||||
activeQuestionId,
|
||||
survey,
|
||||
product,
|
||||
}: UnifiedStylingPreviewSurveyProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
|
||||
const [previewPosition, setPreviewPosition] = useState("relative");
|
||||
const ContentRef = useRef<HTMLDivElement | null>(null);
|
||||
const [shrink, setshrink] = useState(false);
|
||||
|
||||
const [previewType, setPreviewType] = useState<"link" | "web">("link");
|
||||
|
||||
const { productOverwrites } = survey || {};
|
||||
|
||||
const previewScreenVariants: Variants = {
|
||||
expanded: {
|
||||
right: "5%",
|
||||
bottom: "10%",
|
||||
top: "12%",
|
||||
width: "40%",
|
||||
position: "fixed",
|
||||
height: "80%",
|
||||
zIndex: 1050,
|
||||
boxShadow: "0px 4px 5px 4px rgba(169, 169, 169, 0.25)",
|
||||
transition: {
|
||||
ease: "easeInOut",
|
||||
duration: shrink ? 0.3 : 0,
|
||||
},
|
||||
},
|
||||
expanded_with_fixed_positioning: {
|
||||
zIndex: 1050,
|
||||
position: "fixed",
|
||||
top: "5%",
|
||||
right: "5%",
|
||||
bottom: "10%",
|
||||
width: "90%",
|
||||
height: "90%",
|
||||
transition: {
|
||||
ease: "easeOut",
|
||||
duration: 0.4,
|
||||
},
|
||||
},
|
||||
shrink: {
|
||||
display: "relative",
|
||||
width: ["83.33%"],
|
||||
height: ["95%"],
|
||||
},
|
||||
};
|
||||
|
||||
const { placement: surveyPlacement } = productOverwrites || {};
|
||||
|
||||
const placement = surveyPlacement || product.placement;
|
||||
|
||||
const highlightBorderColor = product.styling?.highlightBorderColor?.light;
|
||||
|
||||
const styling: TStyling = useMemo(() => {
|
||||
if (product.styling) {
|
||||
return product.styling;
|
||||
}
|
||||
|
||||
return {
|
||||
unifiedStyling: true,
|
||||
allowStyleOverwrite: true,
|
||||
brandColor: {
|
||||
light: product.brandColor || "#64748b",
|
||||
},
|
||||
};
|
||||
}, [product.brandColor, product.styling]);
|
||||
|
||||
// this useEffect is fo 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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewType === "web") {
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
}, [previewType]);
|
||||
|
||||
function resetQuestionProgress() {
|
||||
setActiveQuestionId(survey?.questions[0]?.id);
|
||||
}
|
||||
|
||||
const onFileUpload = async (file: File) => file.name;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-items-center">
|
||||
<motion.div
|
||||
variants={previewParentContainerVariant}
|
||||
className="fixed hidden h-[95%] w-5/6"
|
||||
animate={isFullScreenPreview ? "expanded" : "shrink"}
|
||||
/>
|
||||
<motion.div
|
||||
layout
|
||||
variants={previewScreenVariants}
|
||||
animate={
|
||||
isFullScreenPreview
|
||||
? previewPosition === "relative"
|
||||
? "expanded"
|
||||
: "expanded_with_fixed_positioning"
|
||||
: "shrink"
|
||||
}
|
||||
className="relative flex h-[95] max-h-[95%] w-5/6 items-center justify-center rounded-lg border border-slate-300 bg-slate-200">
|
||||
<div className="flex h-full w-5/6 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>
|
||||
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
|
||||
</div>
|
||||
<div className="ml-4 flex w-full justify-between font-mono text-sm text-slate-400">
|
||||
<p>{previewType === "web" ? "Your web app" : "Preview"}</p>
|
||||
|
||||
<div className="flex items-center">
|
||||
{isFullScreenPreview ? (
|
||||
<ArrowsPointingInIcon
|
||||
className="mr-2 h-4 w-4 cursor-pointer"
|
||||
onClick={() => {
|
||||
setshrink(true);
|
||||
setPreviewPosition("relative");
|
||||
setTimeout(() => setIsFullScreenPreview(false), 300);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ArrowsPointingOutIcon
|
||||
className="mr-2 h-4 w-4 cursor-pointer"
|
||||
onClick={() => {
|
||||
setshrink(false);
|
||||
setIsFullScreenPreview(true);
|
||||
setTimeout(() => setPreviewPosition("fixed"), 300);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{previewType === "web" ? (
|
||||
<Modal
|
||||
isOpen
|
||||
placement={placement}
|
||||
highlightBorderColor={highlightBorderColor}
|
||||
previewMode="desktop"
|
||||
borderRadius={styling.roundness ?? 12}>
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
styling={styling}
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
<MediaBackground survey={survey} ContentRef={ContentRef} isEditorView>
|
||||
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
responseCount={42}
|
||||
styling={styling}
|
||||
/>
|
||||
</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">
|
||||
<div
|
||||
className={`${previewType === "link" ? "rounded-full bg-slate-200" : ""} cursor-pointer px-3 py-1`}
|
||||
onClick={() => setPreviewType("link")}>
|
||||
Link survey
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${previewType === "web" ? "rounded-full bg-slate-200" : ""} cursor-pointer px-3 py-1`}
|
||||
onClick={() => setPreviewType("web")}>
|
||||
App survey
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResetProgressButton({ resetQuestionProgress }) {
|
||||
return (
|
||||
<Button
|
||||
variant="minimal"
|
||||
className="py-0.2 mr-2 bg-white px-2 font-sans text-sm text-slate-500"
|
||||
onClick={resetQuestionProgress}>
|
||||
Restart
|
||||
<ArrowPathRoundedSquareIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -10,12 +10,14 @@ export default function Modal({
|
||||
placement,
|
||||
previewMode,
|
||||
highlightBorderColor,
|
||||
borderRadius,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
isOpen: boolean;
|
||||
placement: TPlacement;
|
||||
previewMode: string;
|
||||
highlightBorderColor: string | null | undefined;
|
||||
borderRadius?: number;
|
||||
}) {
|
||||
const [show, setShow] = useState(false);
|
||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -102,7 +104,14 @@ export default function Modal({
|
||||
<div aria-live="assertive" className="relative h-full w-full overflow-hidden bg-slate-300">
|
||||
<div
|
||||
ref={modalRef}
|
||||
style={{ ...highlightBorderColorStyle, ...scalingClasses }}
|
||||
style={{
|
||||
...highlightBorderColorStyle,
|
||||
...scalingClasses,
|
||||
|
||||
...(borderRadius && {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
}),
|
||||
}}
|
||||
className={cn(
|
||||
"no-scrollbar pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out ",
|
||||
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full",
|
||||
|
||||
@@ -280,7 +280,7 @@ export function Survey({
|
||||
return (
|
||||
<>
|
||||
<AutoCloseWrapper survey={survey} onClose={onClose}>
|
||||
<div className="no-scrollbar flex h-full w-full flex-col justify-between rounded-lg bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
|
||||
<div className="no-scrollbar rounded-custom flex h-full w-full flex-col justify-between bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
|
||||
<div ref={contentRef} className={cn(loadingElement ? "animate-pulse opacity-60" : "", "my-auto")}>
|
||||
{survey.questions.length === 0 && !survey.welcomeCard.enabled && !survey.thankYouCard.enabled ? (
|
||||
// Handle the case when there are no questions and both welcome and thank you cards are disabled
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function MultipleChoiceSingleQuestion({
|
||||
<legend className="sr-only">Options</legend>
|
||||
|
||||
<div
|
||||
className="bg-survey-bg relative max-h-[33vh] space-y-2 overflow-y-auto rounded-md py-0.5 pr-2"
|
||||
className="bg-survey-bg rounded-custom relative max-h-[33vh] space-y-2 overflow-y-auto py-0.5 pr-2"
|
||||
role="radiogroup">
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
|
||||
@@ -41,8 +41,6 @@ export default function OpenTextQuestion({
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
|
||||
const handleInputChange = (inputValue: string) => {
|
||||
// const isValidInput = validateInput(inputValue, question.inputType, question.required);
|
||||
// setIsValid(isValidInput);
|
||||
onChange({ [question.id]: inputValue });
|
||||
};
|
||||
|
||||
@@ -69,11 +67,9 @@ export default function OpenTextQuestion({
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
// if ( validateInput(value as string, question.inputType, question.required)) {
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
onSubmit({ [question.id]: value, inputType: question.inputType }, updatedttc);
|
||||
// }
|
||||
}}
|
||||
className="w-full">
|
||||
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
|
||||
@@ -113,7 +109,7 @@ export default function OpenTextQuestion({
|
||||
handleInputResize(e);
|
||||
}}
|
||||
autoFocus={autoFocus}
|
||||
className="border-border bg-survey-bg text-subheading focus:border-border-highlight block w-full rounded-md border p-2 shadow-sm focus:ring-0 sm:text-sm"
|
||||
className="border-border bg-survey-bg text-subheading focus:border-border-highlight rounded-custom block w-full border p-2 shadow-sm focus:ring-0 sm:text-sm"
|
||||
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
|
||||
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,8 @@ declare global {
|
||||
|
||||
export const renderSurveyInline = (props: SurveyInlineProps) => {
|
||||
addStylesToDom();
|
||||
addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
|
||||
// addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
|
||||
addCustomThemeToDom({ styling: props.styling });
|
||||
|
||||
const element = document.getElementById(props.containerId);
|
||||
if (!element) {
|
||||
@@ -27,7 +28,8 @@ export const renderSurveyInline = (props: SurveyInlineProps) => {
|
||||
|
||||
export const renderSurveyModal = (props: SurveyModalProps) => {
|
||||
addStylesToDom();
|
||||
addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
|
||||
// addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
|
||||
addCustomThemeToDom({ styling: props.styling });
|
||||
|
||||
// add container element to DOM
|
||||
const element = document.createElement("div");
|
||||
|
||||
@@ -2,6 +2,8 @@ import { isLight } from "@/lib/utils";
|
||||
import global from "@/styles/global.css?inline";
|
||||
import preflight from "@/styles/preflight.css?inline";
|
||||
|
||||
import { TStyling } from "@formbricks/types/styling";
|
||||
|
||||
import editorCss from "../../../ui/Editor/stylesEditorFrontend.css?inline";
|
||||
|
||||
export const addStylesToDom = () => {
|
||||
@@ -13,15 +15,19 @@ export const addStylesToDom = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const addCustomThemeToDom = ({ brandColor }: { brandColor: string }) => {
|
||||
export const addCustomThemeToDom = ({ styling }: { styling: TStyling }) => {
|
||||
if (document.getElementById("formbricks__css") === null) return;
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.id = "formbricks__css__custom";
|
||||
styleElement.innerHTML = `
|
||||
:root {
|
||||
--fb-brand-color: ${brandColor};
|
||||
${isLight(brandColor) ? "--fb-brand-text-color: black;" : "--fb-brand-text-color: white;"}
|
||||
--fb-brand-color: ${styling.brandColor?.light};
|
||||
${isLight(styling.brandColor?.light ?? "") ? "--fb-brand-text-color: black;" : "--fb-brand-text-color: white;"}
|
||||
--fb-heading-color: ${styling.questionColor?.light};
|
||||
--fb-border-color: ${styling.inputBorderColor?.light};
|
||||
--fb-survey-background-color: ${styling.cardBackgroundColor?.light};
|
||||
--fb-border-radius: ${styling.roundness}px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
@@ -81,4 +81,6 @@ p.fb-editor-paragraph {
|
||||
--fb-rating-selected: black;
|
||||
--fb-close-btn-color: var(--slate-500);
|
||||
--fb-close-btn-color-hover: var(--slate-700);
|
||||
|
||||
--fb-border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ module.exports = {
|
||||
"close-button": "var(--fb-close-btn-color)",
|
||||
"close-button-focus": "var(--fb-close-btn-hover-color)",
|
||||
},
|
||||
borderRadius: {
|
||||
custom: "var(--fb-border-radius)",
|
||||
},
|
||||
zIndex: {
|
||||
999999: "999999",
|
||||
},
|
||||
|
||||
@@ -30,6 +30,6 @@ export interface SurveyInlineProps extends SurveyBaseProps {
|
||||
export interface SurveyModalProps extends SurveyBaseProps {
|
||||
clickOutside: boolean;
|
||||
darkOverlay: boolean;
|
||||
highlightBorderColor: string | null;
|
||||
// highlightBorderColor: string | null;
|
||||
placement: "bottomLeft" | "bottomRight" | "topLeft" | "topRight" | "center";
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const ZStylingColor = z.object({
|
||||
light: ZColor,
|
||||
dark: ZColor.optional(),
|
||||
});
|
||||
export type TStylingColor = z.infer<typeof ZStylingColor>;
|
||||
|
||||
export const ZCardArrangementOptions = z.enum(["casual", "straight", "simple"]);
|
||||
export type TCardArrangementOptions = z.infer<typeof ZCardArrangementOptions>;
|
||||
|
||||
Reference in New Issue
Block a user