feat: product settings page styling UI and service

This commit is contained in:
pandeymangg
2024-03-04 13:18:39 +05:30
parent d7fc7995bc
commit d5b183155b
6 changed files with 446 additions and 17 deletions

View File

@@ -1,18 +1,139 @@
"use client";
import React, { useState } from "react";
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 { 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 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 UnifiedStyling = ({ product }: UnifiedStylingProps) => {
// const [color, setColor] = useState("#333");
const [color, setColor] = useState(product.styling?.brandColor?.light);
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"
);
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">
@@ -21,18 +142,29 @@ const UnifiedStyling = ({ product }: UnifiedStylingProps) => {
<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 />
<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-500">Set base styles for all surveys below</p>
<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 />
<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-500">
<p className="text-sm text-slate-800">
Activate if you want some surveys to be styled differently
</p>
</div>
@@ -42,47 +174,129 @@ const UnifiedStyling = ({ product }: UnifiedStylingProps) => {
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">Brand color</h3>
<p className="text-sm text-slate-500">Change the text color of the survey questions.</p>
<p className="text-sm text-slate-800">Change the text color of the survey questions.</p>
</div>
<ColorPicker color={color} onChange={setColor} containerClass="my-0" />
<ColorPicker color={brandColor} onChange={setBrandColor} containerClass="my-0" />
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">Question color</h3>
<p className="text-sm text-slate-500">Change the text color of the survey questions.</p>
<p className="text-sm text-slate-800">Change the text color of the survey questions.</p>
</div>
<ColorPicker color={color} onChange={setColor} containerClass="my-0" />
<ColorPicker color={questionColor} onChange={setQuestionColor} containerClass="my-0" />
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">Input color</h3>
<p className="text-sm text-slate-500">Change the text color of the survey questions.</p>
<p className="text-sm text-slate-800">Change the text color of the survey questions.</p>
</div>
<ColorPicker color={color} onChange={setColor} containerClass="my-0" />
<ColorPicker color={inputColor} onChange={setInputColor} containerClass="my-0" />
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">Input border color</h3>
<p className="text-sm text-slate-500">Change the text color of the survey questions.</p>
<p className="text-sm text-slate-800">Change the text color of the survey questions.</p>
</div>
<ColorPicker color={color} onChange={setColor} containerClass="my-0" />
<ColorPicker color={inputBorderColor} onChange={setInputBorderColor} containerClass="my-0" />
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">Card background color</h3>
<p className="text-sm text-slate-500">Change the text color of the survey questions.</p>
<p className="text-sm text-slate-800">Change the text color of the survey questions.</p>
</div>
<ColorPicker color={color} onChange={setColor} containerClass="my-0" />
<ColorPicker
color={cardBackgroundColor}
onChange={setCardBackgroundColor}
containerClass="my-0"
/>
</div>
<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>

View File

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

View File

@@ -8,6 +8,7 @@ export const ZStylingColor = z.object({
});
export const ZCardArrangementOptions = z.enum(["casual", "straight", "simple"]);
export type TCardArrangementOptions = z.infer<typeof ZCardArrangementOptions>;
export const ZCardArrangement = z.object({
linkSurveys: ZCardArrangementOptions,

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

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

View File

@@ -0,0 +1,117 @@
import { ColorPicker } from "../ColorPicker";
import { Switch } from "../Switch";
const colorDefaults = {
brandColor: "#64748b",
questionColor: "#2b2524",
inputColor: "#efefef",
inputBorderColor: "#c0c0c0",
cardBackgroundColor: "#c0c0c0",
highlightBorderColor: "#64748b",
};
const ColorSelectorWithLabel = ({
label,
color,
setColor,
}: {
label: string;
color: string;
setColor: React.Dispatch<React.SetStateAction<string>>;
}) => {
return (
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">{label}</h3>
<ColorPicker color={color} onChange={setColor} />
</div>
);
};
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 &quot;Dark Mode&quot; 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}
/>
<ColorSelectorWithLabel
label="Question color"
color={questionColor ?? colorDefaults.questionColor}
setColor={setQuestionColor}
/>
<ColorSelectorWithLabel
label="Input color"
color={inputColor ?? colorDefaults.inputColor}
setColor={setInputColor}
/>
<ColorSelectorWithLabel
label="Input border color"
color={inputBorderColor ?? colorDefaults.inputBorderColor}
setColor={setInputBorderColor}
/>
<ColorSelectorWithLabel
label="Card background color"
color={cardBackgroundColor ?? colorDefaults.cardBackgroundColor}
setColor={setCardBackgroundColor}
/>
<ColorSelectorWithLabel
label="Highlight border color"
color={highlightBorderColor ?? colorDefaults.highlightBorderColor}
setColor={setHighlighBorderColor}
/>
</div>
)}
</div>
);
};
export default DarkModeColors;