mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-04 12:51:37 -05:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user