feat: add option to overwrite color and position for single surveys (#645)

* feat: styles ui

* feat: handling brand color and position

* schema update

* survey type update

* feat: local styles preview

* prisma migration

* feat: overriding global settings

* fix: update wording

* fix: do not show positioning in link mode

* update wording

* feat: new component for placement ui

* fix: destructuring localsurvey

* fix: overwritePosition optional

* fix: brandcolor zod validation

* feat: overwrite highlight border setting

* update wording

* merge conflit resolved

* fix: minor review suggestions

* fix: renamed localsurvey to survey

* fix: db schema

* fix comment in .env.example

* fix build errors

* fix: placement

* fix getFirstEnvironmentByUserId function

* fix build errors

---------

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Joe
2023-10-04 22:39:48 +05:30
committed by GitHub
parent de40dc36e6
commit 59752f3ebe
26 changed files with 428 additions and 40 deletions

View File

@@ -79,6 +79,9 @@ export async function duplicateSurveyAction(environmentId: string, surveyId: str
singleUse: existingSurvey.singleUse
? JSON.parse(JSON.stringify(existingSurvey.singleUse))
: prismaClient.JsonNull,
productOverwrites: existingSurvey.productOverwrites
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
: prismaClient.JsonNull,
verifyEmail: existingSurvey.verifyEmail
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
: prismaClient.JsonNull,
@@ -228,6 +231,7 @@ export async function copyToOtherEnvironmentAction(
},
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
},
});

View File

@@ -2,11 +2,11 @@
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 { useState } from "react";
import toast from "react-hot-toast";
import { updateProductAction } from "./actions";
const placements = [
@@ -35,7 +35,9 @@ export function EditPlacement({ product }: EditPlacementProps) {
darkOverlay: overlay === "darkOverlay",
clickOutsideClose: clickOutside === "allow",
};
await updateProductAction(product.id, inputProduct);
toast.success("Placement updated successfully.");
} catch (error) {
toast.error(`Error: ${error.message}`);

View File

@@ -15,6 +15,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
if (!product) {
throw new Error("Product not found");
}
return (
<div>
<SettingsTitle title="Look & Feel" />

View File

@@ -11,20 +11,23 @@ import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
import { ComputerDesktopIcon, DevicePhoneMobileIcon } from "@heroicons/react/24/solid";
import { useEffect, useRef, useState } from "react";
type TPreviewType = "modal" | "fullwidth" | "email";
interface PreviewSurveyProps {
survey: TSurvey | Survey;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId?: string | null;
previewType?: "modal" | "fullwidth" | "email";
previewType?: TPreviewType;
product: TProduct;
environment: TEnvironment;
}
let surveyNameTemp;
export default function PreviewSurvey({
survey,
setActiveQuestionId,
activeQuestionId,
survey,
previewType,
product,
environment,
@@ -34,6 +37,18 @@ export default function PreviewSurvey({
const [previewMode, setPreviewMode] = useState("desktop");
const ContentRef = useRef<HTMLDivElement | null>(null);
const { productOverwrites } = survey || {};
const {
brandColor: surveyBrandColor,
highlightBorderColor: surveyHighlightBorderColor,
placement: surveyPlacement,
} = productOverwrites || {};
const brandColor = surveyBrandColor || product.brandColor;
const placement = surveyPlacement || product.placement;
const highlightBorderColor = surveyHighlightBorderColor || product.highlightBorderColor;
useEffect(() => {
// close modal if there are no questions left
if (survey.type === "web" && !survey.thankYouCard.enabled) {
@@ -52,6 +67,8 @@ export default function PreviewSurvey({
resetQuestionProgress();
surveyNameTemp = survey.name;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [survey]);
function resetQuestionProgress() {
@@ -94,12 +111,12 @@ export default function PreviewSurvey({
{previewType === "modal" ? (
<Modal
isOpen={isModalOpen}
placement={product.placement}
highlightBorderColor={product.highlightBorderColor}
placement={placement}
highlightBorderColor={highlightBorderColor}
previewMode="mobile">
<SurveyInline
survey={survey}
brandColor={product.brandColor}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
formbricksSignature={product.formbricksSignature}
onActiveQuestionChange={setActiveQuestionId}
@@ -114,7 +131,7 @@ export default function PreviewSurvey({
<div className="w-full max-w-md px-4">
<SurveyInline
survey={survey}
brandColor={product.brandColor}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
formbricksSignature={product.formbricksSignature}
onActiveQuestionChange={setActiveQuestionId}
@@ -143,12 +160,12 @@ export default function PreviewSurvey({
{previewType === "modal" ? (
<Modal
isOpen={isModalOpen}
placement={product.placement}
highlightBorderColor={product.highlightBorderColor}
placement={placement}
highlightBorderColor={highlightBorderColor}
previewMode="desktop">
<SurveyInline
survey={survey}
brandColor={product.brandColor}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
formbricksSignature={product.formbricksSignature}
onActiveQuestionChange={setActiveQuestionId}
@@ -161,7 +178,7 @@ export default function PreviewSurvey({
<div className="w-full max-w-md">
<SurveyInline
survey={survey}
brandColor={product.brandColor}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
formbricksSignature={product.formbricksSignature}
onActiveQuestionChange={setActiveQuestionId}

View File

@@ -0,0 +1,101 @@
"use client";
import { cn } from "@formbricks/lib/cn";
import { Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
import { getPlacementStyle } from "@/lib/preview";
import { PlacementType } from "@formbricks/types/js";
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 },
];
type TPlacementProps = {
currentPlacement: PlacementType;
setCurrentPlacement: (placement: PlacementType) => void;
setOverlay: (overlay: string) => void;
overlay: string;
setClickOutside: (clickOutside: boolean) => void;
clickOutside: boolean;
};
export default function Placement({
setCurrentPlacement,
currentPlacement,
setOverlay,
overlay,
setClickOutside,
clickOutside,
}: TPlacementProps) {
return (
<>
<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={(overlay) => setOverlay(overlay)}
value={overlay}
className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="lightOverlay" value="light" />
<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="dark" />
<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={(value) => setClickOutside(value === "allow")}
value={clickOutside ? "allow" : "disallow"}
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>
</>
)}
</>
);
}

View File

@@ -3,6 +3,7 @@ import RecontactOptionsCard from "./RecontactOptionsCard";
import ResponseOptionsCard from "./ResponseOptionsCard";
import WhenToSendCard from "./WhenToSendCard";
import WhoToSendCard from "./WhoToSendCard";
import StylingCard from "./StylingCard";
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
import { TEnvironment } from "@formbricks/types/v1/environment";
import { TActionClass } from "@formbricks/types/v1/actionClasses";
@@ -54,6 +55,8 @@ export default function SettingsView({
setLocalSurvey={setLocalSurvey}
environmentId={environment.id}
/>
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
</div>
);
}

View File

@@ -0,0 +1,215 @@
"use client";
import Placement from "./Placement";
import { PlacementType } from "@formbricks/types/js";
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
import { ColorPicker, Label, Switch } from "@formbricks/ui";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
interface StylingCardProps {
localSurvey: TSurveyWithAnalytics;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurveyWithAnalytics>>;
}
export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCardProps) {
const [open, setOpen] = useState(false);
const { type, productOverwrites } = localSurvey;
const { brandColor, clickOutside, darkOverlay, placement, highlightBorderColor } = productOverwrites ?? {};
const togglePlacement = () => {
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
placement: !!placement ? null : "bottomRight",
},
});
};
const toggleBrandColor = () => {
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
brandColor: !!brandColor ? null : "#64748b",
},
});
};
const toggleHighlightBorderColor = () => {
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
highlightBorderColor: !!highlightBorderColor ? null : "#64748b",
},
});
};
const handleColorChange = (color: string) => {
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
brandColor: color,
},
});
};
const handleBorderColorChange = (color: string) => {
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
highlightBorderColor: color,
},
});
};
const handlePlacementChange = (placement: PlacementType) => {
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
placement,
},
});
};
const handleOverlay = (overlayType: string) => {
const darkOverlay = overlayType === "dark";
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
darkOverlay,
},
});
};
const handleClickOutside = (clickOutside: boolean) => {
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
clickOutside,
},
});
};
return (
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<CheckCircleIcon className="h-8 w-8 text-green-400" />
</div>
<div>
<p className="font-semibold text-slate-800">Styling</p>
<p className="mt-1 truncate text-sm text-slate-500">Overwrite global styling settings</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent>
<hr className="py-1 text-slate-600" />
<div className="p-3">
{/* Brand Color */}
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
<Switch id="autoComplete" checked={!!brandColor} onCheckedChange={toggleBrandColor} />
<Label htmlFor="autoComplete" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Overwrite Brand Color</h3>
<p className="text-xs font-normal text-slate-500">Change the main color for this survey.</p>
</div>
</Label>
</div>
{brandColor && (
<div className="ml-2 mt-4 rounded-lg border bg-slate-50 p-4">
<div className="w-full max-w-xs">
<Label htmlFor="brandcolor">Color (HEX)</Label>
<ColorPicker color={brandColor} onChange={handleColorChange} />
</div>
</div>
)}
</div>
{/* positioning */}
{type !== "link" && (
<div className="p-3 ">
<div className="ml-2 flex items-center space-x-1">
<Switch id="surveyDeadline" checked={!!placement} onCheckedChange={togglePlacement} />
<Label htmlFor="surveyDeadline" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Overwrite Placement</h3>
<p className="text-xs font-normal text-slate-500">Change the placement of this survey.</p>
</div>
</Label>
</div>
{placement && (
<div className="ml-2 mt-4 flex items-center space-x-1 pb-4">
<div className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
<div className="w-full items-center">
<Placement
currentPlacement={placement}
setCurrentPlacement={handlePlacementChange}
setOverlay={handleOverlay}
overlay={darkOverlay ? "dark" : "light"}
setClickOutside={handleClickOutside}
clickOutside={!!clickOutside}
/>
</div>
</div>
</div>
)}
</div>
)}
{/* Highlight border */}
{type !== "link" && (
<div className="p-3 ">
<div className="ml-2 flex items-center space-x-1">
<Switch
id="autoComplete"
checked={!!highlightBorderColor}
onCheckedChange={toggleHighlightBorderColor}
/>
<Label htmlFor="autoComplete" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Overwrite border highlight</h3>
<p className="text-xs font-normal text-slate-500">
Change the border highlight for this survey.
</p>
</div>
</Label>
</div>
{!!highlightBorderColor && (
<div className="ml-2 mt-4 rounded-lg border bg-slate-50 p-4">
<div className="flex items-center space-x-2">
<Switch
id="highlightBorder"
checked={!!highlightBorderColor}
onCheckedChange={toggleHighlightBorderColor}
/>
<h2 className="text-sm font-medium text-slate-800">Show highlight border</h2>
</div>
{!!highlightBorderColor && (
<div className="mt-6 w-full max-w-xs">
<Label htmlFor="brandcolor">Color (HEX)</Label>
<ColorPicker color={highlightBorderColor || ""} onChange={handleBorderColorChange} />
</div>
)}
</div>
)}
</div>
)}
</div>
</Collapsible.CollapsibleContent>
</Collapsible.Root>
);
}

View File

@@ -51,6 +51,8 @@ export default function SurveyEditor({
if (localSurvey?.questions?.length && localSurvey.questions.length > 0) {
setActiveQuestionId(localSurvey.questions[0].id);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localSurvey?.type]);
if (!localSurvey) {

View File

@@ -2062,5 +2062,6 @@ export const minimalSurvey: TSurvey = {
surveyClosedMessage: {
enabled: false,
},
productOverwrites: null,
singleUse: null,
};

View File

@@ -13,8 +13,14 @@ export default async function OnboardingPage() {
if (!session) {
throw new Error("No session found");
}
const environment = await getFirstEnvironmentByUserId(session?.user.id);
const profile = await getProfile(session?.user.id!);
const userId = session?.user.id;
const environment = await getFirstEnvironmentByUserId(userId);
if (!environment) {
throw new Error("No environment found for user");
}
const profile = await getProfile(userId);
const product = await getProductByEnvironmentId(environment?.id!);
if (!environment || !profile || !product) {

View File

@@ -46,6 +46,8 @@ export default function LinkSurvey({
? getPrefillResponseData(survey.questions[0], survey, prefillAnswer)
: undefined;
const brandColor = survey.productOverwrites?.brandColor || product.brandColor;
const responseQueue = useMemo(
() =>
new ResponseQueue(
@@ -110,7 +112,7 @@ export default function LinkSurvey({
)}
<SurveyInline
survey={survey}
brandColor={product.brandColor}
brandColor={brandColor}
formbricksSignature={product.formbricksSignature}
onDisplay={async () => {
if (!isPreview) {

View File

@@ -147,6 +147,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
},
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
},
});

View File

@@ -67,6 +67,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
},
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -3,6 +3,7 @@ import { TIntegrationConfig } from "@formbricks/types/v1/integrations";
import { TResponsePersonAttributes, TResponseData, TResponseMeta } from "@formbricks/types/v1/responses";
import {
TSurveyClosedMessage,
TSurveyProductOverwrites,
TSurveyQuestions,
TSurveySingleUse,
TSurveyThankYouCard,
@@ -20,6 +21,7 @@ declare global {
export type ResponsePersonAttributes = TResponsePersonAttributes;
export type SurveyQuestions = TSurveyQuestions;
export type SurveyThankYouCard = TSurveyThankYouCard;
export type SurveyProductOverwrites = TSurveyProductOverwrites;
export type SurveyClosedMessage = TSurveyClosedMessage;
export type SurveySingleUse = TSurveySingleUse;
export type SurveyVerifyEmail = TSurveyVerifyEmail;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Survey" ADD COLUMN "productOverwrites" JSONB;

View File

@@ -251,10 +251,16 @@ model Survey {
surveyClosedMessage Json?
/// @zod.custom(imports.ZSurveySingleUse)
/// [SurveySingleUse]
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
/// @zod.custom(imports.ZSurveyProductOverwrites)
/// [SurveyProductOverwrites]
productOverwrites Json?
/// @zod.custom(imports.ZSurveySingleUse)
/// [SurveySingleUse]
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
/// @zod.custom(imports.ZSurveyVerifyEmail)
/// [SurveyVerifyEmail]
verifyEmail Json?
verifyEmail Json?
}
model Event {

View File

@@ -10,6 +10,7 @@ export {
ZSurveyQuestions,
ZSurveyThankYouCard,
ZSurveyClosedMessage,
ZSurveyProductOverwrites,
ZSurveyVerifyEmail,
ZSurveySingleUse,
} from "@formbricks/types/v1/surveys";

View File

@@ -42,15 +42,22 @@ export const renderWidget = (survey: TSurvey) => {
surveyState
);
const productOverwrites = survey.productOverwrites ?? {};
const brandColor = productOverwrites.brandColor ?? product.brandColor;
const highlightBorderColor = productOverwrites.highlightBorderColor ?? product.highlightBorderColor;
const clickOutside = productOverwrites.clickOutside ?? product.clickOutsideClose;
const darkOverlay = productOverwrites.darkOverlay ?? product.darkOverlay;
const placement = productOverwrites.placement ?? product.placement;
setTimeout(() => {
renderSurveyModal({
survey: survey,
brandColor: product.brandColor,
brandColor,
formbricksSignature: product.formbricksSignature,
clickOutside: product.clickOutsideClose,
darkOverlay: product.darkOverlay,
highlightBorderColor: product.highlightBorderColor,
placement: product.placement,
clickOutside,
darkOverlay,
highlightBorderColor,
placement,
onDisplay: async () => {
const { id } = await createDisplay(
{

View File

@@ -136,9 +136,8 @@ export const updateEnvironment = async (
export const getFirstEnvironmentByUserId = async (userId: string): Promise<TEnvironment | null> => {
validateInputs([userId, ZId]);
let environmentPrisma;
try {
environmentPrisma = await prisma.environment.findFirst({
return await prisma.environment.findFirst({
where: {
type: "production",
product: {
@@ -159,16 +158,6 @@ export const getFirstEnvironmentByUserId = async (userId: string): Promise<TEnvi
throw error;
}
try {
const environment = ZEnvironment.parse(environmentPrisma);
return environment;
} catch (error) {
if (error instanceof z.ZodError) {
console.error(JSON.stringify(error.errors, null, 2));
}
throw new ValidationError("Data validation of environment failed");
}
};
export const createEnvironment = async (

View File

@@ -42,6 +42,7 @@ export const selectSurvey = {
autoComplete: true,
verifyEmail: true,
redirectUrl: true,
productOverwrites: true,
surveyClosedMessage: true,
singleUse: true,
triggers: {

View File

@@ -1,3 +1,4 @@
import { PlacementType } from "./js";
import { Question } from "./questions";
export interface ThankYouCard {
@@ -22,6 +23,14 @@ export interface VerifyEmail {
subheading?: string;
}
export interface SurveyProductOverwrites {
brandColor: string;
highlightBorderColor: string | null;
placement: PlacementType;
clickOutside: boolean;
darkOverlay: boolean;
}
export interface Survey {
id: string;
createdAt: string;
@@ -47,6 +56,7 @@ export interface Survey {
closeOnDate: Date | null;
singleUse: SurveySingleUse | null;
_count: { responses: number | null } | null;
productOverwrites: SurveyProductOverwrites | null;
}
export interface AttributeFilter {

View File

@@ -0,0 +1,4 @@
import { z } from "zod";
export const ZColor = z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/);
export const ZSurveyPlacement = z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]);

View File

@@ -1,5 +1,6 @@
import { z } from "zod";
import { ZEnvironment } from "./environment";
import { ZColor, ZSurveyPlacement } from "./common";
export const ZProduct = z.object({
id: z.string().cuid2(),
@@ -7,14 +8,11 @@ export const ZProduct = z.object({
updatedAt: z.date(),
name: z.string(),
teamId: z.string(),
brandColor: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/),
highlightBorderColor: z
.string()
.regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)
.nullable(),
brandColor: ZColor,
highlightBorderColor: ZColor.nullable(),
recontactDays: z.number().int(),
formbricksSignature: z.boolean(),
placement: z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]),
placement: ZSurveyPlacement,
clickOutsideClose: z.boolean(),
darkOverlay: z.boolean(),
environments: z.array(ZEnvironment),

View File

@@ -1,6 +1,7 @@
import { z } from "zod";
import { ZActionClass } from "./actionClasses";
import { QuestionType } from "../questions";
import { ZColor, ZSurveyPlacement } from "./common";
export const ZSurveyThankYouCard = z.object({
enabled: z.boolean(),
@@ -8,6 +9,16 @@ export const ZSurveyThankYouCard = z.object({
subheader: z.optional(z.string()),
});
export const ZSurveyProductOverwrites = z.object({
brandColor: ZColor.nullish(),
highlightBorderColor: ZColor.nullish(),
placement: ZSurveyPlacement.nullish(),
clickOutside: z.boolean().nullish(),
darkOverlay: z.boolean().nullish(),
});
export type TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
export const ZSurveyClosedMessage = z
.object({
enabled: z.boolean().optional(),
@@ -269,6 +280,7 @@ export const ZSurvey = z.object({
delay: z.number(),
autoComplete: z.number().nullable(),
closeOnDate: z.date().nullable(),
productOverwrites: ZSurveyProductOverwrites.nullable(),
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
singleUse: ZSurveySingleUse.nullable(),
verifyEmail: ZSurveyVerifyEmail.nullable(),