mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
Compare commits
15 Commits
v2.5.3
...
ReviewBot/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3c846a30c | ||
|
|
7a1af85141 | ||
|
|
2586a3ba3a | ||
|
|
77408bf0b0 | ||
|
|
d5b183155b | ||
|
|
d7fc7995bc | ||
|
|
a9d8239a25 | ||
|
|
71bdb5095a | ||
|
|
b84e322eee | ||
|
|
08ccb954f3 | ||
|
|
38c6cb01df | ||
|
|
2c13121487 | ||
|
|
73f1d09dc8 | ||
|
|
1f884a408c | ||
|
|
ed2253dcfc |
@@ -43,7 +43,7 @@ export default async function SettingsLayout({ children, params }) {
|
||||
membershipRole={currentUserMembership?.role}
|
||||
/>
|
||||
<div className="w-full md:ml-64">
|
||||
<div className="max-w-4xl px-20 pb-6 pt-14 md:pt-6">
|
||||
<div className="px-20 pb-6 pt-14 md:pt-6">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
"use client";
|
||||
|
||||
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 { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { Slider } from "@formbricks/ui/Slider";
|
||||
import CardArrangement from "@formbricks/ui/Styling/CardArrangement";
|
||||
import ColorSelectorWithLabel from "@formbricks/ui/Styling/ColorSelectorWithLabel";
|
||||
import DarkModeColors from "@formbricks/ui/Styling/DarkModeColors";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
import { updateProductAction } from "../actions";
|
||||
|
||||
type UnifiedStylingProps = {
|
||||
product: TProduct;
|
||||
};
|
||||
|
||||
const colorDefaults = {
|
||||
brandColor: "#64748b",
|
||||
questionColor: "#2b2524",
|
||||
inputColor: "#efefef",
|
||||
inputBorderColor: "#c0c0c0",
|
||||
cardBackgroundColor: "#c0c0c0",
|
||||
highlighBorderColor: "#64748b",
|
||||
};
|
||||
|
||||
const previewSurvey = {
|
||||
id: "cltcppyqk00006uothzb3ybh0",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Product Market Fit (Superhuman)",
|
||||
type: "link",
|
||||
environmentId: "cltcf8i2n00099wlx7cu12zi6",
|
||||
createdBy: "cltcf8i1c00009wlx3sk1ryss",
|
||||
status: "draft",
|
||||
welcomeCard: {
|
||||
html: "Thanks for providing your feedback - let's go!",
|
||||
enabled: false,
|
||||
headline: "Welcome!",
|
||||
timeToFinish: true,
|
||||
showResponseCount: false,
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
id: "uvnrhtngswxlibktglanh45f",
|
||||
type: "openText",
|
||||
headline: "This is a preview survey",
|
||||
required: true,
|
||||
inputType: "text",
|
||||
subheader: "Click through it to check the look and feel of the surveying experience.",
|
||||
longAnswer: true,
|
||||
placeholder: "Type your answer here...",
|
||||
},
|
||||
{
|
||||
id: "swfnndfht0ubsu9uh17tjcej",
|
||||
type: "rating",
|
||||
range: 5,
|
||||
scale: "star",
|
||||
headline: "How would you rate My Product",
|
||||
required: true,
|
||||
subheader: "Don't worry, be honest.",
|
||||
lowerLabel: "Not good",
|
||||
upperLabel: "Very good",
|
||||
},
|
||||
{
|
||||
id: "je70a714xjdxc70jhxgv5web",
|
||||
type: "multipleChoiceSingle",
|
||||
choices: [
|
||||
{
|
||||
id: "vx9q4mlr6ffaw35m99bselwm",
|
||||
label: "Eat the cake 🍰",
|
||||
},
|
||||
{
|
||||
id: "ynj051qawxd4dszxkbvahoe5",
|
||||
label: "Have the cake 🎂",
|
||||
},
|
||||
],
|
||||
headline: "What do you do?",
|
||||
required: true,
|
||||
subheader: "Can't do both.",
|
||||
shuffleOption: "none",
|
||||
},
|
||||
],
|
||||
thankYouCard: {
|
||||
enabled: true,
|
||||
headline: "Thank you!",
|
||||
subheader: "We appreciate your feedback.",
|
||||
buttonLink: "https://formbricks.com/signup",
|
||||
buttonLabel: "Create your own Survey",
|
||||
},
|
||||
hiddenFields: {
|
||||
enabled: true,
|
||||
fieldIds: [],
|
||||
},
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
verifyEmail: null,
|
||||
redirectUrl: null,
|
||||
productOverwrites: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: {
|
||||
enabled: false,
|
||||
isEncrypted: true,
|
||||
},
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
triggers: [],
|
||||
inlineTriggers: null,
|
||||
segment: null,
|
||||
};
|
||||
|
||||
const UnifiedStyling = ({ product }: UnifiedStylingProps) => {
|
||||
const router = useRouter();
|
||||
const [unifiedStyling, setUnifiedStyling] = useState(product.styling?.unifiedStyling ?? false);
|
||||
const [allowStyleOverwrite, setAllowStyleOverwrite] = useState(
|
||||
product.styling?.allowStyleOverwrite ?? false
|
||||
);
|
||||
const [brandColor, setBrandColor] = useState(
|
||||
product.styling?.brandColor?.light ?? colorDefaults.brandColor
|
||||
);
|
||||
const [questionColor, setQuestionColor] = useState(
|
||||
product.styling?.questionColor?.light ?? colorDefaults.questionColor
|
||||
);
|
||||
const [inputColor, setInputColor] = useState(
|
||||
product.styling?.inputColor?.light ?? colorDefaults.inputColor
|
||||
);
|
||||
const [inputBorderColor, setInputBorderColor] = useState(
|
||||
product.styling?.inputBorderColor?.light ?? colorDefaults.inputBorderColor
|
||||
);
|
||||
const [cardBackgroundColor, setCardBackgroundColor] = useState(
|
||||
product.styling?.cardBackgroundColor?.light ?? colorDefaults.cardBackgroundColor
|
||||
);
|
||||
|
||||
// highlight border
|
||||
const [allowHighlightBorder, setAllowHighlightBorder] = useState(
|
||||
!!product.styling?.highlightBorderColor?.light ?? false
|
||||
);
|
||||
const [highlightBorderColor, setHighlightBorderColor] = useState(
|
||||
product.styling?.highlightBorderColor?.light ?? colorDefaults.highlighBorderColor
|
||||
);
|
||||
|
||||
const [isDarkMode, setIsDarkMode] = useState(product.styling?.isDarkModeEnabled ?? false);
|
||||
|
||||
const [brandColorDark, setBrandColorDark] = useState(product.styling?.brandColor?.dark);
|
||||
|
||||
const [questionColorDark, setQuestionColorDark] = useState(product.styling?.questionColor?.dark);
|
||||
|
||||
const [inputColorDark, setInputColorDark] = useState(product.styling?.inputColor?.dark);
|
||||
|
||||
const [inputBorderColorDark, setInputBorderColorDark] = useState(product.styling?.inputBorderColor?.dark);
|
||||
|
||||
const [cardBackgroundColorDark, setCardBackgroundColorDark] = useState(
|
||||
product.styling?.cardBackgroundColor?.dark
|
||||
);
|
||||
|
||||
const [highlightBorderColorDark, setHighlightBorderColorDark] = useState(
|
||||
product.styling?.highlightBorderColor?.dark
|
||||
);
|
||||
|
||||
const [roundness, setRoundness] = useState(product.styling?.roundness ?? 8);
|
||||
|
||||
const [linkSurveysCardArrangement, setLinkSurveysCardArrangement] = useState(
|
||||
product.styling?.cardArrangement?.linkSurveys ?? "casual"
|
||||
);
|
||||
const [inAppSurveysCardArrangement, setInAppSurveysCardArrangement] = useState(
|
||||
product.styling?.cardArrangement?.inAppSurveys ?? "casual"
|
||||
);
|
||||
|
||||
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveQuestionId(previewSurvey.questions[0].id);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!unifiedStyling) {
|
||||
setAllowStyleOverwrite(false);
|
||||
}
|
||||
}, [unifiedStyling]);
|
||||
|
||||
const onSave = async () => {
|
||||
await updateProductAction(product.id, {
|
||||
styling: {
|
||||
unifiedStyling,
|
||||
allowStyleOverwrite,
|
||||
brandColor: {
|
||||
light: brandColor,
|
||||
dark: brandColorDark,
|
||||
},
|
||||
questionColor: {
|
||||
light: questionColor,
|
||||
dark: questionColorDark,
|
||||
},
|
||||
inputColor: {
|
||||
light: inputColor,
|
||||
dark: inputColorDark,
|
||||
},
|
||||
inputBorderColor: {
|
||||
light: inputBorderColor,
|
||||
dark: inputBorderColorDark,
|
||||
},
|
||||
cardBackgroundColor: {
|
||||
light: cardBackgroundColor,
|
||||
dark: cardBackgroundColorDark,
|
||||
},
|
||||
highlightBorderColor: allowHighlightBorder
|
||||
? {
|
||||
light: highlightBorderColor,
|
||||
dark: highlightBorderColorDark,
|
||||
}
|
||||
: undefined,
|
||||
isDarkModeEnabled: isDarkMode,
|
||||
roundness,
|
||||
cardArrangement: {
|
||||
linkSurveys: linkSurveysCardArrangement,
|
||||
inAppSurveys: inAppSurveysCardArrangement,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("Styling updated successfully.");
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{/* Styling settings */}
|
||||
<div className="w-1/2 pr-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-slate-50 p-4">
|
||||
<div className="flex items-center gap-6">
|
||||
<Switch
|
||||
checked={unifiedStyling}
|
||||
onCheckedChange={(value) => {
|
||||
setUnifiedStyling(value);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold">Enable unified styling</h3>
|
||||
<p className="text-sm text-slate-800">Set base styles for all surveys below</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<Switch
|
||||
checked={allowStyleOverwrite}
|
||||
onCheckedChange={(value) => {
|
||||
setAllowStyleOverwrite(value);
|
||||
}}
|
||||
disabled={!unifiedStyling}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold">Allow overwriting styles</h3>
|
||||
<p className="text-sm text-slate-800">
|
||||
Activate if you want some surveys to be styled differently
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Brand color"
|
||||
color={brandColor}
|
||||
setColor={setBrandColor}
|
||||
description="Change the text color of the survey questions."
|
||||
disabled
|
||||
/>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Question color"
|
||||
color={questionColor}
|
||||
setColor={setQuestionColor}
|
||||
description="Change the text color of the survey questions."
|
||||
/>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Input color"
|
||||
color={inputColor}
|
||||
setColor={setInputColor}
|
||||
description="Change the text color of the survey questions."
|
||||
/>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Input border color"
|
||||
color={inputBorderColor}
|
||||
setColor={setInputBorderColor}
|
||||
description="Change the text color of the survey questions."
|
||||
/>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Card background color"
|
||||
color={cardBackgroundColor}
|
||||
setColor={setCardBackgroundColor}
|
||||
description="Change the text color of the survey questions."
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-6">
|
||||
<Switch
|
||||
checked={allowHighlightBorder}
|
||||
onCheckedChange={(value) => {
|
||||
setAllowHighlightBorder(value);
|
||||
}}
|
||||
disabled={!unifiedStyling}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold">Add highlight border</h3>
|
||||
<p className="text-sm text-slate-800">Add on outer border to your survey card</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{allowHighlightBorder && (
|
||||
<ColorPicker
|
||||
color={highlightBorderColor}
|
||||
onChange={setHighlightBorderColor}
|
||||
containerClass="my-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DarkModeColors
|
||||
isDarkMode={isDarkMode}
|
||||
setIsDarkMode={setIsDarkMode}
|
||||
brandColor={brandColorDark}
|
||||
cardBackgroundColor={cardBackgroundColorDark}
|
||||
highlightBorderColor={highlightBorderColorDark}
|
||||
inputBorderColor={inputBorderColorDark}
|
||||
inputColor={inputColorDark}
|
||||
questionColor={questionColorDark}
|
||||
setBrandColor={setBrandColorDark}
|
||||
setCardBackgroundColor={setCardBackgroundColorDark}
|
||||
setHighlighBorderColor={setHighlightBorderColorDark}
|
||||
setInputBorderColor={setInputBorderColorDark}
|
||||
setInputColor={setInputColorDark}
|
||||
setQuestionColor={setQuestionColorDark}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold text-slate-900">Roundness</h3>
|
||||
<p className="text-sm text-slate-800">Change the border radius of the card and the inputs.</p>
|
||||
</div>
|
||||
|
||||
<Slider
|
||||
value={[roundness]}
|
||||
max={16}
|
||||
onValueChange={(value) => setRoundness(value[0])}
|
||||
disabled={!unifiedStyling}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardArrangement
|
||||
activeCardArrangement={linkSurveysCardArrangement}
|
||||
surveyType="link"
|
||||
setActiveCardArrangement={setLinkSurveysCardArrangement}
|
||||
/>
|
||||
|
||||
<CardArrangement
|
||||
activeCardArrangement={inAppSurveysCardArrangement}
|
||||
surveyType="web"
|
||||
setActiveCardArrangement={setInAppSurveysCardArrangement}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center justify-end gap-2">
|
||||
<Button variant="minimal" className="flex items-center gap-2">
|
||||
Reset
|
||||
<RotateCcwIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button variant="darkCTA" onClick={onSave}>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Survey Preview */}
|
||||
|
||||
<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}
|
||||
product={product}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedStyling;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { EditBrandColor } from "./components/EditBrandColor";
|
||||
import { EditFormbricksBranding } from "./components/EditBranding";
|
||||
import { EditHighlightBorder } from "./components/EditHighlightBorder";
|
||||
import { EditPlacement } from "./components/EditPlacement";
|
||||
import UnifiedStyling from "./components/UnifiedStyling";
|
||||
|
||||
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
|
||||
const [session, team, product] = await Promise.all([
|
||||
@@ -50,19 +51,24 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
||||
return (
|
||||
<div>
|
||||
<SettingsTitle title="Look & Feel" />
|
||||
<SettingsCard title="Brand Color" description="Match the surveys with your user interface.">
|
||||
<SettingsCard
|
||||
title="Unified Styling"
|
||||
description="Set styling for ALL surveys in this project. You can still overwrite these styles in the survey editor.">
|
||||
<UnifiedStyling product={product} />
|
||||
</SettingsCard>
|
||||
{/* <SettingsCard title="Brand Color" description="Match the surveys with your user interface.">
|
||||
<EditBrandColor
|
||||
product={product}
|
||||
isBrandColorDisabled={isBrandColorEditDisabled}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
</SettingsCard>
|
||||
</SettingsCard> */}
|
||||
<SettingsCard
|
||||
title="In-app Survey Placement"
|
||||
description="Change where surveys will be shown in your web app.">
|
||||
<EditPlacement product={product} environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
{/* <SettingsCard
|
||||
noPadding
|
||||
title="Highlight Border"
|
||||
description="Make sure your users notice the survey you display">
|
||||
@@ -71,7 +77,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
||||
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
</SettingsCard>
|
||||
</SettingsCard> */}
|
||||
<SettingsCard
|
||||
title="Formbricks Branding"
|
||||
description="We love your support but understand if you toggle it off.">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
DevicePhoneMobileIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Variants, motion } from "framer-motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import type { TProduct } from "@formbricks/types/product";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TStyling } from "@formbricks/types/styling";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
@@ -120,10 +121,24 @@ export default function PreviewSurvey({
|
||||
placement: surveyPlacement,
|
||||
} = productOverwrites || {};
|
||||
|
||||
const brandColor = surveyBrandColor || product.brandColor;
|
||||
// const brandColor = surveyBrandColor || product.brandColor;
|
||||
const placement = surveyPlacement || product.placement;
|
||||
const highlightBorderColor = surveyHighlightBorderColor || product.highlightBorderColor;
|
||||
|
||||
const styling: TStyling = useMemo(() => {
|
||||
if (product.styling) {
|
||||
return product.styling;
|
||||
}
|
||||
|
||||
return {
|
||||
unifiedStyling: true,
|
||||
allowStyleOverwrite: true,
|
||||
brandColor: {
|
||||
light: product.brandColor || "#64748b",
|
||||
},
|
||||
};
|
||||
}, [product.brandColor, product.styling]);
|
||||
|
||||
useEffect(() => {
|
||||
// close modal if there are no questions left
|
||||
if (survey.type === "web" && !survey.thankYouCard.enabled) {
|
||||
@@ -207,12 +222,13 @@ export default function PreviewSurvey({
|
||||
previewMode="mobile">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
// brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
styling={styling}
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
@@ -220,12 +236,13 @@ export default function PreviewSurvey({
|
||||
<div className="no-scrollbar z-10 w-full max-w-md overflow-y-auto rounded-lg border border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
// brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
onFileUpload={onFileUpload}
|
||||
responseCount={42}
|
||||
styling={styling}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,12 +294,13 @@ export default function PreviewSurvey({
|
||||
previewMode="desktop">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
// brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
styling={styling}
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
@@ -290,13 +308,14 @@ export default function PreviewSurvey({
|
||||
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
// brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
responseCount={42}
|
||||
styling={styling}
|
||||
/>
|
||||
</div>
|
||||
</MediaBackground>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
|
||||
import { TIntegrationConfig } from "@formbricks/types/integration";
|
||||
import { TResponseData, TResponseMeta, TResponsePersonAttributes } from "@formbricks/types/responses";
|
||||
import { TBaseFilters } from "@formbricks/types/segment";
|
||||
import { TStyling } from "@formbricks/types/styling";
|
||||
import {
|
||||
TSurveyClosedMessage,
|
||||
TSurveyHiddenFields,
|
||||
@@ -38,5 +39,6 @@ declare global {
|
||||
export type UserNotificationSettings = TUserNotificationSettings;
|
||||
export type SegmentFilter = TBaseFilters;
|
||||
export type SurveyInlineTriggers = TSurveyInlineTriggers;
|
||||
export type Styling = TStyling;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { TStyling } from "@formbricks/types/styling";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// product table with brand color and the highlight border color (if available)
|
||||
// styling object needs to be created for each product
|
||||
const products = await tx.product.findMany({});
|
||||
|
||||
if (!products) {
|
||||
// something went wrong, could not find any products
|
||||
return;
|
||||
}
|
||||
|
||||
if (products.length) {
|
||||
for (const product of products) {
|
||||
if (product.styling !== null) {
|
||||
// styling object already exists for this product
|
||||
continue;
|
||||
}
|
||||
|
||||
const styling: TStyling = {
|
||||
unifiedStyling: true,
|
||||
allowStyleOverwrite: true,
|
||||
brandColor: {
|
||||
light: product.brandColor,
|
||||
},
|
||||
...(product.highlightBorderColor && {
|
||||
highlightBorderColor: {
|
||||
light: product.highlightBorderColor,
|
||||
dark: product.highlightBorderColor,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
await tx.product.update({
|
||||
where: {
|
||||
id: product.id,
|
||||
},
|
||||
data: {
|
||||
styling,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => await prisma.$disconnect());
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ADD COLUMN "styling" JSONB DEFAULT '{"unifiedStyling":true,"allowStyleOverwrite":true,"brandColor":{"light":"#64748b"}}';
|
||||
@@ -23,7 +23,8 @@
|
||||
"lint": "eslint ./src --fix",
|
||||
"post-install": "pnpm generate",
|
||||
"predev": "pnpm generate",
|
||||
"data-migration:v1.6": "ts-node ./migrations/20240207041922_advanced_targeting/data-migration.ts"
|
||||
"data-migration:v1.6": "ts-node ./migrations/20240207041922_advanced_targeting/data-migration.ts",
|
||||
"data-migration:styling": "ts-node ./migrations/20240229062232_adds_styling_column_to_product_model/data-migration.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.10.2",
|
||||
|
||||
@@ -419,6 +419,9 @@ model Product {
|
||||
environments Environment[]
|
||||
brandColor String @default("#64748b")
|
||||
highlightBorderColor String?
|
||||
/// @zod.custom(imports.ZStyling)
|
||||
/// [Styling]
|
||||
styling Json? @default("{\"unifiedStyling\":true,\"allowStyleOverwrite\":true,\"brandColor\":{\"light\":\"#64748b\"}}")
|
||||
recontactDays Int @default(7)
|
||||
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
|
||||
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import z from "zod";
|
||||
|
||||
export { ZStyling } from "@formbricks/types/styling";
|
||||
|
||||
export const ZActionProperties = z.record(z.string());
|
||||
export { ZActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
|
||||
export { ZIntegrationConfig } from "@formbricks/types/integration";
|
||||
|
||||
@@ -13,8 +13,11 @@ import { logoutPerson, resetPerson, setPersonAttribute, setPersonUserId } from "
|
||||
declare global {
|
||||
interface Window {
|
||||
formbricksSurveys: {
|
||||
renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
|
||||
renderSurveyModal: (props: SurveyModalProps & { brandColor: string }) => void;
|
||||
// renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
|
||||
// renderSurveyModal: (props: SurveyModalProps & { brandColor: string }) => void;
|
||||
|
||||
renderSurveyInline: (props: SurveyInlineProps) => void;
|
||||
renderSurveyModal: (props: SurveyModalProps) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const selectProduct = {
|
||||
clickOutsideClose: true,
|
||||
darkOverlay: true,
|
||||
environments: true,
|
||||
styling: true,
|
||||
};
|
||||
|
||||
export const getProducts = async (teamId: string, page?: number): Promise<TProduct[]> => {
|
||||
|
||||
@@ -32,6 +32,7 @@ export function Survey({
|
||||
getSetIsResponseSendingFinished,
|
||||
onFileUpload,
|
||||
responseCount,
|
||||
styling,
|
||||
}: SurveyBaseProps) {
|
||||
const [questionId, setQuestionId] = useState(
|
||||
activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
|
||||
@@ -279,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
|
||||
|
||||
@@ -24,6 +24,7 @@ export function SurveyModal({
|
||||
onRetry,
|
||||
isRedirectDisabled = false,
|
||||
responseCount,
|
||||
styling,
|
||||
}: SurveyModalProps) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
@@ -67,6 +68,7 @@ export function SurveyModal({
|
||||
onFileUpload={onFileUpload}
|
||||
isRedirectDisabled={isRedirectDisabled}
|
||||
responseCount={responseCount}
|
||||
styling={styling}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -8,15 +8,16 @@ import { SurveyInlineProps, SurveyModalProps } from "@formbricks/types/formbrick
|
||||
declare global {
|
||||
interface Window {
|
||||
formbricksSurveys: {
|
||||
renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
|
||||
renderSurveyModal: (props: SurveyModalProps & { brandColor: string }) => void;
|
||||
renderSurveyInline: (props: SurveyInlineProps) => void;
|
||||
renderSurveyModal: (props: SurveyModalProps) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const renderSurveyInline = (props: SurveyInlineProps & { brandColor: string }) => {
|
||||
export const renderSurveyInline = (props: SurveyInlineProps) => {
|
||||
addStylesToDom();
|
||||
addCustomThemeToDom({ brandColor: props.brandColor });
|
||||
// addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
|
||||
addCustomThemeToDom({ styling: props.styling });
|
||||
|
||||
const element = document.getElementById(props.containerId);
|
||||
if (!element) {
|
||||
@@ -25,9 +26,10 @@ export const renderSurveyInline = (props: SurveyInlineProps & { brandColor: stri
|
||||
render(h(SurveyInline, props), element);
|
||||
};
|
||||
|
||||
export const renderSurveyModal = (props: SurveyModalProps & { brandColor: string }) => {
|
||||
export const renderSurveyModal = (props: SurveyModalProps) => {
|
||||
addStylesToDom();
|
||||
addCustomThemeToDom({ brandColor: props.brandColor });
|
||||
// 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: {
|
||||
fbBorderRadius: "var(--fb-border-radius)",
|
||||
},
|
||||
zIndex: {
|
||||
999999: "999999",
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TResponseData, TResponseUpdate } from "./responses";
|
||||
import { TUploadFileConfig } from "./storage";
|
||||
import { TStyling } from "./styling";
|
||||
import { TSurvey } from "./surveys";
|
||||
|
||||
export interface SurveyBaseProps {
|
||||
@@ -19,6 +20,7 @@ export interface SurveyBaseProps {
|
||||
prefillResponseData?: TResponseData;
|
||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
||||
responseCount?: number;
|
||||
styling: TStyling;
|
||||
}
|
||||
|
||||
export interface SurveyInlineProps extends SurveyBaseProps {
|
||||
@@ -28,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";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { ZColor, ZPlacement } from "./common";
|
||||
import { ZEnvironment } from "./environment";
|
||||
import { ZStyling } from "./styling";
|
||||
|
||||
export const ZProduct = z.object({
|
||||
id: z.string().cuid2(),
|
||||
@@ -11,6 +12,7 @@ export const ZProduct = z.object({
|
||||
teamId: z.string(),
|
||||
brandColor: ZColor,
|
||||
highlightBorderColor: ZColor.nullable(),
|
||||
styling: ZStyling.nullable(),
|
||||
recontactDays: z.number().int(),
|
||||
inAppSurveyBranding: z.boolean(),
|
||||
linkSurveyBranding: z.boolean(),
|
||||
@@ -34,6 +36,7 @@ export const ZProductUpdateInput = z.object({
|
||||
clickOutsideClose: z.boolean().optional(),
|
||||
darkOverlay: z.boolean().optional(),
|
||||
environments: z.array(ZEnvironment).optional(),
|
||||
styling: ZStyling.optional(),
|
||||
});
|
||||
|
||||
export type TProductUpdateInput = z.infer<typeof ZProductUpdateInput>;
|
||||
|
||||
33
packages/types/styling.ts
Normal file
33
packages/types/styling.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ZColor } from "./common";
|
||||
|
||||
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>;
|
||||
|
||||
export const ZCardArrangement = z.object({
|
||||
linkSurveys: ZCardArrangementOptions,
|
||||
inAppSurveys: ZCardArrangementOptions,
|
||||
});
|
||||
|
||||
export const ZStyling = z.object({
|
||||
unifiedStyling: z.boolean(),
|
||||
allowStyleOverwrite: z.boolean(),
|
||||
brandColor: ZStylingColor.optional(),
|
||||
questionColor: ZStylingColor.optional(),
|
||||
inputColor: ZStylingColor.optional(),
|
||||
inputBorderColor: ZStylingColor.optional(),
|
||||
cardBackgroundColor: ZStylingColor.optional(),
|
||||
highlightBorderColor: ZStylingColor.optional(),
|
||||
isDarkModeEnabled: z.boolean().optional(),
|
||||
roundness: z.number().optional(),
|
||||
cardArrangement: ZCardArrangement.optional(),
|
||||
});
|
||||
|
||||
export type TStyling = z.infer<typeof ZStyling>;
|
||||
@@ -1,14 +1,24 @@
|
||||
"use client";
|
||||
|
||||
/* import { persistForm, useForm } from "@/app/lib/forms"; */
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import useClickOutside from "@formbricks/lib/useClickOutside";
|
||||
|
||||
export const ColorPicker = ({ color, onChange }: { color: string; onChange: (v: string) => void }) => {
|
||||
export const ColorPicker = ({
|
||||
color,
|
||||
onChange,
|
||||
containerClass,
|
||||
disabled,
|
||||
}: {
|
||||
color: string;
|
||||
onChange: (v: string) => void;
|
||||
containerClass?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="my-2">
|
||||
<div className={cn("my-2", containerClass)}>
|
||||
<div className="flex w-full items-center justify-between space-x-1 rounded-md border border-slate-300 bg-white px-2 text-sm text-slate-400">
|
||||
<div className="flex w-full items-center">
|
||||
#
|
||||
@@ -18,6 +28,7 @@ export const ColorPicker = ({ color, onChange }: { color: string; onChange: (v:
|
||||
onChange={onChange}
|
||||
id="color"
|
||||
aria-label="Primary color"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<PopoverPicker color={color} onChange={onChange} />
|
||||
|
||||
24
packages/ui/Slider/index.tsx
Normal file
24
packages/ui/Slider/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||
{...props}>
|
||||
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-300">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-gray-300" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="border-primary ring-offset-background focus-visible:ring-ring block h-5 w-5 rounded-full border-2 bg-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
72
packages/ui/Styling/CardArrangement.tsx
Normal file
72
packages/ui/Styling/CardArrangement.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
|
||||
import { Button } from "../Button";
|
||||
|
||||
type CardArrangementProps = {
|
||||
surveyType: "link" | "web";
|
||||
activeCardArrangement: TCardArrangementOptions;
|
||||
setActiveCardArrangement: (arrangement: TCardArrangementOptions) => void;
|
||||
};
|
||||
|
||||
const CardArrangement = ({
|
||||
activeCardArrangement,
|
||||
surveyType,
|
||||
setActiveCardArrangement,
|
||||
}: CardArrangementProps) => {
|
||||
const surveyTypeDerived = useMemo(() => {
|
||||
return surveyType == "link" ? "Link" : "In App";
|
||||
}, [surveyType]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold text-slate-900">
|
||||
Card Arrangement for {surveyTypeDerived} Surveys
|
||||
</h3>
|
||||
<p className="text-sm text-slate-800">
|
||||
How funky do you want your cards in {surveyTypeDerived} Surveys
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 rounded-md border border-slate-300 bg-white p-1">
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"flex flex-1 justify-center bg-white text-center",
|
||||
activeCardArrangement === "casual" && "bg-slate-200"
|
||||
)}
|
||||
onClick={() => setActiveCardArrangement("casual")}>
|
||||
Casual
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
onClick={() => setActiveCardArrangement("straight")}
|
||||
className={cn(
|
||||
"flex flex-1 justify-center bg-white text-center",
|
||||
activeCardArrangement === "straight" && "bg-slate-200"
|
||||
)}>
|
||||
Straight
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
onClick={() => setActiveCardArrangement("simple")}
|
||||
className={cn(
|
||||
"flex flex-1 justify-center bg-white text-center",
|
||||
activeCardArrangement === "simple" && "bg-slate-200"
|
||||
)}>
|
||||
Simple
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardArrangement;
|
||||
34
packages/ui/Styling/ColorSelectorWithLabel.tsx
Normal file
34
packages/ui/Styling/ColorSelectorWithLabel.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
|
||||
import { ColorPicker } from "../ColorPicker";
|
||||
|
||||
type ColorSelectorWithLabelProps = {
|
||||
label: string;
|
||||
description?: string;
|
||||
color: string;
|
||||
setColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const ColorSelectorWithLabel = ({
|
||||
color,
|
||||
description = "",
|
||||
label,
|
||||
setColor,
|
||||
className = "",
|
||||
disabled,
|
||||
}: ColorSelectorWithLabelProps) => {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-4", className)}>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold text-slate-900">{label}</h3>
|
||||
{description && <p className="text-sm text-slate-800">{description}</p>}
|
||||
</div>
|
||||
|
||||
<ColorPicker color={color} onChange={setColor} containerClass="my-0" disabled={disabled} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorSelectorWithLabel;
|
||||
105
packages/ui/Styling/DarkModeColors.tsx
Normal file
105
packages/ui/Styling/DarkModeColors.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Switch } from "../Switch";
|
||||
import ColorSelectorWithLabel from "./ColorSelectorWithLabel";
|
||||
|
||||
const colorDefaults = {
|
||||
brandColor: "#64748b",
|
||||
questionColor: "#2b2524",
|
||||
inputColor: "#efefef",
|
||||
inputBorderColor: "#c0c0c0",
|
||||
cardBackgroundColor: "#c0c0c0",
|
||||
highlightBorderColor: "#64748b",
|
||||
};
|
||||
type DarModeColorProps = {
|
||||
isDarkMode: boolean;
|
||||
setIsDarkMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
brandColor?: string;
|
||||
setBrandColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
questionColor?: string;
|
||||
setQuestionColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
inputColor?: string;
|
||||
setInputColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
inputBorderColor?: string;
|
||||
setInputBorderColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
cardBackgroundColor?: string;
|
||||
setCardBackgroundColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
highlightBorderColor?: string;
|
||||
setHighlighBorderColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
const DarkModeColors = ({
|
||||
isDarkMode,
|
||||
setIsDarkMode,
|
||||
brandColor,
|
||||
cardBackgroundColor,
|
||||
highlightBorderColor,
|
||||
inputBorderColor,
|
||||
inputColor,
|
||||
questionColor,
|
||||
setBrandColor,
|
||||
setCardBackgroundColor,
|
||||
setHighlighBorderColor,
|
||||
setInputBorderColor,
|
||||
setInputColor,
|
||||
setQuestionColor,
|
||||
}: DarModeColorProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-slate-50 p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
onCheckedChange={(value) => {
|
||||
setIsDarkMode(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold text-slate-900">Add "Dark Mode" Colors</h3>
|
||||
<p className="text-sm text-slate-800">Your app has a dark mode? Set a different set of colors.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDarkMode && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ColorSelectorWithLabel
|
||||
label="Brand color"
|
||||
color={brandColor ?? colorDefaults.brandColor}
|
||||
setColor={setBrandColor}
|
||||
className="gap-2"
|
||||
/>
|
||||
<ColorSelectorWithLabel
|
||||
label="Question color"
|
||||
color={questionColor ?? colorDefaults.questionColor}
|
||||
setColor={setQuestionColor}
|
||||
className="gap-2"
|
||||
/>
|
||||
<ColorSelectorWithLabel
|
||||
label="Input color"
|
||||
color={inputColor ?? colorDefaults.inputColor}
|
||||
setColor={setInputColor}
|
||||
className="gap-2"
|
||||
/>
|
||||
<ColorSelectorWithLabel
|
||||
label="Input border color"
|
||||
color={inputBorderColor ?? colorDefaults.inputBorderColor}
|
||||
setColor={setInputBorderColor}
|
||||
className="gap-2"
|
||||
/>
|
||||
<ColorSelectorWithLabel
|
||||
label="Card background color"
|
||||
color={cardBackgroundColor ?? colorDefaults.cardBackgroundColor}
|
||||
setColor={setCardBackgroundColor}
|
||||
className="gap-2"
|
||||
/>
|
||||
<ColorSelectorWithLabel
|
||||
label="Highlight border color"
|
||||
color={highlightBorderColor ?? colorDefaults.highlightBorderColor}
|
||||
setColor={setHighlighBorderColor}
|
||||
className="gap-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DarkModeColors;
|
||||
@@ -5,7 +5,7 @@ import { SurveyInlineProps, SurveyModalProps } from "@formbricks/types/formbrick
|
||||
|
||||
const createContainerId = () => `formbricks-survey-container`;
|
||||
|
||||
export const SurveyInline = (props: Omit<SurveyInlineProps & { brandColor: string }, "containerId">) => {
|
||||
export const SurveyInline = (props: Omit<SurveyInlineProps, "containerId">) => {
|
||||
const containerId = useMemo(() => createContainerId(), []);
|
||||
useEffect(() => {
|
||||
renderSurveyInline({
|
||||
@@ -16,7 +16,7 @@ export const SurveyInline = (props: Omit<SurveyInlineProps & { brandColor: strin
|
||||
return <div id={containerId} className="h-full w-full" />;
|
||||
};
|
||||
|
||||
export const SurveyModal = (props: SurveyModalProps & { brandColor: string }) => {
|
||||
export const SurveyModal = (props: SurveyModalProps) => {
|
||||
useEffect(() => {
|
||||
renderSurveyModal(props);
|
||||
}, [props]);
|
||||
|
||||
Reference in New Issue
Block a user