Move Look & Feel Settings to React Server Components (#672)

* feat: migrate look and feel to serverside component with loading screen

* fix: use existing product type instead of creating a custom type

* fix: make improvements as Matti suggested

* change attributes order in updateProduct function

* run pnpm format

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Shubham Palriwala
2023-08-11 20:34:31 +05:30
committed by GitHub
parent 09436c78fc
commit c6686209be
11 changed files with 480 additions and 354 deletions

View File

@@ -0,0 +1,41 @@
"use client";
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
import { Button, ColorPicker, Label } from "@formbricks/ui";
import { useState } from "react";
import toast from "react-hot-toast";
import { updateProductAction } from "./actions";
interface EditBrandColorProps {
product: TProduct;
}
export function EditBrandColor({ product }: EditBrandColorProps) {
const [color, setColor] = useState(product.brandColor);
const [updatingColor, setUpdatingColor] = useState(false);
const handleUpdateBrandColor = async () => {
try {
setUpdatingColor(true);
let inputProduct: Partial<TProductUpdateInput> = {
brandColor: color,
};
await updateProductAction(product.id, inputProduct);
toast.success("Brand color updated successfully.");
} catch (error) {
toast.error(`Error: ${error.message}`);
} finally {
setUpdatingColor(false);
}
};
return (
<div className="w-full max-w-sm items-center">
<Label htmlFor="brandcolor">Color (HEX)</Label>
<ColorPicker color={color} onChange={setColor} />
<Button variant="darkCTA" className="mt-4" loading={updatingColor} onClick={handleUpdateBrandColor}>
Save
</Button>
</div>
);
}

View File

@@ -0,0 +1,95 @@
"use client";
import { cn } from "@formbricks/lib/cn";
import { Button, ColorPicker, Label, Switch } from "@formbricks/ui";
import { useState } from "react";
import toast from "react-hot-toast";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
import { updateProductAction } from "./actions";
interface EditHighlightBorderProps {
product: TProduct;
}
export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => {
const [showHighlightBorder, setShowHighlightBorder] = useState(product.highlightBorderColor ? true : false);
const [color, setColor] = useState<string | null>(product.highlightBorderColor || DEFAULT_BRAND_COLOR);
const [updatingBorder, setUpdatingBorder] = useState(false);
const handleUpdateHighlightBorder = async () => {
try {
setUpdatingBorder(true);
let inputProduct: Partial<TProductUpdateInput> = {
highlightBorderColor: color,
};
await updateProductAction(product.id, inputProduct);
toast.success("Border color updated successfully.");
} catch (error) {
toast.error(`Error: ${error.message}`);
} finally {
setUpdatingBorder(false);
}
};
const handleSwitch = (checked: boolean) => {
if (checked) {
if (!color) {
setColor(DEFAULT_BRAND_COLOR);
setShowHighlightBorder(true);
} else {
setShowHighlightBorder(true);
}
} else {
setShowHighlightBorder(false);
setColor(null);
}
};
return (
<div className="flex min-h-full w-full">
<div className="flex w-1/2 flex-col px-6 py-5">
<div className="mb-6 flex items-center space-x-2">
<Switch id="highlightBorder" checked={showHighlightBorder} onCheckedChange={handleSwitch} />
<h2 className="text-sm font-medium text-slate-800">Show highlight border</h2>
</div>
{showHighlightBorder && color ? (
<>
<Label htmlFor="brandcolor">Color (HEX)</Label>
<ColorPicker color={color} onChange={setColor} />
</>
) : null}
<Button
variant="darkCTA"
className="mt-4 flex max-w-[80px] items-center justify-center"
loading={updatingBorder}
onClick={handleUpdateHighlightBorder}>
Save
</Button>
</div>
<div className="flex w-1/2 flex-col items-center justify-center gap-4 bg-slate-200 px-6 py-5">
<h3 className="text-slate-500">Preview</h3>
<div
className={cn("flex flex-col gap-4 rounded-lg border-2 bg-white p-5")}
{...(showHighlightBorder &&
color && {
style: {
borderColor: color,
},
})}>
<h3 className="text-sm font-semibold text-slate-800">How easy was it for you to do this?</h3>
<div className="flex rounded-2xl border border-slate-400">
{[1, 2, 3, 4, 5].map((num) => (
<div key={num} className="border-r border-slate-400 px-6 py-5 last:border-r-0">
<span className="text-sm font-medium">{num}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,117 @@
"use client";
import { cn } from "@formbricks/lib/cn";
import { Button, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
import { useState } from "react";
import toast from "react-hot-toast";
import { getPlacementStyle } from "@/lib/preview";
import { PlacementType } from "@formbricks/types/js";
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
import { updateProductAction } from "./actions";
const placements = [
{ name: "Bottom Right", value: "bottomRight", disabled: false },
{ name: "Top Right", value: "topRight", disabled: false },
{ name: "Top Left", value: "topLeft", disabled: false },
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
{ name: "Centered Modal", value: "center", disabled: false },
];
interface EditPlacementProps {
product: TProduct;
}
export function EditPlacement({ product }: EditPlacementProps) {
const [currentPlacement, setCurrentPlacement] = useState<PlacementType>(product.placement);
const [overlay, setOverlay] = useState(product.darkOverlay ? "darkOverlay" : "lightOverlay");
const [clickOutside, setClickOutside] = useState(product.clickOutsideClose ? "allow" : "disallow");
const [updatingPlacement, setUpdatingPlacement] = useState(false);
const handleUpdatePlacement = async () => {
try {
setUpdatingPlacement(true);
let inputProduct: Partial<TProductUpdateInput> = {
placement: currentPlacement,
darkOverlay: overlay === "darkOverlay",
clickOutsideClose: clickOutside === "allow",
};
await updateProductAction(product.id, inputProduct);
toast.success("Placement updated successfully.");
} catch (error) {
toast.error(`Error: ${error.message}`);
} finally {
setUpdatingPlacement(false);
}
};
return (
<div className="w-full items-center">
<div className="flex">
<RadioGroup onValueChange={(e) => setCurrentPlacement(e as PlacementType)} value={currentPlacement}>
{placements.map((placement) => (
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id={placement.value} value={placement.value} disabled={placement.disabled} />
<Label
htmlFor={placement.value}
className={cn(placement.disabled ? "cursor-not-allowed text-slate-500" : "text-slate-900")}>
{placement.name}
</Label>
</div>
))}
</RadioGroup>
<div className="relative ml-8 h-40 w-full rounded bg-slate-200">
<div
className={cn(
"absolute h-16 w-16 rounded bg-slate-700",
getPlacementStyle(currentPlacement)
)}></div>
</div>
</div>
{currentPlacement === "center" && (
<>
<div className="mt-6 space-y-2">
<Label className="font-semibold">Centered modal overlay color</Label>
<RadioGroup onValueChange={(e) => setOverlay(e)} value={overlay} className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="lightOverlay" value="lightOverlay" />
<Label htmlFor="lightOverlay" className="text-slate-900">
Light Overlay
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="darkOverlay" value="darkOverlay" />
<Label htmlFor="darkOverlay" className="text-slate-900">
Dark Overlay
</Label>
</div>
</RadioGroup>
</div>
<div className="mt-6 space-y-2">
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
<RadioGroup
onValueChange={(e) => setClickOutside(e)}
value={clickOutside}
className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="disallow" value="disallow" />
<Label htmlFor="disallow" className="text-slate-900">
Don&apos;t Allow
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="allow" value="allow" />
<Label htmlFor="allow" className="text-slate-900">
Allow
</Label>
</div>
</RadioGroup>
</div>
</>
)}
<Button variant="darkCTA" className="mt-4" loading={updatingPlacement} onClick={handleUpdatePlacement}>
Save
</Button>
</div>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import { updateProductAction } from "./actions";
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
import { Label, Switch } from "@formbricks/ui";
import { useState } from "react";
import toast from "react-hot-toast";
interface EditSignatureProps {
product: TProduct;
}
export function EditFormbricksSignature({ product }: EditSignatureProps) {
const [formbricksSignature, setFormbricksSignature] = useState(product.formbricksSignature);
const [updatingSignature, setUpdatingSignature] = useState(false);
const toggleSignature = async () => {
try {
setUpdatingSignature(true);
const newSignatureState = !formbricksSignature;
setFormbricksSignature(newSignatureState);
let inputProduct: Partial<TProductUpdateInput> = {
formbricksSignature: newSignatureState,
};
await updateProductAction(product.id, inputProduct);
toast.success(
newSignatureState ? "Formbricks signature will be shown." : "Formbricks signature will now be hidden."
);
} catch (error) {
toast.error(`Error: ${error.message}`);
} finally {
setUpdatingSignature(false);
}
};
return (
<div className="w-full items-center">
<div className="flex items-center space-x-2">
<Switch
id="signature"
checked={formbricksSignature}
onCheckedChange={toggleSignature}
disabled={updatingSignature}
/>
<Label htmlFor="signature">Show &apos;Powered by Formbricks&apos; Signature</Label>
</div>
</div>
);
}

View File

@@ -0,0 +1,8 @@
"use server";
import { updateProduct } from "@formbricks/lib/services/product";
import { TProductUpdateInput } from "@formbricks/types/v1/product";
export async function updateProductAction(productId: string, inputProduct: Partial<TProductUpdateInput>) {
return await updateProduct(productId, inputProduct);
}

View File

@@ -1,339 +0,0 @@
"use client";
import { cn } from "@formbricks/lib/cn";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useEnvironment } from "@/lib/environments/environments";
import { useProductMutation } from "@/lib/products/mutateProducts";
import { useProduct } from "@/lib/products/products";
import {
Button,
ColorPicker,
ErrorComponent,
Label,
RadioGroup,
RadioGroupItem,
Switch,
} from "@formbricks/ui";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { getPlacementStyle } from "@/lib/preview";
import { PlacementType } from "@formbricks/types/js";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
export function EditBrandColor({ environmentId }) {
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
const { triggerProductMutate, isMutatingProduct } = useProductMutation(environmentId);
const [color, setColor] = useState("");
useEffect(() => {
if (product) setColor(product.brandColor);
}, [product]);
if (isLoadingProduct) {
return <LoadingSpinner />;
}
if (isErrorProduct) {
return <div>Error</div>;
}
return (
<div className="w-full max-w-sm items-center">
<Label htmlFor="brandcolor">Color (HEX)</Label>
<ColorPicker color={color} onChange={setColor} />
<Button
type="submit"
variant="darkCTA"
className="mt-4"
loading={isMutatingProduct}
onClick={() => {
triggerProductMutate({ brandColor: color })
.then(() => {
toast.success("Brand color updated successfully.");
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
});
}}>
Save
</Button>
</div>
);
}
export function EditPlacement({ environmentId }) {
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
const { triggerProductMutate, isMutatingProduct } = useProductMutation(environmentId);
const [currentPlacement, setCurrentPlacement] = useState<PlacementType>("bottomRight");
const [overlay, setOverlay] = useState("");
const [clickOutside, setClickOutside] = useState("");
useEffect(() => {
if (product) {
setCurrentPlacement(product.placement);
setOverlay(product.darkOverlay ? "darkOverlay" : "lightOverlay");
setClickOutside(product.clickOutsideClose ? "allow" : "disallow");
}
}, [product]);
if (isLoadingProduct) {
return <LoadingSpinner />;
}
if (isErrorProduct) {
return <ErrorComponent />;
}
const placements = [
{ name: "Bottom Right", value: "bottomRight", disabled: false },
{ name: "Top Right", value: "topRight", disabled: false },
{ name: "Top Left", value: "topLeft", disabled: false },
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
{ name: "Centered Modal", value: "center", disabled: false },
];
return (
<div className="w-full items-center">
<div className="flex">
<RadioGroup onValueChange={(e) => setCurrentPlacement(e as PlacementType)} value={currentPlacement}>
{placements.map((placement) => (
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id={placement.value} value={placement.value} disabled={placement.disabled} />
<Label
htmlFor={placement.value}
className={cn(placement.disabled ? "cursor-not-allowed text-slate-500" : "text-slate-900")}>
{placement.name}
</Label>
</div>
))}
</RadioGroup>
<div className="relative ml-8 h-40 w-full rounded bg-slate-200">
<div
className={cn(
"absolute h-16 w-16 rounded bg-slate-700",
getPlacementStyle(currentPlacement)
)}></div>
</div>
</div>
{currentPlacement === "center" && (
<>
<div className="mt-6 space-y-2">
<Label className="font-semibold">Centered modal overlay color</Label>
<RadioGroup onValueChange={(e) => setOverlay(e)} value={overlay} className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="lightOverlay" value="lightOverlay" />
<Label htmlFor="lightOverlay" className="text-slate-900">
Light Overlay
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="darkOverlay" value="darkOverlay" />
<Label htmlFor="darkOverlay" className="text-slate-900">
Dark Overlay
</Label>
</div>
</RadioGroup>
</div>
<div className="mt-6 space-y-2">
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
<RadioGroup
onValueChange={(e) => setClickOutside(e)}
value={clickOutside}
className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="disallow" value="disallow" />
<Label htmlFor="disallow" className="text-slate-900">
Don&apos;t Allow
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="allow" value="allow" />
<Label htmlFor="allow" className="text-slate-900">
Allow
</Label>
</div>
</RadioGroup>
</div>
</>
)}
<Button
type="submit"
variant="darkCTA"
className="mt-4"
loading={isMutatingProduct}
onClick={() => {
triggerProductMutate({
placement: currentPlacement,
darkOverlay: overlay === "darkOverlay",
clickOutsideClose: clickOutside === "allow",
})
.then(() => {
toast.success("Placement updated successfully.");
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
});
}}>
Save
</Button>
</div>
);
}
export const EditHighlightBorder: React.FC<{ environmentId: string }> = ({ environmentId }) => {
const { product, isLoadingProduct, isErrorProduct, mutateProduct } = useProduct(environmentId);
const { triggerProductMutate, isMutatingProduct } = useProductMutation(environmentId);
const [showHighlightBorder, setShowHighlightBorder] = useState(false);
const [color, setColor] = useState<string | null>(DEFAULT_BRAND_COLOR);
// Sync product state with local state
// not a good pattern, we should find a better way to do this
useEffect(() => {
if (product) {
setShowHighlightBorder(product.highlightBorderColor ? true : false);
setColor(product.highlightBorderColor);
}
}, [product]);
const handleSave = () => {
triggerProductMutate(
{ highlightBorderColor: color },
{
onSuccess: () => {
toast.success("Settings updated successfully.");
// refetch product to update data
mutateProduct();
},
onError: () => {
toast.error("Something went wrong!");
},
}
);
};
const handleSwitch = (checked: boolean) => {
if (checked) {
if (!color) {
setColor(DEFAULT_BRAND_COLOR);
setShowHighlightBorder(true);
} else {
setShowHighlightBorder(true);
}
} else {
setShowHighlightBorder(false);
setColor(null);
}
};
if (isLoadingProduct) {
return <LoadingSpinner />;
}
if (isErrorProduct) {
return <div>Error</div>;
}
return (
<div className="flex min-h-full w-full">
<div className="flex w-1/2 flex-col px-6 py-5">
<div className="mb-6 flex items-center space-x-2">
<Switch id="highlightBorder" checked={showHighlightBorder} onCheckedChange={handleSwitch} />
<h2 className="text-sm font-medium text-slate-800">Show highlight border</h2>
</div>
{showHighlightBorder && color ? (
<>
<Label htmlFor="brandcolor">Color (HEX)</Label>
<ColorPicker color={color} onChange={setColor} />
</>
) : null}
<Button
type="submit"
variant="darkCTA"
className="mt-4 flex max-w-[80px] items-center justify-center"
loading={isMutatingProduct}
onClick={() => {
handleSave();
}}>
Save
</Button>
</div>
<div className="flex w-1/2 flex-col items-center justify-center gap-4 bg-slate-200 px-6 py-5">
<h3 className="text-slate-500">Preview</h3>
<div
className={cn("flex flex-col gap-4 rounded-lg border-2 bg-white p-5")}
{...(showHighlightBorder &&
color && {
style: {
borderColor: color,
},
})}>
<h3 className="text-sm font-semibold text-slate-800">How easy was it for you to do this?</h3>
<div className="flex rounded-2xl border border-slate-400">
{[1, 2, 3, 4, 5].map((num) => (
<div className="border-r border-slate-400 px-6 py-5 last:border-r-0">
<span className="text-sm font-medium">{num}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export function EditFormbricksSignature({ environmentId }) {
const { isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId);
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
const { triggerProductMutate, isMutatingProduct } = useProductMutation(environmentId);
const [formbricksSignature, setFormbricksSignature] = useState(false);
useEffect(() => {
if (product) {
setFormbricksSignature(product.formbricksSignature);
}
}, [product]);
const toggleSignature = () => {
const newSignatureState = !formbricksSignature;
setFormbricksSignature(newSignatureState);
triggerProductMutate({ formbricksSignature: newSignatureState })
.then(() => {
toast.success(newSignatureState ? "Formbricks signature shown." : "Formbricks signature hidden.");
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
});
};
if (isLoadingEnvironment || isLoadingProduct) {
return <LoadingSpinner />;
}
if (isErrorEnvironment || isErrorProduct) {
return <ErrorComponent />;
}
if (formbricksSignature !== null) {
return (
<div className="w-full items-center">
<div className="flex items-center space-x-2">
<Switch
id="signature"
checked={formbricksSignature}
onCheckedChange={toggleSignature}
disabled={isMutatingProduct}
/>
<Label htmlFor="signature">Show &apos;Powered by Formbricks&apos; Signature</Label>
</div>
</div>
);
}
return null;
}

View File

@@ -0,0 +1,114 @@
import SettingsCard from "@/app/(app)/environments/[environmentId]/settings/SettingsCard";
import SettingsTitle from "@/app/(app)/environments/[environmentId]/settings/SettingsTitle";
import { cn } from "@formbricks/lib/cn";
import { Button, Label, RadioGroup, RadioGroupItem, Switch } from "@formbricks/ui";
const placements = [
{ name: "Bottom Right", value: "bottomRight", disabled: false },
{ name: "Top Right", value: "topRight", disabled: false },
{ name: "Top Left", value: "topLeft", disabled: false },
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
{ name: "Centered Modal", value: "center", disabled: false },
];
export default function Loading() {
return (
<div>
<SettingsTitle title="Look & Feel" />
<SettingsCard title="Brand Color" description="Match the surveys with your user interface.">
<div className="w-full max-w-sm items-center">
<Label htmlFor="brandcolor">Color (HEX)</Label>
<div className="my-2">
<div className="flex w-full items-center justify-between space-x-1 rounded-md border border-slate-300 px-2 text-sm text-slate-400">
<div className="ml-2 mr-2 h-10 w-32 border-0 bg-transparent text-slate-500 outline-none focus:border-none"></div>
</div>
</div>
<Button
variant="darkCTA"
className="pointer-events-none mt-4 animate-pulse cursor-not-allowed select-none bg-gray-200">
Loading
</Button>
</div>
</SettingsCard>
<SettingsCard
title="In-app Survey Placement"
description="Change where surveys will be shown in your web app.">
<div className="w-full items-center">
<div className="flex cursor-not-allowed select-none">
<RadioGroup>
{placements.map((placement) => (
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap ">
<RadioGroupItem
className="cursor-not-allowed select-none"
id={placement.value}
value={placement.value}
disabled={placement.disabled}
/>
<Label
htmlFor={placement.value}
className={cn(
placement.disabled ? "cursor-not-allowed text-slate-500" : "text-slate-900"
)}>
{placement.name}
</Label>
</div>
))}
</RadioGroup>
<div className="relative ml-8 h-40 w-full rounded bg-slate-200">
<div className={cn("absolute bottom-3 h-16 w-16 rounded bg-slate-700 sm:right-3")}></div>
</div>
</div>
<Button
variant="darkCTA"
className="pointer-events-none mt-4 animate-pulse cursor-not-allowed select-none bg-gray-200">
Loading
</Button>
</div>
</SettingsCard>
<SettingsCard
noPadding
title="Highlight Border"
description="Make sure your users notice the survey you display">
<div className="flex min-h-full w-full">
<div className="flex w-1/2 flex-col px-6 py-5">
<div className="pointer-events-none mb-6 flex cursor-not-allowed select-none items-center space-x-2">
<Switch id="highlightBorder" checked={false} />
<h2 className="text-sm font-medium text-slate-800">Show highlight border</h2>
</div>
<Button
type="submit"
variant="darkCTA"
className="pointer-events-none mt-4 flex max-w-[100px] animate-pulse cursor-not-allowed select-none items-center justify-center">
Loading
</Button>
</div>
<div className="flex w-1/2 flex-col items-center justify-center gap-4 bg-slate-200 px-6 py-5">
<h3 className="text-slate-500">Preview</h3>
<div className={cn("flex flex-col gap-4 rounded-lg border-2 bg-white p-5")}>
<h3 className="text-sm font-semibold text-slate-800">How easy was it for you to do this?</h3>
<div className="flex rounded-2xl border border-slate-400">
{[1, 2, 3, 4, 5].map((num) => (
<div key={num} className="border-r border-slate-400 px-6 py-5 last:border-r-0">
<span className="text-sm font-medium">{num}</span>
</div>
))}
</div>
</div>
</div>
</div>
</SettingsCard>
<SettingsCard
title="Formbricks Signature"
description="We love your support but understand if you toggle it off.">
<div className="w-full items-center">
<div className="pointer-events-none flex cursor-not-allowed select-none items-center space-x-2">
<Switch id="signature" checked={false} />
<Label htmlFor="signature">Show &apos;Powered by Formbricks&apos; Signature</Label>
</div>
</div>
</SettingsCard>
</div>
);
}

View File

@@ -1,34 +1,37 @@
export const revalidate = REVALIDATION_INTERVAL;
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import SettingsCard from "../SettingsCard";
import SettingsTitle from "../SettingsTitle";
import {
EditBrandColor,
EditPlacement,
EditFormbricksSignature,
EditHighlightBorder,
} from "./editLookAndFeel";
import { EditFormbricksSignature } from "./EditSignature";
import { EditBrandColor } from "./EditBrandColor";
import { EditPlacement } from "./EditPlacement";
import { EditHighlightBorder } from "./EditHighlightBorder";
export default function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
const product = await getProductByEnvironmentId(params.environmentId);
return (
<div>
<SettingsTitle title="Look & Feel" />
<SettingsCard title="Brand Color" description="Match the surveys with your user interface.">
<EditBrandColor environmentId={params.environmentId} />
<EditBrandColor product={product} />
</SettingsCard>
<SettingsCard
title="In-app Survey Placement"
description="Change where surveys will be shown in your web app.">
<EditPlacement environmentId={params.environmentId} />
<EditPlacement product={product} />
</SettingsCard>
<SettingsCard
noPadding
title="Highlight Border"
description="Make sure your users notice the survey you display">
<EditHighlightBorder environmentId={params.environmentId} />
<EditHighlightBorder product={product} />
</SettingsCard>
<SettingsCard
title="Formbricks Signature"
description="We love your support but understand if you toggle it off.">
<EditFormbricksSignature environmentId={params.environmentId} />
<EditFormbricksSignature product={product} />
</SettingsCard>
</div>
);

View File

@@ -56,11 +56,10 @@ export default function Modal({
ref={modalRef}
style={highlightBorderColorStyle}
className={cn(
"pointer-events-auto absolute max-h-[90%] w-full h-fit 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",
"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 ",
slidingAnimationClass
)}>
{children}
</div>
</div>

View File

@@ -1,11 +1,10 @@
import "server-only";
import { prisma } from "@formbricks/database";
import { z } from "zod";
import { Prisma } from "@prisma/client";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/errors";
import { ZProduct } from "@formbricks/types/v1/product";
import type { TProduct } from "@formbricks/types/v1/product";
import type { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
import { cache } from "react";
const selectProduct = {
@@ -57,3 +56,35 @@ export const getProductByEnvironmentId = cache(async (environmentId: string): Pr
throw new ValidationError("Data validation of product failed");
}
});
export const updateProduct = async (
productId: string,
inputProduct: Partial<TProductUpdateInput>
): Promise<TProduct> => {
let updatedProduct;
try {
updatedProduct = await prisma.product.update({
where: {
id: productId,
},
data: {
...inputProduct,
},
select: selectProduct,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
try {
const product = ZProduct.parse(updatedProduct);
return product;
} catch (error) {
if (error instanceof z.ZodError) {
console.error(JSON.stringify(error.errors, null, 2));
}
throw new ValidationError("Data validation of product failed");
}
};

View File

@@ -19,3 +19,11 @@ export const ZProduct = z.object({
});
export type TProduct = z.infer<typeof ZProduct>;
export const ZProductUpdateInput = ZProduct.omit({
id: true,
createdAt: true,
updatedAt: true,
});
export type TProductUpdateInput = z.infer<typeof ZProductUpdateInput>;