feat: Product Model Revamp (#4353)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2024-12-03 10:04:09 +05:30
committed by GitHub
parent 5dcd32050a
commit 35b2d12e18
315 changed files with 4344 additions and 3587 deletions
@@ -5,23 +5,23 @@ import { useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@formbricks/lib/cn";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
interface ClientLogoProps {
environmentId?: string;
product: TProduct;
project: TProject;
previewSurvey?: boolean;
}
export const ClientLogo = ({ environmentId, product, previewSurvey = false }: ClientLogoProps) => {
export const ClientLogo = ({ environmentId, project, previewSurvey = false }: ClientLogoProps) => {
const t = useTranslations();
return (
<div
className={cn(previewSurvey ? "" : "left-3 top-3 md:left-7 md:top-7", "group absolute z-0 rounded-lg")}
style={{ backgroundColor: product.logo?.bgColor }}>
style={{ backgroundColor: project.logo?.bgColor }}>
{previewSurvey && environmentId && (
<Link
href={`/environments/${environmentId}/product/look`}
href={`/environments/${environmentId}/project/look`}
className="group/link absolute h-full w-full hover:cursor-pointer"
target="_blank">
<ArrowUpRight
@@ -30,9 +30,9 @@ export const ClientLogo = ({ environmentId, product, previewSurvey = false }: Cl
/>
</Link>
)}
{product.logo?.url ? (
{project.logo?.url ? (
<Image
src={product.logo?.url}
src={project.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"
@@ -43,7 +43,7 @@ export const ClientLogo = ({ environmentId, product, previewSurvey = false }: Cl
/>
) : (
<Link
href={`/environments/${environmentId}/product/look`}
href={`/environments/${environmentId}/project/look`}
onClick={(e) => {
if (!environmentId) {
e.preventDefault();
@@ -0,0 +1,46 @@
import { Button } from "@/modules/ui/components/button";
import { Muted, P } from "@/modules/ui/components/typography";
export type ModalButton = {
text: string;
href?: string;
onClick?: () => void;
};
interface EmptyContentProps {
icon: React.ReactNode;
title: string;
description: string;
buttons: [ModalButton, ModalButton];
}
export const EmptyContent = ({ icon, title, description, buttons }: EmptyContentProps) => {
const [primaryButton, secondaryButton] = buttons;
return (
<div className="flex flex-col items-center gap-6 p-6">
<div className="rounded-md border border-slate-200 p-3">{icon}</div>
<div className="flex flex-col items-center gap-2">
<P className="text-xl font-semibold text-slate-900">{title}</P>
<Muted className="text-slate-500">{description}</Muted>
</div>
<div className="flex gap-3">
<Button
{...(primaryButton.href
? { href: primaryButton.href, target: "_blank", rel: "noopener noreferrer" }
: {})}
{...(primaryButton.onClick ? { onClick: primaryButton.onClick } : {})}>
{primaryButton.text}
</Button>
<Button
variant="secondary"
{...(secondaryButton.href
? { href: secondaryButton.href, target: "_blank", rel: "noopener noreferrer" }
: {})}
{...(secondaryButton.onClick ? { onClick: secondaryButton.onClick } : {})}>
{secondaryButton.text}
</Button>
</div>
</div>
);
};
@@ -28,7 +28,7 @@ export const EmptySpaceFiller = ({
{!environment.appSetupCompleted && !noWidgetRequired && (
<Link
className="flex w-full items-center justify-center"
href={`/environments/${environment.id}/product/app-connection`}>
href={`/environments/${environment.id}/project/app-connection`}>
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
{t("environments.surveys.summary.install_widget")}{" "}
<strong>{t("environments.surveys.summary.go_to_setup_checklist")} </strong>
@@ -58,7 +58,7 @@ export const EmptySpaceFiller = ({
{!environment.appSetupCompleted && !noWidgetRequired && (
<Link
className="flex h-full w-full items-center justify-center"
href={`/environments/${environment.id}/product/app-connection`}>
href={`/environments/${environment.id}/project/app-connection`}>
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
{t("environments.surveys.summary.install_widget")}{" "}
<strong>{t("environments.surveys.summary.go_to_setup_checklist")} </strong>
@@ -90,7 +90,7 @@ export const EmptySpaceFiller = ({
{!environment.appSetupCompleted && !noWidgetRequired && (
<Link
className="flex h-full w-full items-center justify-center"
href={`/environments/${environment.id}/product/app-connection`}>
href={`/environments/${environment.id}/project/app-connection`}>
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
{t("environments.surveys.summary.install_widget")}{" "}
<strong>{t("environments.surveys.summary.go_to_setup_checklist")} 👉</strong>
@@ -98,7 +98,7 @@ export const EmptySpaceFiller = ({
</Link>
)}
{(environment.appSetupCompleted || noWidgetRequired) && (
<span className="text-center">{t("environments.product.tags.empty_message")}</span>
<span className="text-center">{t("environments.project.tags.empty_message")}</span>
)}
</div>
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
@@ -139,7 +139,7 @@ export const EmptySpaceFiller = ({
{!environment.appSetupCompleted && !noWidgetRequired && (
<Link
className="flex h-full w-full items-center justify-center"
href={`/environments/${environment.id}/product/app-connection`}>
href={`/environments/${environment.id}/project/app-connection`}>
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
{t("environments.surveys.summary.install_widget")}{" "}
<strong>{t("environments.surveys.summary.go_to_setup_checklist")} 👉</strong>
@@ -15,7 +15,7 @@ export const EnvironmentNotice = async ({ environmentId, subPageUrl }: Environme
throw new Error("Environment not found");
}
const environments = await getEnvironments(environment.productId);
const environments = await getEnvironments(environment.projectId);
const otherEnvironmentId = environments.filter((e) => e.id !== environment.id)[0].id;
return (
@@ -4,13 +4,13 @@ import { useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import { TSurvey } from "@formbricks/types/surveys/types";
interface MediaBackgroundProps {
children: React.ReactNode;
survey: TSurvey;
product: TProduct;
project: TProject;
isEditorView?: boolean;
isMobilePreview?: boolean;
ContentRef?: React.RefObject<HTMLDivElement>;
@@ -19,7 +19,7 @@ interface MediaBackgroundProps {
export const MediaBackground: React.FC<MediaBackgroundProps> = ({
children,
product,
project,
survey,
isEditorView = false,
isMobilePreview = false,
@@ -31,26 +31,26 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
const [backgroundLoaded, setBackgroundLoaded] = useState(false);
const [authorDetailsForUnsplash, setAuthorDetailsForUnsplash] = useState({ authorName: "", authorURL: "" });
// get the background from either the survey or the product styling
// get the background from either the survey or the project styling
const background = useMemo(() => {
// allow style overwrite is disabled from the product
if (!product.styling.allowStyleOverwrite) {
return product.styling.background;
// allow style overwrite is disabled from the project
if (!project.styling.allowStyleOverwrite) {
return project.styling.background;
}
// allow style overwrite is enabled from the product
if (product.styling.allowStyleOverwrite) {
// allow style overwrite is enabled from the project
if (project.styling.allowStyleOverwrite) {
// survey style overwrite is disabled
if (!survey.styling?.overwriteThemeStyling) {
return product.styling.background;
return project.styling.background;
}
// survey style overwrite is enabled
return survey.styling.background;
}
return product.styling.background;
}, [product.styling.allowStyleOverwrite, product.styling.background, survey.styling]);
return project.styling.background;
}, [project.styling.allowStyleOverwrite, project.styling.background, survey.styling]);
useEffect(() => {
if (background?.bgType === "animation" && animatedBackgroundRef.current) {
@@ -10,8 +10,8 @@ import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { TEnvironment } from "@formbricks/types/environment";
import { TJsFileUploadParams } from "@formbricks/types/js";
import type { TProduct } from "@formbricks/types/product";
import { TProductStyling } from "@formbricks/types/product";
import type { TProject } from "@formbricks/types/project";
import { TProjectStyling } from "@formbricks/types/project";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurvey, TSurveyQuestionId, TSurveyStyling } from "@formbricks/types/surveys/types";
import { Modal } from "./components/modal";
@@ -23,7 +23,7 @@ interface PreviewSurveyProps {
survey: TSurvey;
questionId?: string | null;
previewType?: TPreviewType;
product: TProduct;
project: TProject;
environment: TEnvironment;
languageCode: string;
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
@@ -64,7 +64,7 @@ export const PreviewSurvey = ({
questionId,
survey,
previewType,
product,
project,
environment,
languageCode,
onFileUpload,
@@ -78,7 +78,7 @@ export const PreviewSurvey = ({
const [previewPosition, setPreviewPosition] = useState("relative");
const ContentRef = useRef<HTMLDivElement | null>(null);
const [shrink, setShrink] = useState(false);
const { productOverwrites } = survey || {};
const { projectOverwrites } = survey || {};
const previewScreenVariants: Variants = {
expanded: {
right: "5%",
@@ -114,35 +114,35 @@ export const PreviewSurvey = ({
},
};
const { placement: surveyPlacement } = productOverwrites || {};
const { darkOverlay: surveyDarkOverlay } = productOverwrites || {};
const { clickOutsideClose: surveyClickOutsideClose } = productOverwrites || {};
const { placement: surveyPlacement } = projectOverwrites || {};
const { darkOverlay: surveyDarkOverlay } = projectOverwrites || {};
const { clickOutsideClose: surveyClickOutsideClose } = projectOverwrites || {};
const placement = surveyPlacement || product.placement;
const darkOverlay = surveyDarkOverlay ?? product.darkOverlay;
const clickOutsideClose = surveyClickOutsideClose ?? product.clickOutsideClose;
const placement = surveyPlacement || project.placement;
const darkOverlay = surveyDarkOverlay ?? project.darkOverlay;
const clickOutsideClose = surveyClickOutsideClose ?? project.clickOutsideClose;
const widgetSetupCompleted = appSetupCompleted;
const styling: TSurveyStyling | TProductStyling = useMemo(() => {
// allow style overwrite is disabled from the product
if (!product.styling.allowStyleOverwrite) {
return product.styling;
const styling: TSurveyStyling | TProjectStyling = useMemo(() => {
// allow style overwrite is disabled from the project
if (!project.styling.allowStyleOverwrite) {
return project.styling;
}
// allow style overwrite is enabled from the product
if (product.styling.allowStyleOverwrite) {
// allow style overwrite is enabled from the project
if (project.styling.allowStyleOverwrite) {
// survey style overwrite is disabled
if (!survey.styling?.overwriteThemeStyling) {
return product.styling;
return project.styling;
}
// survey style overwrite is enabled
return survey.styling;
}
return product.styling;
}, [product.styling, survey.styling]);
return project.styling;
}, [project.styling, survey.styling]);
const updateQuestionId = useCallback(
(newQuestionId: TSurveyQuestionId) => {
@@ -251,7 +251,7 @@ export const PreviewSurvey = ({
<div className="absolute right-0 top-0 m-2">
<ResetProgressButton onClick={resetQuestionProgress} />
</div>
<MediaBackground survey={survey} product={product} ContentRef={ContentRef} isMobilePreview>
<MediaBackground survey={survey} project={project} ContentRef={ContentRef} isMobilePreview>
{previewType === "modal" ? (
<Modal
isOpen={isModalOpen}
@@ -263,7 +263,7 @@ export const PreviewSurvey = ({
background={styling?.cardBackgroundColor?.light}>
<SurveyInline
survey={survey}
isBrandingEnabled={product.inAppSurveyBranding}
isBrandingEnabled={project.inAppSurveyBranding}
isRedirectDisabled={true}
languageCode={languageCode}
onFileUpload={onFileUpload}
@@ -280,13 +280,13 @@ export const PreviewSurvey = ({
<div className="flex h-full w-full flex-col justify-end">
<div className="absolute left-5 top-5">
{!styling.isLogoHidden && (
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
<ClientLogo environmentId={environment.id} project={project} previewSurvey />
)}
</div>
<div className="z-10 w-full max-w-md rounded-lg border border-transparent">
<SurveyInline
survey={{ ...survey, type: "link" }}
isBrandingEnabled={product.linkSurveyBranding}
isBrandingEnabled={project.linkSurveyBranding}
onFileUpload={onFileUpload}
languageCode={languageCode}
responseCount={42}
@@ -365,7 +365,7 @@ export const PreviewSurvey = ({
background={styling.cardBackgroundColor?.light}>
<SurveyInline
survey={survey}
isBrandingEnabled={product.inAppSurveyBranding}
isBrandingEnabled={project.inAppSurveyBranding}
isRedirectDisabled={true}
languageCode={languageCode}
onFileUpload={onFileUpload}
@@ -379,16 +379,16 @@ export const PreviewSurvey = ({
/>
</Modal>
) : (
<MediaBackground survey={survey} product={product} ContentRef={ContentRef} isEditorView>
<MediaBackground survey={survey} project={project} ContentRef={ContentRef} isEditorView>
<div className="absolute left-5 top-5">
{!styling.isLogoHidden && (
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
<ClientLogo environmentId={environment.id} project={project} previewSurvey />
)}
</div>
<div className="z-0 w-full max-w-lg rounded-lg border-transparent">
<SurveyInline
survey={{ ...survey, type: "link" }}
isBrandingEnabled={product.linkSurveyBranding}
isBrandingEnabled={project.linkSurveyBranding}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
languageCode={languageCode}
@@ -62,7 +62,7 @@ export const TagsCombobox = ({
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button size="sm" aria-expanded={open}>
{t("environments.product.tags.add_tag")}
{t("environments.project.tags.add_tag")}
</Button>
</PopoverTrigger>
<PopoverContent className="max-h-60 w-[200px] overflow-y-auto p-0">
@@ -83,8 +83,8 @@ export const TagsCombobox = ({
<CommandInput
placeholder={
tagsToSearch?.length === 0
? t("environments.product.tags.add_tag")
: t("environments.product.tags.search_tags")
? t("environments.project.tags.add_tag")
: t("environments.project.tags.search_tags")
}
className="border-b border-none border-transparent shadow-none outline-0 ring-offset-transparent focus:border-none focus:border-transparent focus:shadow-none focus:outline-0 focus:ring-offset-transparent"
value={searchValue}
@@ -126,7 +126,7 @@ export const TagsCombobox = ({
onClick={() => createTag?.(searchValue)}
className="h-8 w-full text-left hover:cursor-pointer hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!!currentTags.find((tag) => tag.label === searchValue)}>
+ {t("environments.product.tags.add")} {searchValue}
+ {t("environments.project.tags.add")} {searchValue}
</button>
</CommandItem>
)}