diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx index 72f302e09b..ffe79176a4 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx @@ -1,7 +1,6 @@ import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks"; import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils"; import { XIcon } from "lucide-react"; -import { notFound } from "next/navigation"; import { WEBAPP_URL } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; @@ -26,12 +25,9 @@ const Page = async ({ params }: ConnectPageProps) => { throw new Error("Product not found"); } - const channel = product.config.channel; - const industry = product.config.industry; + const channel = product.config.channel || null; + const industry = product.config.industry || null; - if (!channel || !industry) { - return notFound(); - } const customHeadline = getCustomHeadline(channel, industry); return ( diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/layout.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.tsx similarity index 100% rename from apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/layout.tsx rename to apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.tsx diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx new file mode 100644 index 0000000000..bfc0f014f4 --- /dev/null +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils"; +import { XMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates"; +import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; +import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper"; +import { TProduct } from "@formbricks/types/product"; +import { TSurveyCreateInput } from "@formbricks/types/surveys/types"; +import { TXMTemplate } from "@formbricks/types/templates"; +import { TUser } from "@formbricks/types/user"; +import { createSurveyAction } from "@formbricks/ui/TemplateList/actions"; + +interface XMTemplateListProps { + product: TProduct; + user: TUser; + environmentId: string; +} + +export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListProps) => { + const [activeTemplateId, setActiveTemplateId] = useState(null); + + const router = useRouter(); + + const createSurvey = async (activeTemplate: TXMTemplate) => { + const augmentedTemplate: TSurveyCreateInput = { + ...activeTemplate, + type: "link", + createdBy: user.id, + }; + const createSurveyResponse = await createSurveyAction({ + environmentId: environmentId, + surveyBody: augmentedTemplate, + }); + + if (createSurveyResponse?.data) { + router.push(`/environments/${environmentId}/surveys/${createSurveyResponse.data.id}/edit?mode=cx`); + } else { + const errorMessage = getFormattedErrorMessage(createSurveyResponse); + toast.error(errorMessage); + } + }; + + const handleTemplateClick = (templateIdx) => { + setActiveTemplateId(templateIdx); + const template = XMTemplates[templateIdx]; + const newTemplate = replacePresetPlaceholders(template, product); + createSurvey(newTemplate); + }; + + const XMTemplateOptions = [ + { + title: "NPS", + description: "Implement proven best practices to understand WHY people buy.", + icon: ShoppingCartIcon, + onClick: () => handleTemplateClick(0), + isLoading: activeTemplateId === 0, + }, + { + title: "5-Star Rating", + description: "Universal feedback solution to gauge overall satisfaction.", + icon: StarIcon, + onClick: () => handleTemplateClick(1), + isLoading: activeTemplateId === 1, + }, + { + title: "CSAT", + description: "Implement best practices to measure customer satisfaction.", + icon: ThumbsUpIcon, + onClick: () => handleTemplateClick(2), + isLoading: activeTemplateId === 2, + }, + { + title: "CES", + description: "Leverage every touchpoint to understand ease of customer interaction.", + icon: ActivityIcon, + onClick: () => handleTemplateClick(3), + isLoading: activeTemplateId === 3, + }, + { + title: "Smileys", + description: "Use visual indicators to capture feedback across customer touchpoints.", + icon: SmileIcon, + onClick: () => handleTemplateClick(4), + isLoading: activeTemplateId === 4, + }, + { + title: "eNPS", + description: "Universal feedback to understand employee engagement and satisfaction.", + icon: UsersIcon, + onClick: () => handleTemplateClick(5), + isLoading: activeTemplateId === 5, + }, + ]; + + return ; +}; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils.ts b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils.ts new file mode 100644 index 0000000000..0b48a4479f --- /dev/null +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils.ts @@ -0,0 +1,13 @@ +import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates"; +import { TProduct } from "@formbricks/types/product"; +import { TXMTemplate } from "@formbricks/types/templates"; + +// replace all occurences of productName with the actual product name in the current template +export const replacePresetPlaceholders = (template: TXMTemplate, product: TProduct) => { + const survey = structuredClone(template); + survey.name = survey.name.replace("{{productName}}", product.name); + survey.questions = survey.questions.map((question) => { + return replaceQuestionPresetPlaceholders(question, product); + }); + return { ...template, ...survey }; +}; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts new file mode 100644 index 0000000000..5d326dc8cf --- /dev/null +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts @@ -0,0 +1,226 @@ +import { createId } from "@paralleldrive/cuid2"; +import { getDefaultEndingCard } from "@formbricks/lib/templates"; +import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; +import { TXMTemplate } from "@formbricks/types/templates"; + +export const XMSurveyDefault: TXMTemplate = { + name: "", + endings: [getDefaultEndingCard([])], + questions: [], + styling: { + overwriteThemeStyling: true, + }, +}; + +const NPSSurvey: TXMTemplate = { + ...XMSurveyDefault, + name: "NPS Survey", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.NPS, + headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" }, + required: true, + lowerLabel: { default: "Not at all likely" }, + upperLabel: { default: "Extremely likely" }, + isColorCodingEnabled: true, + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "To help us improve, can you describe the reason(s) for your rating?" }, + required: false, + inputType: "text", + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Any other comments, feedback, or concerns?" }, + required: false, + inputType: "text", + }, + ], +}; + +const StarRatingSurvey: TXMTemplate = { + ...XMSurveyDefault, + name: "{{productName}}'s Rating Survey", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.Rating, + logic: [{ value: 3, condition: "lessEqual", destination: "tk9wpw2gxgb8fa6pbpp3qq5l" }], + range: 5, + scale: "number", + headline: { default: "How do you like {{productName}}?" }, + required: true, + lowerLabel: { default: "Extremely dissatisfied" }, + upperLabel: { default: "Extremely satisfied" }, + isColorCodingEnabled: false, + }, + { + id: createId(), + html: { default: '

This helps us a lot.

' }, + type: TSurveyQuestionTypeEnum.CTA, + logic: [{ condition: "clicked", destination: XMSurveyDefault.endings[0].id }], + headline: { default: "Happy to hear 🙏 Please write a review for us!" }, + required: true, + buttonUrl: "https://formbricks.com/github", + buttonLabel: { default: "Write review" }, + buttonExternal: true, + }, + { + id: "tk9wpw2gxgb8fa6pbpp3qq5l", + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Sorry to hear! What is ONE thing we can do better?" }, + required: true, + subheader: { default: "Help us improve your experience." }, + buttonLabel: { default: "Send" }, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + ], +}; + +const CSATSurvey: TXMTemplate = { + ...XMSurveyDefault, + name: "{{productName}} CSAT", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.Rating, + logic: [{ value: 3, condition: "lessEqual", destination: "vyo4mkw4ln95ts4ya7qp2tth" }], + range: 5, + scale: "smiley", + headline: { default: "How satisfied are you with your {{productName}} experience?" }, + required: true, + lowerLabel: { default: "Extremely dissatisfied" }, + upperLabel: { default: "Extremely satisfied" }, + isColorCodingEnabled: false, + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + logic: [{ condition: "submitted", destination: XMSurveyDefault.endings[0].id }], + headline: { default: "Lovely! Is there anything we can do to improve your experience?" }, + required: false, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + { + id: "vyo4mkw4ln95ts4ya7qp2tth", + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Ugh, sorry! Is there anything we can do to improve your experience?" }, + required: false, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + ], +}; + +const CESSurvey: TXMTemplate = { + ...XMSurveyDefault, + name: "CES Survey", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.Rating, + range: 5, + scale: "number", + headline: { default: "{{productName}} makes it easy for me to [ADD GOAL]" }, + required: true, + lowerLabel: { default: "Disagree strongly" }, + upperLabel: { default: "Agree strongly" }, + isColorCodingEnabled: false, + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Thanks! How could we make it easier for you to [ADD GOAL]?" }, + required: true, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + ], +}; + +const SmileysRatingSurvey: TXMTemplate = { + ...XMSurveyDefault, + name: "Smileys Survey", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.Rating, + logic: [{ value: 3, condition: "lessEqual", destination: "tk9wpw2gxgb8fa6pbpp3qq5l" }], + range: 5, + scale: "smiley", + headline: { default: "How do you like {{productName}}?" }, + required: true, + lowerLabel: { default: "Not good" }, + upperLabel: { default: "Very satisfied" }, + isColorCodingEnabled: false, + }, + { + id: createId(), + html: { default: '

This helps us a lot.

' }, + type: TSurveyQuestionTypeEnum.CTA, + logic: [{ condition: "clicked", destination: XMSurveyDefault.endings[0].id }], + headline: { default: "Happy to hear 🙏 Please write a review for us!" }, + required: true, + buttonUrl: "https://formbricks.com/github", + buttonLabel: { default: "Write review" }, + buttonExternal: true, + }, + { + id: "tk9wpw2gxgb8fa6pbpp3qq5l", + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Sorry to hear! What is ONE thing we can do better?" }, + required: true, + subheader: { default: "Help us improve your experience." }, + buttonLabel: { default: "Send" }, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + ], +}; + +const eNPSSurvey: TXMTemplate = { + ...XMSurveyDefault, + name: "eNPS Survey", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.NPS, + headline: { + default: "How likely are you to recommend working at this company to a friend or colleague?", + }, + required: false, + lowerLabel: { default: "Not at all likely" }, + upperLabel: { default: "Extremely likely" }, + isColorCodingEnabled: true, + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "To help us improve, can you describe the reason(s) for your rating?" }, + required: false, + inputType: "text", + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Any other comments, feedback, or concerns?" }, + required: false, + inputType: "text", + }, + ], +}; + +export const XMTemplates: TXMTemplate[] = [ + NPSSurvey, + StarRatingSurvey, + CSATSurvey, + CESSurvey, + SmileysRatingSurvey, + eNPSSurvey, +]; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx new file mode 100644 index 0000000000..f9e01a92b4 --- /dev/null +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx @@ -0,0 +1,60 @@ +import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList"; +import { XIcon } from "lucide-react"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@formbricks/lib/authOptions"; +import { getEnvironment } from "@formbricks/lib/environment/service"; +import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils"; +import { getProductByEnvironmentId, getProducts } from "@formbricks/lib/product/service"; +import { getUser } from "@formbricks/lib/user/service"; +import { Button } from "@formbricks/ui/Button"; +import { Header } from "@formbricks/ui/Header"; + +interface XMTemplatePageProps { + params: { + environmentId: string; + }; +} + +const Page = async ({ params }: XMTemplatePageProps) => { + const session = await getServerSession(authOptions); + const environment = await getEnvironment(params.environmentId); + + if (!session) { + throw new Error("Session not found"); + } + + const user = await getUser(session.user.id); + if (!user) { + throw new Error("User not found"); + } + + if (!environment) { + throw new Error("Environment not found"); + } + + const organizationId = await getOrganizationIdFromEnvironmentId(environment.id); + + const product = await getProductByEnvironmentId(environment.id); + if (!product) { + throw new Error("Product not found"); + } + + const products = await getProducts(organizationId); + + return ( +
+
+ + {products.length >= 2 && ( + + )} +
+ ); +}; + +export default Page; diff --git a/apps/web/app/(app)/(onboarding)/lib/utils.ts b/apps/web/app/(app)/(onboarding)/lib/utils.ts index 5c1c975bca..3da1dc562d 100644 --- a/apps/web/app/(app)/(onboarding)/lib/utils.ts +++ b/apps/web/app/(app)/(onboarding)/lib/utils.ts @@ -1,6 +1,6 @@ import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product"; -export const getCustomHeadline = (channel: TProductConfigChannel, industry: TProductConfigIndustry) => { +export const getCustomHeadline = (channel?: TProductConfigChannel, industry?: TProductConfigIndustry) => { const combinations = { "website+eCommerce": "web shop", "website+saas": "landing page", diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/channel/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/channel/page.tsx index 9801863cd7..88132a642c 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/channel/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/channel/page.tsx @@ -17,14 +17,14 @@ const Page = async ({ params }: ChannelPageProps) => { description: "Run well-timed pop-up surveys.", icon: GlobeIcon, iconText: "Built for scale", - href: `/organizations/${params.organizationId}/products/new/industry?channel=website`, + href: `/organizations/${params.organizationId}/products/new/settings?channel=website`, }, { title: "App with sign up", description: "Run highly-targeted micro-surveys.", icon: GlobeLockIcon, iconText: "Enrich user profiles", - href: `/organizations/${params.organizationId}/products/new/industry?channel=app`, + href: `/organizations/${params.organizationId}/products/new/settings?channel=app`, }, { channel: "link", @@ -32,7 +32,7 @@ const Page = async ({ params }: ChannelPageProps) => { description: "Reach people anywhere online.", icon: LinkIcon, iconText: "Anywhere online", - href: `/organizations/${params.organizationId}/products/new/industry?channel=link`, + href: `/organizations/${params.organizationId}/products/new/settings?channel=link`, }, ]; diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/mode/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/mode/page.tsx new file mode 100644 index 0000000000..26db8c51a4 --- /dev/null +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/mode/page.tsx @@ -0,0 +1,47 @@ +import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; +import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react"; +import { getProducts } from "@formbricks/lib/product/service"; +import { Button } from "@formbricks/ui/Button"; +import { Header } from "@formbricks/ui/Header"; + +interface ModePageProps { + params: { + organizationId: string; + }; +} + +const Page = async ({ params }: ModePageProps) => { + const channelOptions = [ + { + title: "Formbricks Surveys", + description: "Multi-purpose survey platform for web, app and email surveys.", + icon: ListTodoIcon, + href: `/organizations/${params.organizationId}/products/new/channel`, + }, + { + title: "Formbricks CX", + description: "Surveys and reports to understand what your customers need.", + icon: HeartIcon, + href: `/organizations/${params.organizationId}/products/new/settings?mode=cx`, + }, + ]; + + const products = await getProducts(params.organizationId); + + return ( +
+
+ + {products.length >= 1 && ( + + )} +
+ ); +}; + +export default Page; diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings.tsx index b9b2788d4b..93703ba20c 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings.tsx @@ -13,6 +13,7 @@ import { PREVIEW_SURVEY } from "@formbricks/lib/styling/constants"; import { TProductConfigChannel, TProductConfigIndustry, + TProductMode, TProductUpdateInput, ZProductUpdateInput, } from "@formbricks/types/product"; @@ -32,6 +33,7 @@ import { SurveyInline } from "@formbricks/ui/Survey"; interface ProductSettingsProps { organizationId: string; + productMode: TProductMode; channel: TProductConfigChannel; industry: TProductConfigIndustry; defaultBrandColor: string; @@ -39,6 +41,7 @@ interface ProductSettingsProps { export const ProductSettings = ({ organizationId, + productMode, channel, industry, defaultBrandColor, @@ -68,10 +71,12 @@ export const ProductSettings = ({ localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS); } } - if (channel !== "link") { + if (channel === "app" || channel === "website") { router.push(`/environments/${productionEnvironment?.id}/connect`); - } else { + } else if (channel === "link") { router.push(`/environments/${productionEnvironment?.id}/surveys`); + } else if (productMode === "cx") { + router.push(`/environments/${productionEnvironment?.id}/xm-templates`); } } else { const errorMessage = getFormattedErrorMessage(createProductResponse); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx index bd8b758306..50104fbb99 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx @@ -1,11 +1,10 @@ import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils"; import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings"; import { XIcon } from "lucide-react"; -import { notFound } from "next/navigation"; import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants"; import { getProducts } from "@formbricks/lib/product/service"; import { startsWithVowel } from "@formbricks/lib/utils/strings"; -import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product"; +import { TProductConfigChannel, TProductConfigIndustry, TProductMode } from "@formbricks/types/product"; import { Button } from "@formbricks/ui/Button"; import { Header } from "@formbricks/ui/Header"; @@ -16,19 +15,21 @@ interface ProductSettingsPageProps { searchParams: { channel?: TProductConfigChannel; industry?: TProductConfigIndustry; + mode?: TProductMode; }; } const Page = async ({ params, searchParams }: ProductSettingsPageProps) => { - const channel = searchParams.channel; - const industry = searchParams.industry; - if (!channel || !industry) return notFound(); + const channel = searchParams.channel || null; + const industry = searchParams.industry || null; + const mode = searchParams.mode || "surveys"; + const customHeadline = getCustomHeadline(channel, industry); const products = await getProducts(params.organizationId); return (
- {channel === "link" ? ( + {channel === "link" || mode === "cx" ? (
{ )} & RefAttributes>; - iconText: string; - href: string; + iconText?: string; + href?: string; + onClick?: () => void; + isLoading?: boolean; }[]; } export const OnboardingOptionsContainer = ({ options }: OnboardingOptionsContainerProps) => { + const getOptionCard = (option) => { + const Icon = option.icon; + return ( + +
+ + {option.iconText && ( +

+ {option.iconText} +

+ )} +
+
+ ); + }; + return ( -
- {options.map((option, index) => { - const Icon = option.icon; - return ( - - -
- -

- {option.iconText} -

-
-
+
= 3, + "flex justify-center gap-8": options.length < 3, + })}> + {options.map((option) => + option.href ? ( + + {getOptionCard(option)} - ); - })} + ) : ( + getOptionCard(option) + ) + )}
); }; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx index 1a6d8ea474..fe73f85705 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx @@ -6,6 +6,7 @@ import { PlusIcon } from "lucide-react"; import { useState } from "react"; import { cn } from "@formbricks/lib/cn"; import { + CXQuestionTypes, getQuestionDefaults, questionTypes, universalQuestionPresets, @@ -15,11 +16,14 @@ import { TProduct } from "@formbricks/types/product"; interface AddQuestionButtonProps { addQuestion: (question: any) => void; product: TProduct; + isCxMode: boolean; } -export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonProps) => { +export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestionButtonProps) => { const [open, setOpen] = useState(false); + const availableQuestionTypes = isCxMode ? CXQuestionTypes : questionTypes; + return ( {/*
*/} - {questionTypes.map((questionType) => ( + {availableQuestionTypes.map((questionType) => (
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx index 87e54f2ce1..234176dd69 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx @@ -20,6 +20,7 @@ interface QuestionsDraggableProps { attributeClasses: TAttributeClass[]; addQuestion: (question: any, index?: number) => void; isFormbricksCloud: boolean; + isCxMode: boolean; } export const QuestionsDroppable = ({ @@ -38,6 +39,7 @@ export const QuestionsDroppable = ({ attributeClasses, addQuestion, isFormbricksCloud, + isCxMode, }: QuestionsDraggableProps) => { return (
@@ -62,6 +64,7 @@ export const QuestionsDroppable = ({ attributeClasses={attributeClasses} addQuestion={addQuestion} isFormbricksCloud={isFormbricksCloud} + isCxMode={isCxMode} /> ))} diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx index c2e710a9aa..d0cbfbc344 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx @@ -31,12 +31,14 @@ interface QuestionsAudienceTabsProps { activeId: TSurveyEditorTabs; setActiveId: React.Dispatch>; isStylingTabVisible?: boolean; + isCxMode: boolean; } export const QuestionsAudienceTabs = ({ activeId, setActiveId, isStylingTabVisible, + isCxMode, }: QuestionsAudienceTabsProps) => { const tabsComputed = useMemo(() => { if (isStylingTabVisible) { @@ -45,10 +47,13 @@ export const QuestionsAudienceTabs = ({ return tabs.filter((tab) => tab.id !== "styling"); }, [isStylingTabVisible]); + // Hide settings tab in CX mode + let tabsToDisplay = isCxMode ? tabsComputed.filter((tab) => tab.id !== "settings") : tabsComputed; + return (
- + )} { const [activeView, setActiveView] = useState("questions"); const [activeQuestionId, setActiveQuestionId] = useState(null); @@ -144,6 +146,7 @@ export const SurveyEditor = ({ responseCount={responseCount} selectedLanguageCode={selectedLanguageCode} setSelectedLanguageCode={setSelectedLanguageCode} + isCxMode={isCxMode} />
@@ -170,6 +174,7 @@ export const SurveyEditor = ({ isFormbricksCloud={isFormbricksCloud} attributeClasses={attributeClasses} plan={plan} + isCxMode={isCxMode} /> )} @@ -185,6 +190,7 @@ export const SurveyEditor = ({ localStylingChanges={localStylingChanges} setLocalStylingChanges={setLocalStylingChanges} isUnsplashConfigured={isUnsplashConfigured} + isCxMode={isCxMode} /> )} diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar.tsx index a55efa2da1..2115ba09dd 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar.tsx @@ -39,6 +39,7 @@ interface SurveyMenuBarProps { responseCount: number; selectedLanguageCode: string; setSelectedLanguageCode: (selectedLanguage: string) => void; + isCxMode: boolean; } export const SurveyMenuBar = ({ @@ -52,6 +53,7 @@ export const SurveyMenuBar = ({ product, responseCount, selectedLanguageCode, + isCxMode, }: SurveyMenuBarProps) => { const router = useRouter(); const [audiencePrompt, setAudiencePrompt] = useState(true); @@ -305,16 +307,18 @@ export const SurveyMenuBar = ({ <>
- + {!isCxMode && ( + + )}

{product.name} /

- + {!isCxMode && ( + + )} + {localSurvey.status !== "draft" && ( )}
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx index 5bbf88b14a..bc43c1ef2d 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx @@ -22,7 +22,7 @@ export const generateMetadata = async ({ params }) => { }; }; -const Page = async ({ params }) => { +const Page = async ({ params, searchParams }) => { const [ survey, product, @@ -70,6 +70,8 @@ const Page = async ({ params }) => { return ; } + const isCxMode = searchParams.mode === "cx"; + return ( { plan={organization.billing.plan} isFormbricksCloud={IS_FORMBRICKS_CLOUD} isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false} + isCxMode={isCxMode} /> ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx index a76dbd38cf..5f834cfc04 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx @@ -169,7 +169,7 @@ export const MainNavigation = ({ }; const handleAddProduct = (organizationId: string) => { - router.push(`/organizations/${organizationId}/products/new/channel`); + router.push(`/organizations/${organizationId}/products/new/mode`); }; const mainNavigation = useMemo( diff --git a/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx b/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx index 26a2ffb72f..4bc9d0a60d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx @@ -15,7 +15,7 @@ export const WidgetStatusIndicator = ({ environment, size, type }: WidgetStatusI notImplemented: { icon: AlertTriangleIcon, title: `Your ${type} is not yet connected.`, - subtitle: `Connect your ${type} with Formbricks to get started. To run ${type === "app" ? "in-app" : "website"} surveys follow the setup guide.`, + subtitle: ``, shortText: `Connect your ${type} with Formbricks`, }, running: { diff --git a/apps/web/app/(app)/environments/[environmentId]/product/(setup)/components/SetupInstructions.tsx b/apps/web/app/(app)/environments/[environmentId]/product/(setup)/components/SetupInstructions.tsx index efe62cd8e7..3a91fe3bdf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/(setup)/components/SetupInstructions.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/(setup)/components/SetupInstructions.tsx @@ -23,7 +23,7 @@ interface SetupInstructionsProps { } export const SetupInstructions = ({ environmentId, webAppUrl, type }: SetupInstructionsProps) => { - const [activeTab, setActiveTab] = useState(tabs[0].id); + const [activeTab, setActiveTab] = useState(type === "website" ? tabs[1].id : tabs[0].id); return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/product/(setup)/website-connection/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/(setup)/website-connection/page.tsx index bd212f0b7d..acd3e25b7a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/(setup)/website-connection/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/(setup)/website-connection/page.tsx @@ -52,17 +52,17 @@ const Page = async ({ params }) => { description="Check if your website is successfully connected with Formbricks. Reload page to recheck."> {environment && } - - - + + +
); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey.tsx index 28cfea013b..f31c0c5edf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey.tsx @@ -1,6 +1,14 @@ "use client"; -import { BellRing, BlocksIcon, Code2Icon, LinkIcon, MailIcon, UsersRound } from "lucide-react"; +import { + BellRing, + BlocksIcon, + Code2Icon, + LinkIcon, + MailIcon, + SmartphoneIcon, + UsersRound, +} from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -27,9 +35,10 @@ export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: Sha const { email } = user; const tabs = [ - { id: "email", label: "Embed in an Email", icon: MailIcon }, - { id: "webpage", label: "Embed in a Web Page", icon: Code2Icon }, - { id: "link", label: `${isSingleUseLinkSurvey ? "Single Use Links" : "Share the Link"}`, icon: LinkIcon }, + { id: "email", label: "Embed in an email", icon: MailIcon }, + { id: "webpage", label: "Embed on website", icon: Code2Icon }, + { id: "link", label: `${isSingleUseLinkSurvey ? "Single use links" : "Share the link"}`, icon: LinkIcon }, + { id: "app", label: "Embed in app", icon: SmartphoneIcon }, ]; const [activeId, setActiveId] = useState(tabs[0].id); @@ -77,7 +86,7 @@ export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: Sha Embed survey Configure alerts @@ -104,6 +113,7 @@ export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: Sha handleInitialPageButton={handleInitialPageButton} tabs={tabs} activeId={activeId} + environmentId={environmentId} setActiveId={setActiveId} survey={survey} email={email} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx new file mode 100644 index 0000000000..3e6f9a152d --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx @@ -0,0 +1,96 @@ +"use client"; + +import ChangeSurveyTypeTip from "@/images/tooltips/change-survey-type-app.mp4"; +import { CogIcon } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import { Alert, AlertDescription } from "@formbricks/ui/Alert"; +import { OptionsSwitch } from "@formbricks/ui/OptionsSwitch"; + +export const AppTab = ({ environmentId }) => { + const [selectedTab, setSelectedTab] = useState("webapp"); + + return ( +
+ setSelectedTab(value)} + /> + +
+ {selectedTab === "webapp" ? : } +
+
+ ); +}; + +const MobileAppTab = () => { + return ( +
+

How to embed a survey on your React Native app

+
    +
  1. + Follow the{" "} + + setup instructions for React Native apps + {" "} + to connect your app with Formbricks +
  2. +
+ + + +
We're working on SDKs for Flutter, Swift and Kotlin.
+
+
+
+ ); +}; + +const WebAppTab = ({ environmentId }) => { + return ( +
+

How to embed a survey on your web app

+
    +
  1. + Follow these{" "} + + setup instructions + {" "} + to connect your web app with Formbricks +
  2. +
  3. + Learn how to{" "} + + identify users and set attrubutes + {" "} + to run highly targeted surveys. +
  4. +
  5. + Make sure your survey type is set to App survey + pop up +
  6. +
  7. Dfine when and where the survey should pop up
  8. +
+
+ +
+
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/EmbedView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/EmbedView.tsx index 2015af8ec0..e9a385f4c3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/EmbedView.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/EmbedView.tsx @@ -3,15 +3,17 @@ import { ArrowLeftIcon } from "lucide-react"; import { cn } from "@formbricks/lib/cn"; import { Button } from "@formbricks/ui/Button"; +import { AppTab } from "./AppTab"; import { EmailTab } from "./EmailTab"; import { LinkTab } from "./LinkTab"; -import { WebpageTab } from "./WebpageTab"; +import { WebsiteTab } from "./WebsiteTab"; interface EmbedViewProps { handleInitialPageButton: () => void; tabs: Array<{ id: string; label: string; icon: any }>; activeId: string; setActiveId: React.Dispatch>; + environmentId: string; survey: any; email: string; surveyUrl: string; @@ -24,6 +26,7 @@ export const EmbedView = ({ tabs, activeId, setActiveId, + environmentId, survey, email, surveyUrl, @@ -67,7 +70,7 @@ export const EmbedView = ({ {activeId === "email" ? ( ) : activeId === "webpage" ? ( - + ) : activeId === "link" ? ( + ) : activeId === "app" ? ( + ) : null}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebpageTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebpageTab.tsx deleted file mode 100644 index f7973ffa41..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebpageTab.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { CopyIcon } from "lucide-react"; -import { useState } from "react"; -import toast from "react-hot-toast"; -import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle"; -import { Button } from "@formbricks/ui/Button"; -import { CodeBlock } from "@formbricks/ui/CodeBlock"; - -export const WebpageTab = ({ surveyUrl }) => { - const [embedModeEnabled, setEmbedModeEnabled] = useState(false); - const iframeCode = `
- -
`; - - return ( -
-
-
- -
-
- - {iframeCode} - -
-
- -
-
- ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx new file mode 100644 index 0000000000..f859b9e3bf --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx @@ -0,0 +1,112 @@ +"use client"; + +import ChangeSurveyTypeTip from "@/images/tooltips/change-survey-type.mp4"; +import { CopyIcon } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle"; +import { Button } from "@formbricks/ui/Button"; +import { CodeBlock } from "@formbricks/ui/CodeBlock"; +import { OptionsSwitch } from "@formbricks/ui/OptionsSwitch"; + +export const WebsiteTab = ({ surveyUrl, environmentId }) => { + const [selectedTab, setSelectedTab] = useState("static"); + + return ( +
+ setSelectedTab(value)} + /> + +
+ {selectedTab === "static" ? ( + + ) : ( + + )} +
+
+ ); +}; + +const StaticTab = ({ surveyUrl }) => { + const [embedModeEnabled, setEmbedModeEnabled] = useState(false); + const iframeCode = `
+ +
`; + + return ( +
+
+
+ +
+
+ + {iframeCode} + +
+
+ +
+
+ ); +}; + +const PopupTab = ({ environmentId }) => { + return ( +
+

How to embed a pop-up survey on your website

+
    +
  1. + Follow these{" "} + + setup instructions + {" "} + to connect your website with Formbricks +
  2. +
  3. + Make sure the survey type is set to Website survey +
  4. +
  5. Dfine when and where the survey should pop up
  6. +
+
+ +
+
+ ); +}; diff --git a/apps/web/app/(auth)/auth/verification-requested/page.tsx b/apps/web/app/(auth)/auth/verification-requested/page.tsx index 903bd29365..1260f31631 100644 --- a/apps/web/app/(auth)/auth/verification-requested/page.tsx +++ b/apps/web/app/(auth)/auth/verification-requested/page.tsx @@ -4,8 +4,8 @@ import { z } from "zod"; const VerificationPageSchema = z.string().email(); -const Page = (params) => { - const email = params.searchParams.email; +const Page = ({ searchParams }) => { + const email = searchParams.email; try { const parsedEmail = VerificationPageSchema.parse(email).toLowerCase(); return ( diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 787e4c3e4d..f167fac04a 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -39,7 +39,7 @@ const Page = async () => { if (!environment) { console.error("Failed to get first environment of user"); - return redirect(`/organizations/${userOrganizations[0].id}/products/new/channel`); + return redirect(`/organizations/${userOrganizations[0].id}/products/new/mode`); } return redirect(`/environments/${environment.id}`); diff --git a/apps/web/images/tooltips/change-survey-type-app.mp4 b/apps/web/images/tooltips/change-survey-type-app.mp4 new file mode 100644 index 0000000000..4653954a46 Binary files /dev/null and b/apps/web/images/tooltips/change-survey-type-app.mp4 differ diff --git a/apps/web/images/tooltips/change-survey-type.mp4 b/apps/web/images/tooltips/change-survey-type.mp4 new file mode 100644 index 0000000000..0ba37b9a79 Binary files /dev/null and b/apps/web/images/tooltips/change-survey-type.mp4 differ diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index aee60ecda0..5545d63325 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,4 +1,3 @@ -import { createId } from "@paralleldrive/cuid2"; import { withSentryConfig } from "@sentry/nextjs"; import createJiti from "jiti"; import { createRequire } from "node:module"; @@ -91,6 +90,22 @@ const nextConfig = { }, ]; }, + webpack: (config) => { + config.module.rules.push({ + test: /\.(mp4|webm|ogg|swf|ogv)$/, + use: [ + { + loader: "file-loader", + options: { + publicPath: "/_next/static/videos/", + outputPath: "static/videos/", + name: "[name].[hash].[ext]", + }, + }, + ], + }); + return config; + }, async headers() { return [ { diff --git a/apps/web/package.json b/apps/web/package.json index c20eabcfeb..62086b73b6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -39,6 +39,7 @@ "bcryptjs": "^2.4.3", "dotenv": "^16.4.5", "encoding": "^0.1.13", + "file-loader": "^6.2.0", "framer-motion": "11.3.28", "googleapis": "^140.0.1", "jiti": "^1.21.6", diff --git a/apps/web/playwright/onboarding.spec.ts b/apps/web/playwright/onboarding.spec.ts index 6f5c583488..ad3d42882f 100644 --- a/apps/web/playwright/onboarding.spec.ts +++ b/apps/web/playwright/onboarding.spec.ts @@ -9,10 +9,11 @@ test.describe("Onboarding Flow Test", async () => { const user = await users.create({ withoutProduct: true }); await user.login(); - await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/channel/); + await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/mode/); + await page.getByRole("button", { name: "Formbricks Surveys Multi-" }).click(); await page.getByRole("button", { name: "Anywhere online Link" }).click(); - await page.getByRole("button", { name: "B2B and B2C E-Commerce" }).click(); + // await page.getByRole("button", { name: "B2B and B2C E-Commerce" }).click(); await page.getByPlaceholder("e.g. Formbricks").click(); await page.getByPlaceholder("e.g. Formbricks").fill(productName); await page.locator("form").filter({ hasText: "Brand colorMatch the main" }).getByRole("button").click(); @@ -25,10 +26,11 @@ test.describe("Onboarding Flow Test", async () => { const user = await users.create({ withoutProduct: true }); await user.login(); - await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/channel/); + await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/mode/); + await page.getByRole("button", { name: "Formbricks Surveys Multi-" }).click(); await page.getByRole("button", { name: "Enrich user profiles App with" }).click(); - await page.getByRole("button", { name: "B2B and B2C E-Commerce" }).click(); + // await page.getByRole("button", { name: "B2B and B2C E-Commerce" }).click(); await page.getByPlaceholder("e.g. Formbricks").click(); await page.getByPlaceholder("e.g. Formbricks").fill(productName); await page.locator("form").filter({ hasText: "Brand colorMatch the main" }).getByRole("button").click(); @@ -40,3 +42,23 @@ test.describe("Onboarding Flow Test", async () => { await expect(page.getByText(productName)).toBeVisible(); }); }); + +test.describe("CX Onboarding", async () => { + test("first survey creation", async ({ page, users }) => { + const user = await users.create({ withoutProduct: true }); + await user.login(); + + await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/mode/); + await page.getByRole("button", { name: "Formbricks CX Surveys and" }).click(); + + await page.getByPlaceholder("e.g. Formbricks").click(); + await page.getByPlaceholder("e.g. Formbricks").fill(productName); + await page.locator("form").filter({ hasText: "Brand colorMatch the main" }).getByRole("button").click(); + await page.getByRole("button", { name: "NPS Implement proven best" }).click(); + + await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/edit(\?.*)mode=cx$/); + await page.getByRole("button", { name: "Save & Close" }).click(); + + await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary(\?.*)?$/); + }); +}); diff --git a/apps/web/playwright/utils/helper.ts b/apps/web/playwright/utils/helper.ts index e1fbd141a1..97f3112839 100644 --- a/apps/web/playwright/utils/helper.ts +++ b/apps/web/playwright/utils/helper.ts @@ -79,7 +79,9 @@ export const finishOnboarding = async ( page: Page, ProductChannel: TProductConfigChannel = "website" ): Promise => { - await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/channel/); + await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/mode/); + + await page.getByRole("button", { name: "Formbricks Surveys Multi-" }).click(); if (ProductChannel === "website") { await page.getByRole("button", { name: "Built for scale Public website" }).click(); @@ -89,7 +91,7 @@ export const finishOnboarding = async ( await page.getByRole("button", { name: "Anywhere online Link" }).click(); } - await page.getByRole("button", { name: "Proven methods SaaS" }).click(); + // await page.getByRole("button", { name: "Proven methods SaaS" }).click(); await page.getByPlaceholder("e.g. Formbricks").click(); await page.getByPlaceholder("e.g. Formbricks").fill("My Product"); await page.locator("form").filter({ hasText: "Brand colorMatch the main" }).getByRole("button").click(); diff --git a/packages/lib/templates.ts b/packages/lib/templates.ts index 672c45ec41..b05a98f96b 100644 --- a/packages/lib/templates.ts +++ b/packages/lib/templates.ts @@ -41,7 +41,7 @@ export const welcomeCardDefault: TSurveyWelcomeCard = { showResponseCount: false, }; -const surveyDefault: TTemplate["preset"] = { +export const surveyDefault: TTemplate["preset"] = { name: "New Survey", welcomeCard: welcomeCardDefault, endings: [getDefaultEndingCard([])], @@ -1472,7 +1472,6 @@ export const templates: TTemplate[] = [ { name: "Net Promoter Score (NPS)", - role: "customerSuccess", industries: ["saas", "eCommerce", "other"], channels: ["app", "link", "website"], diff --git a/packages/lib/utils/questions.tsx b/packages/lib/utils/questions.tsx index 433bf1a8b0..e9fb1966f4 100644 --- a/packages/lib/utils/questions.tsx +++ b/packages/lib/utils/questions.tsx @@ -229,6 +229,18 @@ export const questionTypes: TQuestion[] = [ }, ]; +export const CXQuestionTypes = questionTypes.filter((questionType) => { + return [ + TSurveyQuestionTypeEnum.OpenText, + TSurveyQuestionTypeEnum.MultipleChoiceSingle, + TSurveyQuestionTypeEnum.MultipleChoiceMulti, + TSurveyQuestionTypeEnum.Rating, + TSurveyQuestionTypeEnum.NPS, + TSurveyQuestionTypeEnum.Consent, + TSurveyQuestionTypeEnum.CTA, + ].includes(questionType.id as TSurveyQuestionTypeEnum); +}); + export const QUESTIONS_ICON_MAP: Record = questionTypes.reduce( (prev, curr) => ({ ...prev, @@ -245,6 +257,14 @@ export const QUESTIONS_NAME_MAP = questionTypes.reduce( {} ) as Record; +export const CX_QUESTIONS_NAME_MAP = CXQuestionTypes.reduce( + (prev, curr) => ({ + ...prev, + [curr.id]: curr.label, + }), + {} +) as Record; + export const universalQuestionPresets = { required: true, }; diff --git a/packages/types/product.ts b/packages/types/product.ts index a14ee4be84..e22bb0a22d 100644 --- a/packages/types/product.ts +++ b/packages/types/product.ts @@ -15,6 +15,9 @@ export type TProductConfigIndustry = z.infer; export const ZProductConfigChannel = z.enum(["link", "app", "website"]).nullable(); export type TProductConfigChannel = z.infer; +export const ZProductMode = z.enum(["surveys", "cx"]); +export type TProductMode = z.infer; + export const ZProductConfig = z.object({ channel: ZProductConfigChannel, industry: ZProductConfigIndustry, diff --git a/packages/types/templates.ts b/packages/types/templates.ts index ffe6ee8596..720de8f11a 100644 --- a/packages/types/templates.ts +++ b/packages/types/templates.ts @@ -1,6 +1,12 @@ import { z } from "zod"; import { ZProductConfigChannel, ZProductConfigIndustry } from "./product"; -import { ZSurveyEndings, ZSurveyHiddenFields, ZSurveyQuestions, ZSurveyWelcomeCard } from "./surveys/types"; +import { + ZSurveyEndings, + ZSurveyHiddenFields, + ZSurveyQuestions, + ZSurveyStyling, + ZSurveyWelcomeCard, +} from "./surveys/types"; import { ZUserObjective } from "./user"; export const ZTemplateRole = z.enum(["productManager", "customerSuccess", "marketing", "sales"]); @@ -25,6 +31,15 @@ export const ZTemplate = z.object({ export type TTemplate = z.infer; +export const ZXMTemplate = z.object({ + name: z.string(), + questions: ZSurveyQuestions, + endings: ZSurveyEndings, + styling: ZSurveyStyling, +}); + +export type TXMTemplate = z.infer; + export const ZTemplateFilter = z.union([ ZProductConfigChannel, ZProductConfigIndustry, diff --git a/packages/types/video.d.ts b/packages/types/video.d.ts new file mode 100644 index 0000000000..19960d5e17 --- /dev/null +++ b/packages/types/video.d.ts @@ -0,0 +1,4 @@ +declare module "*.mp4" { + const src: string; + export default src; +} diff --git a/packages/ui/Header/index.tsx b/packages/ui/Header/index.tsx index d16a6b7cd6..0255e90f5b 100644 --- a/packages/ui/Header/index.tsx +++ b/packages/ui/Header/index.tsx @@ -2,14 +2,14 @@ import React from "react"; interface HeaderProps { title: string; - subtitle: string; + subtitle?: string; } export const Header: React.FC = ({ title, subtitle }) => { return (

{title}

-

{subtitle}

+ {subtitle &&

{subtitle}

}
); }; diff --git a/packages/ui/OptionCard/index.tsx b/packages/ui/OptionCard/index.tsx index 4d72e56abe..86e5c8e005 100644 --- a/packages/ui/OptionCard/index.tsx +++ b/packages/ui/OptionCard/index.tsx @@ -12,9 +12,9 @@ interface PathwayOptionProps { } const sizeClasses = { - sm: "rounded-lg border border-slate-200 shadow-card-sm transition-all duration-150", - md: "rounded-xl border border-slate-200 shadow-card-md transition-all duration-300", - lg: "rounded-2xl border border-slate-200 shadow-card-lg transition-all duration-500", + sm: "rounded-lg max-w-xs border border-slate-200 shadow-card-sm transition-all duration-150", + md: "rounded-xl max-w-xs border border-slate-200 shadow-card-md transition-all duration-300", + lg: "rounded-2xl max-w-sm border border-slate-200 shadow-card-lg transition-all duration-500", }; export const OptionCard: React.FC = ({ @@ -35,9 +35,9 @@ export const OptionCard: React.FC = ({ tabIndex={0}>
{children} -
+

{title}

-

{description}

+

{description}

diff --git a/packages/ui/OptionsSwitch/index.tsx b/packages/ui/OptionsSwitch/index.tsx index e2fbbb277a..2743b25a18 100644 --- a/packages/ui/OptionsSwitch/index.tsx +++ b/packages/ui/OptionsSwitch/index.tsx @@ -1,40 +1,62 @@ -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; interface TOption { value: string; label: string; icon?: React.ReactNode; + disabled?: boolean; // Add disabled property to individual options } interface OptionsSwitchProps { options: TOption[]; currentOption: string | undefined; handleOptionChange: (value: string) => void; - disabled?: boolean; } export const OptionsSwitch = ({ options: questionTypes, currentOption, handleOptionChange, - disabled = false, }: OptionsSwitchProps) => { + const [highlightStyle, setHighlightStyle] = useState({}); + const containerRef = useRef(null); + + useEffect(() => { + if (containerRef.current) { + const activeElement = containerRef.current.querySelector(`[data-value="${currentOption}"]`); + if (activeElement) { + const { offsetLeft, offsetWidth } = activeElement as HTMLElement; + setHighlightStyle({ + left: `${offsetLeft}px`, + width: `${offsetWidth}px`, + }); + } + } + }, [currentOption]); + return ( -
+
+
{questionTypes.map((type) => (
!disabled && handleOptionChange(type.value)} - className={`flex-grow cursor-pointer rounded-md bg-${ - (currentOption === undefined && type.value === "text") || currentOption === type.value - ? "slate-100" - : "white" - } p-2 text-center ${disabled ? "cursor-not-allowed" : ""}`}> + data-value={type.value} + onClick={() => !type.disabled && handleOptionChange(type.value)} + className={`relative z-10 flex-grow rounded-md p-2 text-center transition-colors duration-200 ${ + type.disabled + ? "cursor-not-allowed opacity-50" + : currentOption === type.value + ? "" + : "cursor-pointer hover:bg-slate-50" + }`}>
{type.label} - {type.icon ? ( -
{type.icon}
- ) : null} + {type.icon &&
{type.icon}
}
))} diff --git a/packages/ui/ShareSurveyLink/components/SurveyLinkDisplay.tsx b/packages/ui/ShareSurveyLink/components/SurveyLinkDisplay.tsx index e0343f1121..c980f1a258 100644 --- a/packages/ui/ShareSurveyLink/components/SurveyLinkDisplay.tsx +++ b/packages/ui/ShareSurveyLink/components/SurveyLinkDisplay.tsx @@ -8,7 +8,7 @@ export const SurveyLinkDisplay = ({ surveyUrl }: SurveyLinkDisplayProps) => { return ( ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5972819229..b5da292668 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -411,7 +411,7 @@ importers: version: 0.0.22(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: ^8.26.0 - version: 8.26.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.93.0) + version: 8.26.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.93.0) '@tanstack/react-table': specifier: ^8.20.1 version: 8.20.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -420,7 +420,7 @@ importers: version: 0.6.2 '@vercel/speed-insights': specifier: ^1.0.12 - version: 1.0.12(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.0.12(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -430,6 +430,9 @@ importers: encoding: specifier: ^0.1.13 version: 0.1.13 + file-loader: + specifier: ^6.2.0 + version: 6.2.0(webpack@5.93.0) framer-motion: specifier: 11.3.28 version: 11.3.28(@emotion/is-prop-valid@0.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -456,10 +459,10 @@ importers: version: 4.0.4 next: specifier: 14.2.5 - version: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-safe-action: specifier: ^7.6.2 - version: 7.7.0(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.23.8) + version: 7.7.0(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.23.8) optional: specifier: ^0.1.4 version: 0.1.4 @@ -511,7 +514,7 @@ importers: version: link:../../packages/config-eslint '@neshca/cache-handler': specifier: ^1.5.1 - version: 1.5.1(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(redis@4.7.0) + version: 1.5.1(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(redis@4.7.0) '@types/bcryptjs': specifier: ^2.4.6 version: 2.4.6 @@ -619,7 +622,7 @@ importers: version: 8.4.41 tailwindcss: specifier: ^3.4.10 - version: 3.4.10(ts-node@10.9.2) + version: 3.4.10(ts-node@10.9.2(@types/node@22.3.0)(typescript@5.5.4)) packages/config-typescript: devDependencies: @@ -1016,7 +1019,7 @@ importers: version: 14.2.3 tailwindcss: specifier: ^3.4.10 - version: 3.4.10(ts-node@10.9.2) + version: 3.4.10(ts-node@10.9.2(@types/node@22.3.0)(typescript@5.5.4)) terser: specifier: ^5.31.6 version: 5.31.6 @@ -8208,6 +8211,12 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-loader@6.2.0: + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + filesize@10.1.2: resolution: {integrity: sha512-Dx770ai81ohflojxhU+oG+Z2QGvKdYxgEr9OSA8UVrqhwNHjfH9A8f5NKfg83fEH8ZFA5N5llJo5T3PIoZ4CRA==} engines: {node: '>= 10.4.0'} @@ -16577,11 +16586,11 @@ snapshots: '@microsoft/tsdoc@0.14.2': {} - '@neshca/cache-handler@1.5.1(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(redis@4.7.0)': + '@neshca/cache-handler@1.5.1(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(redis@4.7.0)': dependencies: cluster-key-slot: 1.1.2 lru-cache: 10.3.0 - next: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) redis: 4.7.0 '@next/env@13.5.6': {} @@ -18931,7 +18940,7 @@ snapshots: '@sentry/types': 8.26.0 '@sentry/utils': 8.26.0 - '@sentry/nextjs@8.26.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.93.0)': + '@sentry/nextjs@8.26.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.93.0)': dependencies: '@opentelemetry/instrumentation-http': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 @@ -18945,7 +18954,7 @@ snapshots: '@sentry/vercel-edge': 8.26.0 '@sentry/webpack-plugin': 2.20.1(encoding@0.1.13)(webpack@5.93.0) chalk: 3.0.0 - next: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) resolve: 1.22.8 rollup: 3.29.4 stacktrace-parser: 0.1.10 @@ -19806,7 +19815,7 @@ snapshots: '@tailwindcss/forms@0.5.7(tailwindcss@3.4.10(ts-node@10.9.2))': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.10(ts-node@10.9.2) + tailwindcss: 3.4.10(ts-node@10.9.2(@types/node@22.3.0)(typescript@5.5.4)) '@tailwindcss/typography@0.5.13(tailwindcss@3.4.7(ts-node@10.9.2(typescript@5.5.4)))': dependencies: @@ -19822,7 +19831,7 @@ snapshots: lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.10(ts-node@10.9.2) + tailwindcss: 3.4.10(ts-node@10.9.2(@types/node@22.3.0)(typescript@5.5.4)) '@tanstack/react-table@8.20.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -20468,9 +20477,9 @@ snapshots: satori: 0.10.9 yoga-wasm-web: 0.3.3 - '@vercel/speed-insights@1.0.12(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/speed-insights@1.0.12(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': optionalDependencies: - next: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 '@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.5)(eslint@8.57.0)(prettier@3.3.3)(typescript@5.5.4)(vitest@2.0.5)': @@ -23160,6 +23169,12 @@ snapshots: dependencies: flat-cache: 3.2.0 + file-loader@6.2.0(webpack@5.93.0): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.93.0 + filesize@10.1.2: {} fill-range@7.1.1: @@ -25747,9 +25762,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next-safe-action@7.7.0(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.23.8): + next-safe-action@7.7.0(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.23.8): dependencies: - next: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: @@ -25800,33 +25815,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@next/env': 14.2.5 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 - caniuse-lite: 1.0.30001636 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.25.2)(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 14.2.5 - '@next/swc-darwin-x64': 14.2.5 - '@next/swc-linux-arm64-gnu': 14.2.5 - '@next/swc-linux-arm64-musl': 14.2.5 - '@next/swc-linux-x64-gnu': 14.2.5 - '@next/swc-linux-x64-musl': 14.2.5 - '@next/swc-win32-arm64-msvc': 14.2.5 - '@next/swc-win32-ia32-msvc': 14.2.5 - '@next/swc-win32-x64-msvc': 14.2.5 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.45.3 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.5 @@ -26505,7 +26493,7 @@ snapshots: postcss: 8.4.40 ts-node: 10.9.2(@swc/core@1.3.101)(@types/node@22.3.0)(typescript@5.5.4) - postcss-load-config@4.0.2(postcss@8.4.41)(ts-node@10.9.2): + postcss-load-config@4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@22.3.0)(typescript@5.5.4)): dependencies: lilconfig: 3.1.2 yaml: 2.4.5 @@ -28271,13 +28259,6 @@ snapshots: optionalDependencies: '@babel/core': 7.24.5 - styled-jsx@5.1.1(@babel/core@7.25.2)(react@18.3.1): - dependencies: - client-only: 0.0.1 - react: 18.3.1 - optionalDependencies: - '@babel/core': 7.25.2 - styled-jsx@5.1.1(react@19.0.0-rc-935180c7e0-20240524): dependencies: client-only: 0.0.1 @@ -28373,7 +28354,7 @@ snapshots: postcss: 8.4.41 postcss-import: 15.1.0(postcss@8.4.41) postcss-js: 4.0.1(postcss@8.4.41) - postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2) + postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@22.3.0)(typescript@5.5.4)) postcss-nested: 6.0.1(postcss@8.4.41) postcss-selector-parser: 6.1.0 resolve: 1.22.8 @@ -28381,7 +28362,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.10(ts-node@10.9.2): + tailwindcss@3.4.10(ts-node@10.9.2(@types/node@22.3.0)(typescript@5.5.4)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -28400,7 +28381,7 @@ snapshots: postcss: 8.4.41 postcss-import: 15.1.0(postcss@8.4.41) postcss-js: 4.0.1(postcss@8.4.41) - postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2) + postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@22.3.0)(typescript@5.5.4)) postcss-nested: 6.0.1(postcss@8.4.41) postcss-selector-parser: 6.1.0 resolve: 1.22.8