mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-29 09:50:10 -06:00
Compare commits
15 Commits
fix/radix-
...
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}
|
membershipRole={currentUserMembership?.role}
|
||||||
/>
|
/>
|
||||||
<div className="w-full md:ml-64">
|
<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>{children}</div>
|
||||||
</div>
|
</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 { EditFormbricksBranding } from "./components/EditBranding";
|
||||||
import { EditHighlightBorder } from "./components/EditHighlightBorder";
|
import { EditHighlightBorder } from "./components/EditHighlightBorder";
|
||||||
import { EditPlacement } from "./components/EditPlacement";
|
import { EditPlacement } from "./components/EditPlacement";
|
||||||
|
import UnifiedStyling from "./components/UnifiedStyling";
|
||||||
|
|
||||||
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
|
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
|
||||||
const [session, team, product] = await Promise.all([
|
const [session, team, product] = await Promise.all([
|
||||||
@@ -50,19 +51,24 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SettingsTitle title="Look & Feel" />
|
<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
|
<EditBrandColor
|
||||||
product={product}
|
product={product}
|
||||||
isBrandColorDisabled={isBrandColorEditDisabled}
|
isBrandColorDisabled={isBrandColorEditDisabled}
|
||||||
environmentId={params.environmentId}
|
environmentId={params.environmentId}
|
||||||
/>
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard> */}
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title="In-app Survey Placement"
|
title="In-app Survey Placement"
|
||||||
description="Change where surveys will be shown in your web app.">
|
description="Change where surveys will be shown in your web app.">
|
||||||
<EditPlacement product={product} environmentId={params.environmentId} />
|
<EditPlacement product={product} environmentId={params.environmentId} />
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
<SettingsCard
|
{/* <SettingsCard
|
||||||
noPadding
|
noPadding
|
||||||
title="Highlight Border"
|
title="Highlight Border"
|
||||||
description="Make sure your users notice the survey you display">
|
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}
|
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
||||||
environmentId={params.environmentId}
|
environmentId={params.environmentId}
|
||||||
/>
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard> */}
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title="Formbricks Branding"
|
title="Formbricks Branding"
|
||||||
description="We love your support but understand if you toggle it off.">
|
description="We love your support but understand if you toggle it off.">
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ export default function Modal({
|
|||||||
placement,
|
placement,
|
||||||
previewMode,
|
previewMode,
|
||||||
highlightBorderColor,
|
highlightBorderColor,
|
||||||
|
borderRadius,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
placement: TPlacement;
|
placement: TPlacement;
|
||||||
previewMode: string;
|
previewMode: string;
|
||||||
highlightBorderColor: string | null | undefined;
|
highlightBorderColor: string | null | undefined;
|
||||||
|
borderRadius?: number;
|
||||||
}) {
|
}) {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
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 aria-live="assertive" className="relative h-full w-full overflow-hidden bg-slate-300">
|
||||||
<div
|
<div
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
style={{ ...highlightBorderColorStyle, ...scalingClasses }}
|
style={{
|
||||||
|
...highlightBorderColorStyle,
|
||||||
|
...scalingClasses,
|
||||||
|
|
||||||
|
...(borderRadius && {
|
||||||
|
borderRadius: `${borderRadius}px`,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
className={cn(
|
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 ",
|
"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",
|
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full",
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import {
|
|||||||
DevicePhoneMobileIcon,
|
DevicePhoneMobileIcon,
|
||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
import { Variants, motion } from "framer-motion";
|
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 { TEnvironment } from "@formbricks/types/environment";
|
||||||
import type { TProduct } from "@formbricks/types/product";
|
import type { TProduct } from "@formbricks/types/product";
|
||||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||||
|
import { TStyling } from "@formbricks/types/styling";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
import { TSurvey } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||||
@@ -120,10 +121,24 @@ export default function PreviewSurvey({
|
|||||||
placement: surveyPlacement,
|
placement: surveyPlacement,
|
||||||
} = productOverwrites || {};
|
} = productOverwrites || {};
|
||||||
|
|
||||||
const brandColor = surveyBrandColor || product.brandColor;
|
// const brandColor = surveyBrandColor || product.brandColor;
|
||||||
const placement = surveyPlacement || product.placement;
|
const placement = surveyPlacement || product.placement;
|
||||||
const highlightBorderColor = surveyHighlightBorderColor || product.highlightBorderColor;
|
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(() => {
|
useEffect(() => {
|
||||||
// close modal if there are no questions left
|
// close modal if there are no questions left
|
||||||
if (survey.type === "web" && !survey.thankYouCard.enabled) {
|
if (survey.type === "web" && !survey.thankYouCard.enabled) {
|
||||||
@@ -207,12 +222,13 @@ export default function PreviewSurvey({
|
|||||||
previewMode="mobile">
|
previewMode="mobile">
|
||||||
<SurveyInline
|
<SurveyInline
|
||||||
survey={survey}
|
survey={survey}
|
||||||
brandColor={brandColor}
|
// brandColor={brandColor}
|
||||||
activeQuestionId={activeQuestionId || undefined}
|
activeQuestionId={activeQuestionId || undefined}
|
||||||
isBrandingEnabled={product.inAppSurveyBranding}
|
isBrandingEnabled={product.inAppSurveyBranding}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
isRedirectDisabled={true}
|
isRedirectDisabled={true}
|
||||||
onFileUpload={onFileUpload}
|
onFileUpload={onFileUpload}
|
||||||
|
styling={styling}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</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">
|
<div className="no-scrollbar z-10 w-full max-w-md overflow-y-auto rounded-lg border border-transparent">
|
||||||
<SurveyInline
|
<SurveyInline
|
||||||
survey={survey}
|
survey={survey}
|
||||||
brandColor={brandColor}
|
// brandColor={brandColor}
|
||||||
activeQuestionId={activeQuestionId || undefined}
|
activeQuestionId={activeQuestionId || undefined}
|
||||||
isBrandingEnabled={product.linkSurveyBranding}
|
isBrandingEnabled={product.linkSurveyBranding}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
onFileUpload={onFileUpload}
|
onFileUpload={onFileUpload}
|
||||||
responseCount={42}
|
responseCount={42}
|
||||||
|
styling={styling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,12 +294,13 @@ export default function PreviewSurvey({
|
|||||||
previewMode="desktop">
|
previewMode="desktop">
|
||||||
<SurveyInline
|
<SurveyInline
|
||||||
survey={survey}
|
survey={survey}
|
||||||
brandColor={brandColor}
|
// brandColor={brandColor}
|
||||||
activeQuestionId={activeQuestionId || undefined}
|
activeQuestionId={activeQuestionId || undefined}
|
||||||
isBrandingEnabled={product.inAppSurveyBranding}
|
isBrandingEnabled={product.inAppSurveyBranding}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
isRedirectDisabled={true}
|
isRedirectDisabled={true}
|
||||||
onFileUpload={onFileUpload}
|
onFileUpload={onFileUpload}
|
||||||
|
styling={styling}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : (
|
) : (
|
||||||
@@ -290,13 +308,14 @@ export default function PreviewSurvey({
|
|||||||
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||||
<SurveyInline
|
<SurveyInline
|
||||||
survey={survey}
|
survey={survey}
|
||||||
brandColor={brandColor}
|
// brandColor={brandColor}
|
||||||
activeQuestionId={activeQuestionId || undefined}
|
activeQuestionId={activeQuestionId || undefined}
|
||||||
isBrandingEnabled={product.linkSurveyBranding}
|
isBrandingEnabled={product.linkSurveyBranding}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
isRedirectDisabled={true}
|
isRedirectDisabled={true}
|
||||||
onFileUpload={onFileUpload}
|
onFileUpload={onFileUpload}
|
||||||
responseCount={42}
|
responseCount={42}
|
||||||
|
styling={styling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MediaBackground>
|
</MediaBackground>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { TActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
|
|||||||
import { TIntegrationConfig } from "@formbricks/types/integration";
|
import { TIntegrationConfig } from "@formbricks/types/integration";
|
||||||
import { TResponseData, TResponseMeta, TResponsePersonAttributes } from "@formbricks/types/responses";
|
import { TResponseData, TResponseMeta, TResponsePersonAttributes } from "@formbricks/types/responses";
|
||||||
import { TBaseFilters } from "@formbricks/types/segment";
|
import { TBaseFilters } from "@formbricks/types/segment";
|
||||||
|
import { TStyling } from "@formbricks/types/styling";
|
||||||
import {
|
import {
|
||||||
TSurveyClosedMessage,
|
TSurveyClosedMessage,
|
||||||
TSurveyHiddenFields,
|
TSurveyHiddenFields,
|
||||||
@@ -38,5 +39,6 @@ declare global {
|
|||||||
export type UserNotificationSettings = TUserNotificationSettings;
|
export type UserNotificationSettings = TUserNotificationSettings;
|
||||||
export type SegmentFilter = TBaseFilters;
|
export type SegmentFilter = TBaseFilters;
|
||||||
export type SurveyInlineTriggers = TSurveyInlineTriggers;
|
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",
|
"lint": "eslint ./src --fix",
|
||||||
"post-install": "pnpm generate",
|
"post-install": "pnpm generate",
|
||||||
"predev": "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": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.10.2",
|
"@prisma/client": "^5.10.2",
|
||||||
|
|||||||
@@ -419,6 +419,9 @@ model Product {
|
|||||||
environments Environment[]
|
environments Environment[]
|
||||||
brandColor String @default("#64748b")
|
brandColor String @default("#64748b")
|
||||||
highlightBorderColor String?
|
highlightBorderColor String?
|
||||||
|
/// @zod.custom(imports.ZStyling)
|
||||||
|
/// [Styling]
|
||||||
|
styling Json? @default("{\"unifiedStyling\":true,\"allowStyleOverwrite\":true,\"brandColor\":{\"light\":\"#64748b\"}}")
|
||||||
recontactDays Int @default(7)
|
recontactDays Int @default(7)
|
||||||
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
|
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
|
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
export { ZStyling } from "@formbricks/types/styling";
|
||||||
|
|
||||||
export const ZActionProperties = z.record(z.string());
|
export const ZActionProperties = z.record(z.string());
|
||||||
export { ZActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
|
export { ZActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
|
||||||
export { ZIntegrationConfig } from "@formbricks/types/integration";
|
export { ZIntegrationConfig } from "@formbricks/types/integration";
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import { logoutPerson, resetPerson, setPersonAttribute, setPersonUserId } from "
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
formbricksSurveys: {
|
formbricksSurveys: {
|
||||||
renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
|
// renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
|
||||||
renderSurveyModal: (props: SurveyModalProps & { 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,
|
clickOutsideClose: true,
|
||||||
darkOverlay: true,
|
darkOverlay: true,
|
||||||
environments: true,
|
environments: true,
|
||||||
|
styling: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProducts = async (teamId: string, page?: number): Promise<TProduct[]> => {
|
export const getProducts = async (teamId: string, page?: number): Promise<TProduct[]> => {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function Survey({
|
|||||||
getSetIsResponseSendingFinished,
|
getSetIsResponseSendingFinished,
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
responseCount,
|
responseCount,
|
||||||
|
styling,
|
||||||
}: SurveyBaseProps) {
|
}: SurveyBaseProps) {
|
||||||
const [questionId, setQuestionId] = useState(
|
const [questionId, setQuestionId] = useState(
|
||||||
activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
|
activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
|
||||||
@@ -279,7 +280,7 @@ export function Survey({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AutoCloseWrapper survey={survey} onClose={onClose}>
|
<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")}>
|
<div ref={contentRef} className={cn(loadingElement ? "animate-pulse opacity-60" : "", "my-auto")}>
|
||||||
{survey.questions.length === 0 && !survey.welcomeCard.enabled && !survey.thankYouCard.enabled ? (
|
{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
|
// 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,
|
onRetry,
|
||||||
isRedirectDisabled = false,
|
isRedirectDisabled = false,
|
||||||
responseCount,
|
responseCount,
|
||||||
|
styling,
|
||||||
}: SurveyModalProps) {
|
}: SurveyModalProps) {
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ export function SurveyModal({
|
|||||||
onFileUpload={onFileUpload}
|
onFileUpload={onFileUpload}
|
||||||
isRedirectDisabled={isRedirectDisabled}
|
isRedirectDisabled={isRedirectDisabled}
|
||||||
responseCount={responseCount}
|
responseCount={responseCount}
|
||||||
|
styling={styling}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function MultipleChoiceSingleQuestion({
|
|||||||
<legend className="sr-only">Options</legend>
|
<legend className="sr-only">Options</legend>
|
||||||
|
|
||||||
<div
|
<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">
|
role="radiogroup">
|
||||||
{questionChoices.map((choice, idx) => (
|
{questionChoices.map((choice, idx) => (
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ export default function OpenTextQuestion({
|
|||||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||||
|
|
||||||
const handleInputChange = (inputValue: string) => {
|
const handleInputChange = (inputValue: string) => {
|
||||||
// const isValidInput = validateInput(inputValue, question.inputType, question.required);
|
|
||||||
// setIsValid(isValidInput);
|
|
||||||
onChange({ [question.id]: inputValue });
|
onChange({ [question.id]: inputValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,11 +67,9 @@ export default function OpenTextQuestion({
|
|||||||
key={question.id}
|
key={question.id}
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// if ( validateInput(value as string, question.inputType, question.required)) {
|
|
||||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||||
setTtc(updatedttc);
|
setTtc(updatedttc);
|
||||||
onSubmit({ [question.id]: value, inputType: question.inputType }, updatedttc);
|
onSubmit({ [question.id]: value, inputType: question.inputType }, updatedttc);
|
||||||
// }
|
|
||||||
}}
|
}}
|
||||||
className="w-full">
|
className="w-full">
|
||||||
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
|
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
|
||||||
@@ -113,7 +109,7 @@ export default function OpenTextQuestion({
|
|||||||
handleInputResize(e);
|
handleInputResize(e);
|
||||||
}}
|
}}
|
||||||
autoFocus={autoFocus}
|
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 ]+" : ".*"}
|
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
|
||||||
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ import { SurveyInlineProps, SurveyModalProps } from "@formbricks/types/formbrick
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
formbricksSurveys: {
|
formbricksSurveys: {
|
||||||
renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
|
renderSurveyInline: (props: SurveyInlineProps) => void;
|
||||||
renderSurveyModal: (props: SurveyModalProps & { brandColor: string }) => void;
|
renderSurveyModal: (props: SurveyModalProps) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renderSurveyInline = (props: SurveyInlineProps & { brandColor: string }) => {
|
export const renderSurveyInline = (props: SurveyInlineProps) => {
|
||||||
addStylesToDom();
|
addStylesToDom();
|
||||||
addCustomThemeToDom({ brandColor: props.brandColor });
|
// addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
|
||||||
|
addCustomThemeToDom({ styling: props.styling });
|
||||||
|
|
||||||
const element = document.getElementById(props.containerId);
|
const element = document.getElementById(props.containerId);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
@@ -25,9 +26,10 @@ export const renderSurveyInline = (props: SurveyInlineProps & { brandColor: stri
|
|||||||
render(h(SurveyInline, props), element);
|
render(h(SurveyInline, props), element);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderSurveyModal = (props: SurveyModalProps & { brandColor: string }) => {
|
export const renderSurveyModal = (props: SurveyModalProps) => {
|
||||||
addStylesToDom();
|
addStylesToDom();
|
||||||
addCustomThemeToDom({ brandColor: props.brandColor });
|
// addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
|
||||||
|
addCustomThemeToDom({ styling: props.styling });
|
||||||
|
|
||||||
// add container element to DOM
|
// add container element to DOM
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { isLight } from "@/lib/utils";
|
|||||||
import global from "@/styles/global.css?inline";
|
import global from "@/styles/global.css?inline";
|
||||||
import preflight from "@/styles/preflight.css?inline";
|
import preflight from "@/styles/preflight.css?inline";
|
||||||
|
|
||||||
|
import { TStyling } from "@formbricks/types/styling";
|
||||||
|
|
||||||
import editorCss from "../../../ui/Editor/stylesEditorFrontend.css?inline";
|
import editorCss from "../../../ui/Editor/stylesEditorFrontend.css?inline";
|
||||||
|
|
||||||
export const addStylesToDom = () => {
|
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;
|
if (document.getElementById("formbricks__css") === null) return;
|
||||||
|
|
||||||
const styleElement = document.createElement("style");
|
const styleElement = document.createElement("style");
|
||||||
styleElement.id = "formbricks__css__custom";
|
styleElement.id = "formbricks__css__custom";
|
||||||
styleElement.innerHTML = `
|
styleElement.innerHTML = `
|
||||||
:root {
|
:root {
|
||||||
--fb-brand-color: ${brandColor};
|
--fb-brand-color: ${styling.brandColor?.light};
|
||||||
${isLight(brandColor) ? "--fb-brand-text-color: black;" : "--fb-brand-text-color: white;"}
|
${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);
|
document.head.appendChild(styleElement);
|
||||||
|
|||||||
@@ -81,4 +81,6 @@ p.fb-editor-paragraph {
|
|||||||
--fb-rating-selected: black;
|
--fb-rating-selected: black;
|
||||||
--fb-close-btn-color: var(--slate-500);
|
--fb-close-btn-color: var(--slate-500);
|
||||||
--fb-close-btn-color-hover: var(--slate-700);
|
--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": "var(--fb-close-btn-color)",
|
||||||
"close-button-focus": "var(--fb-close-btn-hover-color)",
|
"close-button-focus": "var(--fb-close-btn-hover-color)",
|
||||||
},
|
},
|
||||||
|
borderRadius: {
|
||||||
|
fbBorderRadius: "var(--fb-border-radius)",
|
||||||
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
999999: "999999",
|
999999: "999999",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TResponseData, TResponseUpdate } from "./responses";
|
import { TResponseData, TResponseUpdate } from "./responses";
|
||||||
import { TUploadFileConfig } from "./storage";
|
import { TUploadFileConfig } from "./storage";
|
||||||
|
import { TStyling } from "./styling";
|
||||||
import { TSurvey } from "./surveys";
|
import { TSurvey } from "./surveys";
|
||||||
|
|
||||||
export interface SurveyBaseProps {
|
export interface SurveyBaseProps {
|
||||||
@@ -19,6 +20,7 @@ export interface SurveyBaseProps {
|
|||||||
prefillResponseData?: TResponseData;
|
prefillResponseData?: TResponseData;
|
||||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
||||||
responseCount?: number;
|
responseCount?: number;
|
||||||
|
styling: TStyling;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SurveyInlineProps extends SurveyBaseProps {
|
export interface SurveyInlineProps extends SurveyBaseProps {
|
||||||
@@ -28,6 +30,6 @@ export interface SurveyInlineProps extends SurveyBaseProps {
|
|||||||
export interface SurveyModalProps extends SurveyBaseProps {
|
export interface SurveyModalProps extends SurveyBaseProps {
|
||||||
clickOutside: boolean;
|
clickOutside: boolean;
|
||||||
darkOverlay: boolean;
|
darkOverlay: boolean;
|
||||||
highlightBorderColor: string | null;
|
// highlightBorderColor: string | null;
|
||||||
placement: "bottomLeft" | "bottomRight" | "topLeft" | "topRight" | "center";
|
placement: "bottomLeft" | "bottomRight" | "topLeft" | "topRight" | "center";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { ZColor, ZPlacement } from "./common";
|
import { ZColor, ZPlacement } from "./common";
|
||||||
import { ZEnvironment } from "./environment";
|
import { ZEnvironment } from "./environment";
|
||||||
|
import { ZStyling } from "./styling";
|
||||||
|
|
||||||
export const ZProduct = z.object({
|
export const ZProduct = z.object({
|
||||||
id: z.string().cuid2(),
|
id: z.string().cuid2(),
|
||||||
@@ -11,6 +12,7 @@ export const ZProduct = z.object({
|
|||||||
teamId: z.string(),
|
teamId: z.string(),
|
||||||
brandColor: ZColor,
|
brandColor: ZColor,
|
||||||
highlightBorderColor: ZColor.nullable(),
|
highlightBorderColor: ZColor.nullable(),
|
||||||
|
styling: ZStyling.nullable(),
|
||||||
recontactDays: z.number().int(),
|
recontactDays: z.number().int(),
|
||||||
inAppSurveyBranding: z.boolean(),
|
inAppSurveyBranding: z.boolean(),
|
||||||
linkSurveyBranding: z.boolean(),
|
linkSurveyBranding: z.boolean(),
|
||||||
@@ -34,6 +36,7 @@ export const ZProductUpdateInput = z.object({
|
|||||||
clickOutsideClose: z.boolean().optional(),
|
clickOutsideClose: z.boolean().optional(),
|
||||||
darkOverlay: z.boolean().optional(),
|
darkOverlay: z.boolean().optional(),
|
||||||
environments: z.array(ZEnvironment).optional(),
|
environments: z.array(ZEnvironment).optional(),
|
||||||
|
styling: ZStyling.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProductUpdateInput = z.infer<typeof ZProductUpdateInput>;
|
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";
|
"use client";
|
||||||
|
|
||||||
/* import { persistForm, useForm } from "@/app/lib/forms"; */
|
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||||
|
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import useClickOutside from "@formbricks/lib/useClickOutside";
|
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 (
|
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 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">
|
<div className="flex w-full items-center">
|
||||||
#
|
#
|
||||||
@@ -18,6 +28,7 @@ export const ColorPicker = ({ color, onChange }: { color: string; onChange: (v:
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
id="color"
|
id="color"
|
||||||
aria-label="Primary color"
|
aria-label="Primary color"
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PopoverPicker color={color} onChange={onChange} />
|
<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`;
|
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(), []);
|
const containerId = useMemo(() => createContainerId(), []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderSurveyInline({
|
renderSurveyInline({
|
||||||
@@ -16,7 +16,7 @@ export const SurveyInline = (props: Omit<SurveyInlineProps & { brandColor: strin
|
|||||||
return <div id={containerId} className="h-full w-full" />;
|
return <div id={containerId} className="h-full w-full" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SurveyModal = (props: SurveyModalProps & { brandColor: string }) => {
|
export const SurveyModal = (props: SurveyModalProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderSurveyModal(props);
|
renderSurveyModal(props);
|
||||||
}, [props]);
|
}, [props]);
|
||||||
|
|||||||
Reference in New Issue
Block a user