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:
Manish Singh Bisht
2024-04-05 16:50:39 +05:30
committed by GitHub
parent 171469e26a
commit 160b3f6353
26 changed files with 320 additions and 25 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -150,6 +150,7 @@ const StylingView = ({
setStyling={setStyling}
surveyType={localSurvey.type}
disabled={!overwriteThemeStyling}
localProduct={product}
/>
{localSurvey.type === "link" && (

View File

@@ -349,6 +349,7 @@ export default function SurveyMenuBar({
try {
await updateSurveyAction({ ...strippedSurvey });
setIsSurveySaving(false);
toast.success("Changes saved.");
if (shouldNavigateBack) {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "logo" JSONB;

View File

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

View File

@@ -34,6 +34,7 @@ const selectProduct = {
darkOverlay: true,
environments: true,
styling: true,
logo: true,
};
export const getProducts = async (teamId: string, page?: number): Promise<TProduct[]> => {

View File

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

View File

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

View File

@@ -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) => (

View File

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

View File

@@ -38,4 +38,5 @@ export const ZBaseStyling = z.object({
cardArrangement: ZCardArrangement.nullish(),
background: ZSurveyStylingBackground.nullish(),
hideProgressBar: z.boolean().nullish(),
isLogoHidden: z.boolean().nullish(),
});

View File

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

View File

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

View 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>
);
};

View File

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