mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-17 19:14:53 -05:00
feat: Branded Link Surveys (#2262)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Shubham Palriwala <spalriwalau@gmail.com>
This commit is contained in:
committed by
GitHub
parent
171469e26a
commit
160b3f6353
@@ -19,7 +19,7 @@ export default function SettingsCard({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("my-4 w-full max-w-4xl bg-white shadow sm:rounded-lg", className)}>
|
||||
<div className={cn("my-4 w-full max-w-4xl bg-white shadow sm:rounded-lg", className)} id={title}>
|
||||
<div className="border-b border-slate-200 bg-slate-100 px-6 py-5">
|
||||
<div className="flex">
|
||||
<h3 className="text-lg font-medium leading-6 text-slate-900">{title}</h3>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import { handleFileUpload } from "@/app/lib/fileUpload";
|
||||
import Image from "next/image";
|
||||
import { ChangeEvent, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
|
||||
import { updateProductAction } from "../actions";
|
||||
|
||||
interface EditLogoProps {
|
||||
product: TProduct;
|
||||
environmentId: string;
|
||||
isViewer: boolean;
|
||||
}
|
||||
|
||||
export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) => {
|
||||
const [logoUrl, setLogoUrl] = useState<string | undefined>(product.logo?.url || undefined);
|
||||
const [logoBgColor, setLogoBgColor] = useState<string | undefined>(product.logo?.bgColor || undefined);
|
||||
const [isBgColorEnabled, setIsBgColorEnabled] = useState<boolean>(!!product.logo?.bgColor);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const uploadResult = await handleFileUpload(file, environmentId);
|
||||
if (uploadResult.error) {
|
||||
toast.error(uploadResult.error);
|
||||
return;
|
||||
}
|
||||
setLogoUrl(uploadResult.url);
|
||||
} catch (error) {
|
||||
toast.error("Logo upload failed. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) await handleImageUpload(file);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const saveChanges = async () => {
|
||||
if (!isEditing) {
|
||||
setIsEditing(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const updatedProduct: Partial<TProductUpdateInput> = {
|
||||
logo: { url: logoUrl, bgColor: isBgColorEnabled ? logoBgColor : undefined },
|
||||
};
|
||||
await updateProductAction(product.id, updatedProduct);
|
||||
toast.success("Logo updated successfully");
|
||||
} catch (error) {
|
||||
toast.error("Failed to update the logo");
|
||||
} finally {
|
||||
setIsEditing(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeLogo = async () => {
|
||||
if (window.confirm("Are you sure you want to remove the logo?")) {
|
||||
setLogoUrl(undefined);
|
||||
if (!isEditing) {
|
||||
setIsEditing(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const updatedProduct: Partial<TProductUpdateInput> = {
|
||||
logo: { url: undefined, bgColor: undefined },
|
||||
};
|
||||
await updateProductAction(product.id, updatedProduct);
|
||||
toast.success("Logo removed successfully", { icon: "🗑️" });
|
||||
} catch (error) {
|
||||
toast.error("Failed to remove the logo");
|
||||
} finally {
|
||||
setIsEditing(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleBackgroundColor = (enabled: boolean) => {
|
||||
setIsBgColorEnabled(enabled);
|
||||
if (!enabled) {
|
||||
setLogoBgColor(undefined);
|
||||
} else if (!logoBgColor) {
|
||||
setLogoBgColor("#f8f8f8");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-8">
|
||||
{logoUrl ? (
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt="Logo"
|
||||
width={256}
|
||||
height={56}
|
||||
style={{ backgroundColor: logoBgColor || undefined }}
|
||||
className="-mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
||||
/>
|
||||
) : (
|
||||
<FileInput
|
||||
id="logo-input"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(files: string[]) => {
|
||||
setLogoUrl(files[0]), setIsEditing(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleFileChange} />
|
||||
|
||||
{isEditing && logoUrl && (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => fileInputRef.current?.click()} variant="secondary" size="sm">
|
||||
Replace Logo
|
||||
</Button>
|
||||
<Button variant="warn" size="sm" onClick={removeLogo} disabled={!isEditing}>
|
||||
Remove Logo
|
||||
</Button>
|
||||
</div>
|
||||
<AdvancedOptionToggle
|
||||
isChecked={isBgColorEnabled}
|
||||
onToggle={toggleBackgroundColor}
|
||||
htmlId="addBackgroundColor"
|
||||
title="Add background color"
|
||||
description="Add a background color to the logo container."
|
||||
childBorder
|
||||
customContainerClass="p-0"
|
||||
disabled={!isEditing}>
|
||||
{isBgColorEnabled && (
|
||||
<div className="px-2">
|
||||
<ColorPicker
|
||||
color={logoBgColor || "#f8f8f8"}
|
||||
onChange={setLogoBgColor}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AdvancedOptionToggle>
|
||||
</>
|
||||
)}
|
||||
{logoUrl && (
|
||||
<Button onClick={saveChanges} disabled={isLoading || isViewer} variant="darkCTA">
|
||||
{isEditing ? "Save" : "Edit"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -92,6 +92,7 @@ export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingPro
|
||||
cardBorderColor: {
|
||||
light: COLOR_DEFAULTS.cardBorderColor,
|
||||
},
|
||||
isLogoHidden: undefined,
|
||||
highlightBorderColor: undefined,
|
||||
isDarkModeEnabled: false,
|
||||
roundness: 8,
|
||||
@@ -124,6 +125,7 @@ export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingPro
|
||||
cardBorderColor: {
|
||||
light: COLOR_DEFAULTS.cardBorderColor,
|
||||
},
|
||||
isLogoHidden: undefined,
|
||||
highlightBorderColor: undefined,
|
||||
isDarkModeEnabled: false,
|
||||
roundness: 8,
|
||||
@@ -197,6 +199,7 @@ export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingPro
|
||||
styling={styling}
|
||||
setStyling={setStyling}
|
||||
hideCheckmark
|
||||
localProduct={localProduct}
|
||||
/>
|
||||
|
||||
<BackgroundStylingCard
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useRef, useState } from "react";
|
||||
import type { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ClientLogo } from "@formbricks/ui/ClientLogo";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
|
||||
interface ThemeStylingPreviewSurveyProps {
|
||||
@@ -165,7 +166,13 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
</Modal>
|
||||
) : (
|
||||
<MediaBackground survey={survey} product={product} ContentRef={ContentRef} isEditorView>
|
||||
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||
{!product.styling?.isLogoHidden && product.logo?.url && (
|
||||
<div className="absolute left-5 top-5">
|
||||
<ClientLogo product={product} previewSurvey />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${product.logo?.url && !product.styling.isLogoHidden && !isFullScreenPreview ? "mt-12" : ""} z-0 w-full max-w-md rounded-lg p-4`}>
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EditLogo } from "@/app/(app)/environments/[environmentId]/settings/lookandfeel/components/EditLogo";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import {
|
||||
@@ -53,6 +54,9 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
||||
className="max-w-7xl"
|
||||
description="Create a style theme for all surveys. You can enable custom styling for each survey.">
|
||||
<ThemeStyling environmentId={params.environmentId} product={product} colors={SURVEY_BG_COLORS} />
|
||||
</SettingsCard>{" "}
|
||||
<SettingsCard title="Logo" description="Upload your company logo to brand surveys and link previews.">
|
||||
<EditLogo product={product} environmentId={params.environmentId} isViewer={isViewer} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title="In-app Survey Placement"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { updateAvatarAction } from "@/app/(app)/environments/[environmentId]/settings/profile/actions";
|
||||
import { handleFileUpload } from "@/app/lib/fileUpload";
|
||||
import { Session } from "next-auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRef, useState } from "react";
|
||||
@@ -9,8 +10,6 @@ import toast from "react-hot-toast";
|
||||
import { ProfileAvatar } from "@formbricks/ui/Avatars";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
import { handleFileUpload } from "../lib";
|
||||
|
||||
export function EditAvatar({ session, environmentId }: { session: Session; environmentId: string }) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function BackgroundStylingCard({
|
||||
{/* Background */}
|
||||
<div className="p-3">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Change Background</h3>
|
||||
<h3 className="text-sm font-semibold text-slate-700">Change background</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Pick a background from our library or upload your own.
|
||||
</p>
|
||||
@@ -119,7 +119,7 @@ export default function BackgroundStylingCard({
|
||||
{/* Overlay */}
|
||||
<div className="flex flex-col gap-4 p-3">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Background Overlay</h3>
|
||||
<h3 className="text-sm font-semibold text-slate-700">Background overlay</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Darken or lighten background of your choice.
|
||||
</p>
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, { useMemo } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TProduct, TProductStyling } from "@formbricks/types/product";
|
||||
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
@@ -23,6 +23,7 @@ type CardStylingSettingsProps = {
|
||||
hideCheckmark?: boolean;
|
||||
surveyType?: TSurveyType;
|
||||
disabled?: boolean;
|
||||
localProduct: TProduct;
|
||||
};
|
||||
|
||||
const CardStylingSettings = ({
|
||||
@@ -32,9 +33,15 @@ const CardStylingSettings = ({
|
||||
surveyType,
|
||||
disabled,
|
||||
open,
|
||||
localProduct,
|
||||
setOpen,
|
||||
}: CardStylingSettingsProps) => {
|
||||
const cardBgColor = styling?.cardBackgroundColor?.light || COLOR_DEFAULTS.cardBackgroundColor;
|
||||
|
||||
const isLogoHidden = styling?.isLogoHidden ?? false;
|
||||
|
||||
const isLogoVisible = !isLogoHidden && !!localProduct.logo?.url;
|
||||
|
||||
const setCardBgColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
@@ -112,6 +119,13 @@ const CardStylingSettings = ({
|
||||
});
|
||||
};
|
||||
|
||||
const toggleLogoVisibility = () => {
|
||||
setStyling({
|
||||
...styling,
|
||||
isLogoHidden: !isLogoHidden,
|
||||
});
|
||||
};
|
||||
|
||||
const hideProgressBar = useMemo(() => {
|
||||
return styling?.hideProgressBar;
|
||||
}, [styling]);
|
||||
@@ -192,7 +206,7 @@ const CardStylingSettings = ({
|
||||
/>
|
||||
<Label htmlFor="hideProgressBar" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Hide Progress Bar</h3>
|
||||
<h3 className="text-sm font-semibold text-slate-700">Hide progress bar</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Disable the visibility of survey progress.
|
||||
</p>
|
||||
@@ -200,6 +214,23 @@ const CardStylingSettings = ({
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{isLogoVisible && (!surveyType || surveyType === "link") && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="isLogoHidden" checked={isLogoHidden} onCheckedChange={toggleLogoVisibility} />
|
||||
<Label htmlFor="isLogoHidden" className="cursor-pointer">
|
||||
<div className="ml-2 flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Hide logo</h3>
|
||||
{hideCheckmark && <Badge text="Link Surveys" type="gray" size="normal" />}
|
||||
</div>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Hides the logo in this specific survey
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!surveyType || surveyType === "web") && (
|
||||
<div className="flex max-w-xs flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -150,6 +150,7 @@ const StylingView = ({
|
||||
setStyling={setStyling}
|
||||
surveyType={localSurvey.type}
|
||||
disabled={!overwriteThemeStyling}
|
||||
localProduct={product}
|
||||
/>
|
||||
|
||||
{localSurvey.type === "link" && (
|
||||
|
||||
@@ -349,6 +349,7 @@ export default function SurveyMenuBar({
|
||||
|
||||
try {
|
||||
await updateSurveyAction({ ...strippedSurvey });
|
||||
|
||||
setIsSurveySaving(false);
|
||||
toast.success("Changes saved.");
|
||||
if (shouldNavigateBack) {
|
||||
|
||||
@@ -4,8 +4,7 @@ import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/M
|
||||
import TabOption from "@/app/(app)/environments/[environmentId]/surveys/components/TabOption";
|
||||
import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground";
|
||||
import { Variants, motion } from "framer-motion";
|
||||
import { ExpandIcon, MonitorIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react";
|
||||
import { RefreshCcwIcon } from "lucide-react";
|
||||
import { ExpandIcon, MonitorIcon, RefreshCcwIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
@@ -14,6 +13,7 @@ import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ClientLogo } from "@formbricks/ui/ClientLogo";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
|
||||
type TPreviewType = "modal" | "fullwidth" | "email";
|
||||
@@ -241,6 +241,9 @@ export default function PreviewSurvey({
|
||||
</Modal>
|
||||
) : (
|
||||
<div className="w-full px-3">
|
||||
<div className="absolute left-5 top-5">
|
||||
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
|
||||
</div>
|
||||
<div className="no-scrollbar z-10 w-full max-w-md overflow-y-auto rounded-lg border border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
@@ -317,7 +320,10 @@ export default function PreviewSurvey({
|
||||
</Modal>
|
||||
) : (
|
||||
<MediaBackground survey={survey} product={product} ContentRef={ContentRef} isEditorView>
|
||||
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||
<div className="absolute left-5 top-5">
|
||||
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
|
||||
</div>
|
||||
<div className="z-0 w-full max-w-md rounded-lg border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { TProduct } from "@formbricks/types/product";
|
||||
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { ClientLogo } from "@formbricks/ui/ClientLogo";
|
||||
import ContentWrapper from "@formbricks/ui/ContentWrapper";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
|
||||
@@ -158,7 +159,7 @@ export default function LinkSurvey({
|
||||
return <VerifyEmail singleUseId={suId ?? ""} survey={survey} languageCode={languageCode} />;
|
||||
}
|
||||
|
||||
const getStyling = () => {
|
||||
const determineStyling = () => {
|
||||
// allow style overwrite is disabled from the product
|
||||
if (!product.styling.allowStyleOverwrite) {
|
||||
return product.styling;
|
||||
@@ -179,8 +180,9 @@ export default function LinkSurvey({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentWrapper className="my-12 h-full w-full p-0 md:max-w-md">
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
{!determineStyling().isLogoHidden && product.logo?.url && <ClientLogo product={product} />}
|
||||
<ContentWrapper className="w-11/12 p-0 md:max-w-md">
|
||||
{isPreview && (
|
||||
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
<div />
|
||||
@@ -195,9 +197,10 @@ export default function LinkSurvey({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
styling={getStyling()}
|
||||
styling={determineStyling()}
|
||||
languageCode={languageCode}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
getSetIsError={(f: (value: boolean) => void) => {
|
||||
@@ -267,6 +270,6 @@ export default function LinkSurvey({
|
||||
responseCount={responseCount}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,9 +119,9 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
||||
return (
|
||||
<div
|
||||
ref={ContentRef}
|
||||
className={`relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-3xl border-8 border-slate-500 ${getFilterStyle()}`}>
|
||||
className={`relative h-[90%] max-h-[40rem] w-[22rem] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
|
||||
{/* below element is use to create notch for the mobile device mockup */}
|
||||
<div className="absolute left-1/2 right-1/2 top-0 z-20 h-4 w-1/2 -translate-x-1/2 transform rounded-b-md bg-slate-500"></div>
|
||||
<div className="absolute left-1/2 right-1/2 top-2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
|
||||
{renderBackground()}
|
||||
{renderContent()}
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center px-2">
|
||||
<div className="flex min-h-screen flex-col items-center justify-center">
|
||||
{renderBackground()}
|
||||
<div className="relative w-full">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ADD COLUMN "logo" JSONB;
|
||||
@@ -433,6 +433,9 @@ model Product {
|
||||
clickOutsideClose Boolean @default(true)
|
||||
darkOverlay Boolean @default(false)
|
||||
languages Language[]
|
||||
/// @zod.custom(imports.ZLogo)
|
||||
/// [Logo]
|
||||
logo Json?
|
||||
|
||||
@@unique([teamId, name])
|
||||
@@index([teamId])
|
||||
|
||||
@@ -34,6 +34,7 @@ const selectProduct = {
|
||||
darkOverlay: true,
|
||||
environments: true,
|
||||
styling: true,
|
||||
logo: true,
|
||||
};
|
||||
|
||||
export const getProducts = async (teamId: string, page?: number): Promise<TProduct[]> => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function Subheader({ subheader, questionId }: { subheader?: string; questionId: string }) {
|
||||
return (
|
||||
<label htmlFor={questionId} className="text-subheading block text-sm font-normal leading-6">
|
||||
<label htmlFor={questionId} className="text-subheading block text-sm font-normal leading-5">
|
||||
{subheader}
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -340,7 +340,7 @@ export function Survey({
|
||||
getCardContent()
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<div className="mt-4">
|
||||
{isBrandingEnabled && <FormbricksBranding />}
|
||||
{showProgressBar && <ProgressBar survey={survey} questionId={questionId} />}
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
<legend className="sr-only">Options</legend>
|
||||
|
||||
<div
|
||||
className="bg-survey-bg relative max-h-[33vh] space-y-2 overflow-y-auto py-0.5 pr-2"
|
||||
className="bg-survey-bg relative max-h-[27vh] space-y-2 overflow-y-auto py-0.5 pr-2"
|
||||
role="radiogroup"
|
||||
ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => (
|
||||
|
||||
@@ -30,6 +30,13 @@ export const ZLanguageUpdate = z.object({
|
||||
});
|
||||
export type TLanguageUpdate = z.infer<typeof ZLanguageUpdate>;
|
||||
|
||||
export const ZLogo = z.object({
|
||||
url: z.string().optional(),
|
||||
bgColor: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TLogo = z.infer<typeof ZLogo>;
|
||||
|
||||
export const ZProduct = z.object({
|
||||
id: z.string().cuid2(),
|
||||
createdAt: z.date(),
|
||||
@@ -47,6 +54,7 @@ export const ZProduct = z.object({
|
||||
brandColor: ZColor.nullish(),
|
||||
highlightBorderColor: ZColor.nullish(),
|
||||
languages: z.array(ZLanguage),
|
||||
logo: ZLogo.optional(),
|
||||
});
|
||||
|
||||
export type TProduct = z.infer<typeof ZProduct>;
|
||||
@@ -64,6 +72,7 @@ export const ZProductUpdateInput = z.object({
|
||||
darkOverlay: z.boolean().optional(),
|
||||
environments: z.array(ZEnvironment).optional(),
|
||||
styling: ZProductStyling.optional(),
|
||||
logo: ZLogo.optional(),
|
||||
});
|
||||
|
||||
export type TProductUpdateInput = z.infer<typeof ZProductUpdateInput>;
|
||||
|
||||
@@ -38,4 +38,5 @@ export const ZBaseStyling = z.object({
|
||||
cardArrangement: ZCardArrangement.nullish(),
|
||||
background: ZSurveyStylingBackground.nullish(),
|
||||
hideProgressBar: z.boolean().nullish(),
|
||||
isLogoHidden: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ interface AdvancedOptionToggleProps {
|
||||
children?: React.ReactNode;
|
||||
childBorder?: boolean;
|
||||
customContainerClass?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function AdvancedOptionToggle({
|
||||
@@ -23,11 +24,12 @@ export function AdvancedOptionToggle({
|
||||
children,
|
||||
childBorder,
|
||||
customContainerClass,
|
||||
disabled = false,
|
||||
}: AdvancedOptionToggleProps) {
|
||||
return (
|
||||
<div className={cn("px-4 py-2", customContainerClass)}>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id={htmlId} checked={isChecked} onCheckedChange={onToggle} />
|
||||
<Switch id={htmlId} checked={isChecked} onCheckedChange={onToggle} disabled={disabled} />
|
||||
<Label htmlFor={htmlId} className="cursor-pointer rounded-l-lg">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">{title}</h3>
|
||||
|
||||
@@ -83,7 +83,7 @@ export const Button: React.ForwardRefExoticComponent<
|
||||
variant === "minimal" &&
|
||||
(disabled
|
||||
? "border border-slate-200 text-slate-400"
|
||||
: "hover:text-slate-600 text-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900 dark:text-slate-700 dark:hover:text-slate-500"),
|
||||
: "hover:text-slate-600 text-slate-700 border border-transparent focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900 dark:text-slate-700 dark:hover:text-slate-500"),
|
||||
variant === "alert" &&
|
||||
(disabled
|
||||
? "border border-transparent bg-slate-400 text-white"
|
||||
|
||||
53
packages/ui/ClientLogo/index.tsx
Normal file
53
packages/ui/ClientLogo/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
|
||||
interface ClientLogoProps {
|
||||
environmentId?: string;
|
||||
product: TProduct;
|
||||
previewSurvey?: boolean;
|
||||
}
|
||||
|
||||
export const ClientLogo = ({ environmentId, product, previewSurvey = false }: ClientLogoProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(previewSurvey ? "" : "left-5 top-5 md:left-7 md:top-7", "group absolute z-0 rounded-lg")}
|
||||
style={{ backgroundColor: product.logo?.bgColor }}>
|
||||
{previewSurvey && environmentId && (
|
||||
<Link
|
||||
href={`/environments/${environmentId}/settings/lookandfeel`}
|
||||
className="group/link absolute h-full w-full hover:cursor-pointer"
|
||||
target="_blank">
|
||||
<ArrowUpRight
|
||||
size={24}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform rounded-md bg-white/80 p-0.5 text-slate-700 opacity-0 transition-all duration-200 ease-in-out group-hover/link:opacity-100"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
{product.logo?.url ? (
|
||||
<Image
|
||||
src={product.logo?.url}
|
||||
className={cn(
|
||||
previewSurvey ? "max-h-12" : "max-h-16 md:max-h-20",
|
||||
"w-auto max-w-40 rounded-lg object-contain p-1 md:max-w-56"
|
||||
)}
|
||||
width={256}
|
||||
height={64}
|
||||
alt="Company Logo"
|
||||
/>
|
||||
) : (
|
||||
<Link
|
||||
href={`/environments/${environmentId}/settings/lookandfeel`}
|
||||
className="whitespace-nowrap rounded-md border border-dashed border-slate-400 bg-slate-200 px-6 py-3 text-xs text-slate-900 opacity-50 backdrop-blur-sm hover:cursor-pointer hover:border-slate-600"
|
||||
target="_blank">
|
||||
Add logo
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -56,7 +56,7 @@ export const Modal: React.FC<Modal> = ({
|
||||
/>
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 mt-24 overflow-y-auto">
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
|
||||
Reference in New Issue
Block a user