Skip to content

Commit

Permalink
A highlight border can now be added to the in-product modal using the…
Browse files Browse the repository at this point in the history
… product settings (formbricks#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>
  • Loading branch information
pandeymangg and mattinannt committed Aug 7, 2023
1 parent 1ccc839 commit fae0719
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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.">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
)}
Expand Down
18 changes: 15 additions & 3 deletions apps/web/components/preview/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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]);
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "highlightBorderColor" TEXT;
27 changes: 14 additions & 13 deletions packages/database/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/js/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
17 changes: 15 additions & 2 deletions packages/js/src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -9,13 +9,15 @@ export default function Modal({
placement,
clickOutside,
darkOverlay,
highlightBorderColor,
close,
}: {
children: VNode;
isOpen: boolean;
placement: PlacementType;
clickOutside: boolean;
darkOverlay: boolean;
highlightBorderColor: string | null;
close: () => void;
}) {
const [show, setShow] = useState(false);
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -97,7 +110,7 @@ export default function Modal({
</svg>
</button>
</div>
<div className="">{children}</div>
<div style={highlightBorderColorStyle}>{children}</div>
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
2 changes: 2 additions & 0 deletions packages/types/v1/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down

0 comments on commit fae0719

Please sign in to comment.