mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-14 02:31:34 -06:00
A highlight border can now be added to the in-product modal using the product settings (#610)
* feat: added logic for adding highlight border * feat: adds highlight border color to js widget * fix: fixes class issue * fix: removes log * fix: fixes db fields * fix: fixes border color edit * fix: fixes highlight border styles in demo app and preview * fix migrations * remove console.log * fix build issues --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -18,6 +18,7 @@ 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);
|
||||
@@ -180,6 +181,111 @@ export function EditPlacement({ environmentId }) {
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import SettingsCard from "../SettingsCard";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
import { EditBrandColor, EditPlacement, EditFormbricksSignature } from "./editLookAndFeel";
|
||||
import {
|
||||
EditBrandColor,
|
||||
EditPlacement,
|
||||
EditFormbricksSignature,
|
||||
EditHighlightBorder,
|
||||
} from "./editLookAndFeel";
|
||||
|
||||
export default function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
|
||||
return (
|
||||
@@ -14,6 +19,12 @@ export default function ProfileSettingsPage({ params }: { params: { environmentI
|
||||
description="Change where surveys will be shown in your web app.">
|
||||
<EditPlacement environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
noPadding
|
||||
title="Highlight Border"
|
||||
description="Make sure your users notice the survey you display">
|
||||
<EditHighlightBorder environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title="Formbricks Signature"
|
||||
description="We love your support but understand if you toggle it off.">
|
||||
|
||||
@@ -275,7 +275,10 @@ export default function PreviewSurvey({
|
||||
</div>
|
||||
|
||||
{previewType === "modal" ? (
|
||||
<Modal isOpen={isModalOpen} placement={product.placement}>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
placement={product.placement}
|
||||
highlightBorderColor={product.highlightBorderColor}>
|
||||
{!countdownStop && autoClose !== null && autoClose > 0 && (
|
||||
<Progress progress={countdownProgress} brandColor={brandColor} />
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import { getPlacementStyle } from "@/lib/preview";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { PlacementType } from "@formbricks/types/js";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
|
||||
export default function Modal({
|
||||
children,
|
||||
isOpen,
|
||||
placement,
|
||||
highlightBorderColor,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
isOpen: boolean;
|
||||
placement: PlacementType;
|
||||
highlightBorderColor: string | null;
|
||||
}) {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const highlightBorderColorStyle = useMemo(() => {
|
||||
if (!highlightBorderColor) return {};
|
||||
|
||||
return {
|
||||
border: `2px solid ${highlightBorderColor}`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}, [highlightBorderColor]);
|
||||
|
||||
useEffect(() => {
|
||||
setShow(isOpen);
|
||||
}, [isOpen]);
|
||||
@@ -23,9 +34,10 @@ export default function Modal({
|
||||
<div
|
||||
className={cn(
|
||||
show ? "translate-x-0 opacity-100" : "translate-x-32 opacity-0",
|
||||
"pointer-events-auto absolute max-h-[90%] h-fit w-full max-w-sm overflow-hidden 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-hidden overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out",
|
||||
getPlacementStyle(placement)
|
||||
)}>
|
||||
)}
|
||||
style={highlightBorderColorStyle}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ADD COLUMN "highlightBorderColor" TEXT;
|
||||
@@ -319,19 +319,20 @@ enum WidgetPlacement {
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId String
|
||||
environments Environment[]
|
||||
brandColor String @default("#64748b")
|
||||
recontactDays Int @default(7)
|
||||
formbricksSignature Boolean @default(true)
|
||||
placement WidgetPlacement @default(bottomRight)
|
||||
clickOutsideClose Boolean @default(true)
|
||||
darkOverlay Boolean @default(false)
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId String
|
||||
environments Environment[]
|
||||
brandColor String @default("#64748b")
|
||||
highlightBorderColor String?
|
||||
recontactDays Int @default(7)
|
||||
formbricksSignature Boolean @default(true)
|
||||
placement WidgetPlacement @default(bottomRight)
|
||||
clickOutsideClose Boolean @default(true)
|
||||
darkOverlay Boolean @default(false)
|
||||
}
|
||||
|
||||
enum Plan {
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function App({ config, survey, closeSurvey, errorHandler }: AppPr
|
||||
close={close}
|
||||
placement={config.state.product.placement}
|
||||
darkOverlay={config.state.product.darkOverlay}
|
||||
highlightBorderColor={config.state.product.highlightBorderColor}
|
||||
clickOutside={config.state.product.clickOutsideClose}>
|
||||
<SurveyView config={config} survey={survey} close={close} errorHandler={errorHandler} />
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PlacementType } from "@formbricks/types/js";
|
||||
import { h, VNode } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
export default function Modal({
|
||||
@@ -9,6 +9,7 @@ export default function Modal({
|
||||
placement,
|
||||
clickOutside,
|
||||
darkOverlay,
|
||||
highlightBorderColor,
|
||||
close,
|
||||
}: {
|
||||
children: VNode;
|
||||
@@ -16,6 +17,7 @@ export default function Modal({
|
||||
placement: PlacementType;
|
||||
clickOutside: boolean;
|
||||
darkOverlay: boolean;
|
||||
highlightBorderColor: string | null;
|
||||
close: () => void;
|
||||
}) {
|
||||
const [show, setShow] = useState(false);
|
||||
@@ -57,6 +59,17 @@ export default function Modal({
|
||||
}
|
||||
};
|
||||
|
||||
const highlightBorderColorStyle = useMemo(() => {
|
||||
if (!highlightBorderColor) return {};
|
||||
|
||||
return {
|
||||
borderRadius: "8px",
|
||||
border: "2px solid",
|
||||
overflow: "hidden",
|
||||
borderColor: highlightBorderColor,
|
||||
};
|
||||
}, [highlightBorderColor]);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-live="assertive"
|
||||
@@ -97,7 +110,7 @@ export default function Modal({
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="">{children}</div>
|
||||
<div style={highlightBorderColorStyle}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,3 +18,4 @@ export const WEBAPP_URL =
|
||||
// Other
|
||||
export const INTERNAL_SECRET = process.env.INTERNAL_SECRET || "";
|
||||
export const CRON_SECRET = process.env.CRON_SECRET;
|
||||
export const DEFAULT_BRAND_COLOR = "#64748b";
|
||||
|
||||
@@ -7,6 +7,8 @@ export const ZProduct = z.object({
|
||||
name: z.string(),
|
||||
teamId: z.string(),
|
||||
brandColor: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/),
|
||||
highlightBorderColor: z.union([z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/), z.null()]),
|
||||
|
||||
recontactDays: z.number().int(),
|
||||
formbricksSignature: z.boolean(),
|
||||
placement: z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]),
|
||||
|
||||
Reference in New Issue
Block a user