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:
Anshuman Pandey
2023-08-07 19:44:55 +05:30
committed by GitHub
parent 34ff14d43b
commit 370041b0ae
10 changed files with 172 additions and 20 deletions

View File

@@ -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);

View File

@@ -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.">

View File

@@ -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} />
)}

View File

@@ -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>

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "highlightBorderColor" TEXT;

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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";

View File

@@ -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"]),