fix: product settings

This commit is contained in:
pandeymangg
2024-03-04 22:07:13 +05:30
parent 2586a3ba3a
commit 7a1af85141
12 changed files with 298 additions and 22 deletions

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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}
/>

View File

@@ -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");

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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",
},

View File

@@ -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";
}

View File

@@ -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>;