feat: XM template filters (#2745)

Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
Dhruwang Jariwala
2024-06-19 13:49:45 +05:30
committed by GitHub
parent cd40e655fb
commit a473719eee
16 changed files with 862 additions and 339 deletions

View File

@@ -4,8 +4,8 @@ import { MenuBar } from "@/app/(app)/(survey-editor)/environments/[environmentId
import { useState } from "react";
import { customSurvey } from "@formbricks/lib/templates";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct } from "@formbricks/types/product";
import type { TTemplate } from "@formbricks/types/templates";
import type { TProduct, TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
import { PreviewSurvey } from "@formbricks/ui/PreviewSurvey";
import { SearchBox } from "@formbricks/ui/SearchBox";
@@ -17,13 +17,14 @@ type TemplateContainerWithPreviewProps = {
product: TProduct;
environment: TEnvironment;
user: TUser;
prefilledFilters: (TProductConfigChannel | TProductConfigIndustry | TTemplateRole | null)[];
};
export const TemplateContainerWithPreview = ({
environmentId,
product,
environment,
user,
prefilledFilters,
}: TemplateContainerWithPreviewProps) => {
const initialTemplate = customSurvey;
const [activeTemplate, setActiveTemplate] = useState<TTemplate>(initialTemplate);
@@ -35,7 +36,7 @@ export const TemplateContainerWithPreview = ({
<MenuBar />
<div className="relative z-0 flex flex-1 overflow-hidden">
<div className="flex-1 flex-col overflow-auto bg-slate-50">
<div className="ml-6 mt-6 flex flex-col items-center justify-between md:flex-row md:items-start">
<div className="mb-3 ml-6 mt-6 flex flex-col items-center justify-between md:flex-row md:items-end">
<h1 className="text-2xl font-bold text-slate-800">Create a new survey</h1>
<div className="px-6">
<SearchBox
@@ -51,7 +52,6 @@ export const TemplateContainerWithPreview = ({
</div>
<TemplateList
environmentId={environmentId}
environment={environment}
product={product}
user={user}
@@ -60,6 +60,7 @@ export const TemplateContainerWithPreview = ({
setActiveQuestionId(template.preset.questions[0].id);
setActiveTemplate(template);
}}
prefilledFilters={prefilledFilters}
/>
</div>
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-100 bg-slate-50 md:flex md:flex-col">

View File

@@ -2,9 +2,22 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
import { TTemplateRole } from "@formbricks/types/templates";
import { TemplateContainerWithPreview } from "./components/TemplateContainer";
const Page = async ({ params }) => {
interface SurveyTemplateProps {
params: {
environmentId: string;
};
searchParams: {
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
role?: TTemplateRole;
};
}
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
const session = await getServerSession(authOptions);
const environmentId = params.environmentId;
@@ -25,12 +38,15 @@ const Page = async ({ params }) => {
throw new Error("Environment not found");
}
const prefilledFilters = [product.config.channel, product.config.industry, searchParams.role ?? null];
return (
<TemplateContainerWithPreview
environmentId={environmentId}
user={session.user}
environment={environment}
product={product}
prefilledFilters={prefilledFilters}
/>
);
};

View File

@@ -1,33 +0,0 @@
"use client";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct } from "@formbricks/types/product";
import { TUser } from "@formbricks/types/user";
import { TemplateList } from "@formbricks/ui/TemplateList";
interface SurveyStarterProps {
environmentId: string;
environment: TEnvironment;
product: TProduct;
user: TUser;
}
export const SurveyStarter = ({ environmentId, environment, product, user }: SurveyStarterProps) => {
return (
<>
<h1 className="px-6 text-3xl font-extrabold text-slate-700">
You&apos;re all set! Time to create your first survey.
</h1>
<TemplateList
environmentId={environmentId}
/* onTemplateClick={(template) => {
newSurveyFromTemplate(template);
}} */
environment={environment}
product={product}
user={user}
/>
</>
);
};

View File

@@ -1,4 +1,3 @@
import { SurveyStarter } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyStarter";
import { PlusIcon } from "lucide-react";
import { Metadata } from "next";
import { getServerSession } from "next-auth";
@@ -10,19 +9,31 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSurveyCount } from "@formbricks/lib/survey/service";
import { TTemplateRole } from "@formbricks/types/templates";
import { Button } from "@formbricks/ui/Button";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { SurveysList } from "@formbricks/ui/SurveysList";
import { TemplateList } from "@formbricks/ui/TemplateList";
export const metadata: Metadata = {
title: "Your Surveys",
};
const Page = async ({ params }) => {
interface SurveyTemplateProps {
params: {
environmentId: string;
};
searchParams: {
role?: TTemplateRole;
};
}
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
const session = await getServerSession(authOptions);
const product = await getProductByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!session) {
throw new Error("Session not found");
}
@@ -35,6 +46,8 @@ const Page = async ({ params }) => {
throw new Error("Organization not found");
}
const prefilledFilters = [product?.config.channel, product.config.industry, searchParams.role ?? null];
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
@@ -73,12 +86,17 @@ const Page = async ({ params }) => {
/>
</>
) : (
<SurveyStarter
environmentId={params.environmentId}
environment={environment}
product={product}
user={session.user}
/>
<>
<h1 className="px-6 text-3xl font-extrabold text-slate-700">
You&apos;re all set! Time to create your first survey.
</h1>
<TemplateList
environment={environment}
product={product}
user={session.user}
prefilledFilters={prefilledFilters}
/>
</>
)}
</PageContentWrapper>
);

View File

@@ -11,12 +11,8 @@ test.describe("JS Package Test", async () => {
await signUpAndLogin(page, name, email, password);
await finishOnboarding(page);
await page
.getByText("Product ExperienceProduct Market Fit (Superhuman)Measure PMF by assessing how")
.isVisible();
await page
.getByText("Product ExperienceProduct Market Fit (Superhuman)Measure PMF by assessing how")
.click();
await page.getByRole("heading", { name: "Product Market Fit (Superhuman)" }).isVisible();
await page.getByRole("heading", { name: "Product Market Fit (Superhuman)" }).click();
await page.getByRole("button", { name: "Use this template" }).isVisible();
await page.getByRole("button", { name: "Use this template" }).click();

View File

@@ -7,7 +7,7 @@
e.parentNode.insertBefore(t, e),
setTimeout(function () {
formbricks.init({
environmentId: "clw6ehzd5008zrx0nmrix2pnw",
environmentId: "clxjz87mb001v13nh5vxkerjs",
apiHost: "http://localhost:3000",
});
}, 500);

View File

@@ -43,9 +43,10 @@ const surveyDefault: TTemplate["preset"] = {
questions: [],
};
export const testTemplate: TTemplate = {
/* export const testTemplate: TTemplate = {
name: "Test template",
category: "Product Experience",
role: "productManager",
industries: ["other"],
description: "Test template consisting of all questions",
preset: {
...surveyDefault,
@@ -347,13 +348,280 @@ export const testTemplate: TTemplate = {
},
],
},
};
}; */
export const templates: TTemplate[] = [
{
name: "Cart Abandonment Survey",
role: "productManager",
industries: ["eCommerce"],
channels: ["app", "website", "link"],
description: "Understand the reasons behind cart abandonment in your web shop.",
preset: {
...surveyDefault,
name: "Cart Abandonment Survey",
questions: [
{
id: createId(),
html: {
default:
'<p class="fb-editor-paragraph" dir="ltr"><span>We noticed you left some items in your cart. We would love to understand why.</span></p>',
},
type: TSurveyQuestionTypeEnum.CTA,
logic: [{ condition: "skipped", destination: "end" }],
headline: { default: "Do you have 2 minutes to help us improve?" },
required: false,
buttonLabel: { default: "Sure!" },
buttonExternal: false,
dismissButtonLabel: { default: "No, thanks." },
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "What was the primary reason you didn't complete your purchase?" },
subheader: { default: "Please select one of the following options:" },
required: true,
shuffleOption: "none",
choices: [
{
id: createId(),
label: { default: "High shipping costs" },
},
{
id: createId(),
label: { default: "Found a better price elsewhere" },
},
{
id: createId(),
label: { default: "Just browsing" },
},
{
id: createId(),
label: { default: "Decided not to buy" },
},
{
id: createId(),
label: { default: "Payment issues" },
},
{ id: "other", label: { default: "Other" } },
],
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: {
default: "Please elaborate on your reason for not completing the purchase:",
},
required: false,
inputType: "text",
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "How would you rate your overall shopping experience?" },
required: true,
scale: "number",
range: 5,
lowerLabel: { default: "Very dissatisfied" },
upperLabel: { default: "Very satisfied" },
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
headline: {
default: "What factors would encourage you to complete your purchase in the future?",
},
subheader: { default: "Please select all that apply:" },
required: true,
choices: [
{
id: createId(),
label: { default: "Lower shipping costs" },
},
{
id: createId(),
label: { default: "Discounts or promotions" },
},
{
id: createId(),
label: { default: "More payment options" },
},
{
id: createId(),
label: { default: "Better product descriptions" },
},
{
id: createId(),
label: { default: "Improved website navigation" },
},
{ id: "other", label: { default: "Other" } },
],
},
{
id: createId(),
logic: [{ condition: "skipped", destination: "bxvvhol84ir34q2vsvr5kwl9" }],
type: TSurveyQuestionTypeEnum.Consent,
headline: { default: "Would you like to receive a discount code via email?" },
required: false,
label: { default: "Yes, please reach out." },
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Please share your email address:" },
required: true,
inputType: "email",
longAnswer: false,
placeholder: { default: "example@email.com" },
},
{
id: "bxvvhol84ir34q2vsvr5kwl9",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Any additional comments or suggestions?" },
required: false,
inputType: "text",
},
],
},
},
{
name: "Site Abandonment Survey",
role: "productManager",
industries: ["eCommerce"],
channels: ["app", "website"],
description: "Understand the reasons behind site abandonment in your web shop.",
preset: {
...surveyDefault,
name: "Site Abandonment Survey",
questions: [
{
id: createId(),
html: {
default:
"<p class='fb-editor-paragraph' dir='ltr'><span>We noticed you're leaving our site without making a purchase. We would love to understand why.</span></p>",
},
type: TSurveyQuestionTypeEnum.CTA,
logic: [{ condition: "skipped", destination: "end" }],
headline: { default: "Do you have a minute?" },
required: false,
buttonLabel: { default: "Sure!" },
buttonExternal: false,
dismissButtonLabel: { default: "No, thanks." },
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "What's the primary reason you're leaving our site?" },
subheader: { default: "Please select one of the following options:" },
required: true,
shuffleOption: "none",
choices: [
{
id: createId(),
label: { default: "Can't find what I am looking for" },
},
{
id: createId(),
label: { default: "Site is too slow" },
},
{
id: createId(),
label: { default: "Technical issues" },
},
{
id: createId(),
label: { default: "Just browsing" },
},
{
id: createId(),
label: { default: "Found a better site" },
},
{ id: "other", label: { default: "Other" } },
],
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: {
default: "Please elaborate on your reason for leaving the site:",
},
required: false,
inputType: "text",
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "How would you rate your overall experience on our site?" },
required: true,
scale: "number",
range: 5,
lowerLabel: { default: "Very dissatisfied" },
upperLabel: { default: "Very satisfied" },
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
headline: {
default: "What improvements would encourage you to stay longer on our site?",
},
subheader: { default: "Please select all that apply:" },
required: true,
choices: [
{
id: createId(),
label: { default: "Faster loading times" },
},
{
id: createId(),
label: { default: "Better product search functionality" },
},
{
id: createId(),
label: { default: "More product variety" },
},
{
id: createId(),
label: { default: "Improved site design" },
},
{
id: createId(),
label: { default: "More customer reviews" },
},
{ id: "other", label: { default: "Other" } },
],
},
{
id: createId(),
logic: [{ condition: "skipped", destination: "bxvvhol84ir34q2vsvr5kwl9" }],
type: TSurveyQuestionTypeEnum.Consent,
headline: { default: "Would you like to receive updates about new products and promotions?" },
required: false,
label: { default: "Yes, please reach out." },
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Please share your email address:" },
required: true,
inputType: "email",
longAnswer: false,
placeholder: { default: "example@email.com" },
},
{
id: "bxvvhol84ir34q2vsvr5kwl9",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Any additional comments or suggestions?" },
required: false,
inputType: "text",
},
],
},
},
{
name: "Product Market Fit (Superhuman)",
category: "Product Experience",
role: "productManager",
industries: ["saas"],
channels: ["app", "link"],
description: "Measure PMF by assessing how disappointed users would be if your product disappeared.",
preset: {
...surveyDefault,
@@ -413,7 +681,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
label: { default: "Product Manager" },
label: { default: "productManager" },
},
{
id: createId(),
@@ -452,8 +720,9 @@ export const templates: TTemplate[] = [
},
{
name: "Onboarding Segmentation",
category: "Product Experience",
objectives: ["increase_user_adoption", "improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app", "link"],
description: "Learn more about who signed up to your product and why.",
preset: {
...surveyDefault,
@@ -477,7 +746,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
label: { default: "Product Manager" },
label: { default: "productManager" },
},
{
id: createId(),
@@ -554,8 +823,9 @@ export const templates: TTemplate[] = [
},
{
name: "Churn Survey",
category: "Increase Revenue",
objectives: ["sharpen_marketing_messaging", "improve_user_retention"],
role: "sales",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link"],
description: "Find out why people cancel their subscriptions. These insights are pure gold!",
preset: {
...surveyDefault,
@@ -635,8 +905,9 @@ export const templates: TTemplate[] = [
},
{
name: "Earned Advocacy Score (EAS)",
category: "Growth",
objectives: ["support_sales", "sharpen_marketing_messaging"],
role: "customerSuccess",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link"],
description:
"The EAS is a riff off the NPS but asking for actual past behaviour instead of lofty intentions.",
preset: {
@@ -697,8 +968,9 @@ export const templates: TTemplate[] = [
},
{
name: "Improve Trial Conversion",
category: "Increase Revenue",
objectives: ["increase_user_adoption", "increase_conversion", "improve_user_retention"],
role: "sales",
industries: ["saas"],
channels: ["link", "app"],
description: "Find out why people stopped their trial. These insights help you improve your funnel.",
preset: {
...surveyDefault,
@@ -802,9 +1074,9 @@ export const templates: TTemplate[] = [
},
{
name: "Review Prompt",
category: "Growth",
objectives: ["support_sales"],
role: "marketing",
industries: ["saas", "eCommerce", "other"],
channels: ["link", "app"],
description: "Invite users who love your product to review it publicly.",
preset: {
...surveyDefault,
@@ -847,9 +1119,9 @@ export const templates: TTemplate[] = [
},
{
name: "Interview Prompt",
category: "Exploration",
objectives: ["improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Invite a specific subset of your users to schedule an interview with your product team.",
preset: {
...surveyDefault,
@@ -869,9 +1141,10 @@ export const templates: TTemplate[] = [
},
},
{
name: "Reduce Onboarding Drop-Off",
category: "Product Experience",
objectives: ["increase_user_adoption", "increase_conversion"],
name: "Improve Activation Rate",
role: "productManager",
industries: ["saas"],
channels: ["link"],
description: "Identify weaknesses in your onboarding flow to increase user activation.",
preset: {
...surveyDefault,
@@ -966,8 +1239,9 @@ export const templates: TTemplate[] = [
},
{
name: "Uncover Strengths & Weaknesses",
category: "Growth",
objectives: ["sharpen_marketing_messaging", "improve_user_retention"],
role: "productManager",
industries: ["saas", "other"],
channels: ["app", "link"],
description: "Find out what users like and don't like about your product or offering.",
preset: {
...surveyDefault,
@@ -1014,7 +1288,9 @@ export const templates: TTemplate[] = [
},
{
name: "Product Market Fit Survey (Short)",
category: "Product Experience",
role: "productManager",
industries: ["saas"],
channels: ["app", "link"],
description: "Measure PMF by assessing how disappointed users would be if your product disappeared.",
preset: {
...surveyDefault,
@@ -1055,9 +1331,9 @@ export const templates: TTemplate[] = [
},
{
name: "Marketing Attribution",
category: "Growth",
objectives: ["increase_conversion", "sharpen_marketing_messaging"],
role: "marketing",
industries: ["saas", "eCommerce"],
channels: ["website", "app", "link"],
description: "How did you first hear about us?",
preset: {
...surveyDefault,
@@ -1098,9 +1374,9 @@ export const templates: TTemplate[] = [
},
{
name: "Changing Subscription Experience",
category: "Increase Revenue",
objectives: ["increase_conversion", "improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Find out what goes through peoples minds when changing their subscriptions.",
preset: {
...surveyDefault,
@@ -1162,9 +1438,9 @@ export const templates: TTemplate[] = [
{
name: "Identify Customer Goals",
category: "Product Experience",
objectives: ["increase_user_adoption", "sharpen_marketing_messaging", "improve_user_retention"],
role: "productManager",
industries: ["saas", "other"],
channels: ["app", "website"],
description:
"Better understand if your messaging creates the right expectations of the value your product provides.",
preset: {
@@ -1199,11 +1475,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Feature Chaser",
category: "Product Experience",
objectives: ["improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Follow up with users who just used a specific feature.",
preset: {
...surveyDefault,
@@ -1235,11 +1512,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Fake Door Follow-Up",
category: "Exploration",
objectives: ["increase_user_adoption"],
role: "productManager",
industries: ["saas", "eCommerce"],
channels: ["app", "website"],
description: "Follow up with users who ran into one of your Fake Door experiments.",
preset: {
...surveyDefault,
@@ -1283,11 +1561,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Feedback Box",
category: "Product Experience",
objectives: ["improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Give your users the chance to seamlessly share what's on their minds.",
preset: {
...surveyDefault,
@@ -1348,11 +1627,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Integration Setup Survey",
category: "Product Experience",
objectives: ["increase_user_adoption"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Evaluate how easily users can add integrations to your product. Find blind spots.",
preset: {
...surveyDefault,
@@ -1388,11 +1668,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "New Integration Survey",
category: "Exploration",
objectives: ["increase_user_adoption", "increase_conversion"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Find out which integrations your users would like to see next.",
preset: {
...surveyDefault,
@@ -1430,11 +1711,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Docs Feedback",
category: "Product Experience",
objectives: ["increase_user_adoption", "improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app", "website", "link"],
description: "Measure how clear each page of your developer documentation is.",
preset: {
...surveyDefault,
@@ -1474,11 +1756,13 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Net Promoter Score (NPS)",
category: "Customer Success",
objectives: ["support_sales"],
role: "customerSuccess",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link", "website"],
description: "Measure the Net Promoter Score of your product or service.",
preset: {
...surveyDefault,
@@ -1502,11 +1786,13 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Customer Satisfaction Score (CSAT)",
category: "Customer Success",
objectives: ["support_sales"],
description: "Measure the Customer Satisfaction Score of your product.",
role: "customerSuccess",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link", "website"],
description: "Measure the Customer Satisfaction Score of your product or service.",
preset: {
...surveyDefault,
name: "{{productName}} CSAT",
@@ -1542,10 +1828,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Collect Feedback",
category: "Product Experience",
objectives: ["increase_user_adoption", "improve_user_retention"],
role: "productManager",
industries: ["other", "eCommerce"],
channels: ["website", "link"],
description: "Gather comprehensive feedback on your product or service.",
preset: {
...surveyDefault,
@@ -1628,11 +1916,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Identify Upsell Opportunities",
category: "Increase Revenue",
objectives: ["support_sales", "sharpen_marketing_messaging"],
role: "sales",
industries: ["saas"],
channels: ["app", "link"],
description: "Find out how much time your product saves your user. Use it to upsell.",
preset: {
...surveyDefault,
@@ -1669,9 +1958,9 @@ export const templates: TTemplate[] = [
{
name: "Prioritize Features",
category: "Exploration",
objectives: ["increase_user_adoption"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Identify features your users need most and least.",
preset: {
...surveyDefault,
@@ -1715,11 +2004,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Gauge Feature Satisfaction",
category: "Product Experience",
objectives: ["increase_user_adoption", "improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Evaluate the satisfaction of specific features of your product.",
preset: {
...surveyDefault,
@@ -1747,11 +2037,12 @@ export const templates: TTemplate[] = [
hiddenFields: hiddenFieldsDefault,
},
},
{
name: "Marketing Site Clarity",
category: "Growth",
objectives: ["increase_conversion", "sharpen_marketing_messaging"],
role: "marketing",
industries: ["saas", "eCommerce", "other"],
channels: ["website"],
description: "Identify users dropping off your marketing site. Improve your messaging.",
preset: {
...surveyDefault,
@@ -1797,11 +2088,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Customer Effort Score (CES)",
category: "Product Experience",
objectives: ["increase_user_adoption", "improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: "Determine how easy it is to use a feature.",
preset: {
...surveyDefault,
@@ -1831,9 +2123,9 @@ export const templates: TTemplate[] = [
{
name: "Rate Checkout Experience",
category: "Increase Revenue",
objectives: ["increase_conversion"],
role: "productManager",
industries: ["eCommerce"],
channels: ["website", "app"],
description: "Let customers rate the checkout experience to tweak conversion.",
preset: {
...surveyDefault,
@@ -1870,11 +2162,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Measure Search Experience",
category: "Product Experience",
objectives: ["improve_user_retention"],
role: "productManager",
industries: ["saas", "eCommerce"],
channels: ["app", "website"],
description: "Measure how relevant your search results are.",
preset: {
...surveyDefault,
@@ -1911,11 +2204,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Evaluate Content Quality",
category: "Growth",
objectives: ["increase_conversion"],
role: "marketing",
industries: ["other"],
channels: ["website"],
description: "Measure if your content marketing pieces hit right.",
preset: {
...surveyDefault,
@@ -1952,11 +2246,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Measure Task Accomplishment",
category: "Customer Success",
objectives: ["increase_user_adoption", "improve_user_retention"],
role: "productManager",
industries: ["saas"],
channels: ["app", "website"],
description: "See if people get their 'Job To Be Done' done. Successful people are better customers.",
preset: {
...surveyDefault,
@@ -2026,11 +2321,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Identify Sign Up Barriers",
category: "Growth",
objectives: ["increase_conversion"],
role: "marketing",
industries: ["saas", "eCommerce", "other"],
channels: ["website"],
description: "Offer a discount to gather insights about sign up barriers.",
preset: {
...surveyDefault,
@@ -2151,11 +2447,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Build Product Roadmap",
category: "Exploration",
objectives: ["increase_user_adoption"],
role: "productManager",
industries: ["saas"],
channels: ["app", "link"],
description: "Identify the ONE thing your users want the most and build it.",
preset: {
...surveyDefault,
@@ -2186,11 +2483,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Understand Purchase Intention",
category: "Increase Revenue",
objectives: ["increase_conversion", "increase_user_adoption"],
role: "sales",
industries: ["eCommerce"],
channels: ["website", "link", "app"],
description: "Find out how close your visitors are to buy or subscribe.",
preset: {
...surveyDefault,
@@ -2207,7 +2505,7 @@ export const templates: TTemplate[] = [
],
range: 5,
scale: "number",
headline: { default: "How likely are you to subscribe to {{productName}} today?" },
headline: { default: "How likely are you to shop from us today?" },
required: true,
lowerLabel: { default: "Not at all likely" },
upperLabel: { default: "Extremely likely" },
@@ -2235,10 +2533,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Improve Newsletter Content",
category: "Growth",
objectives: ["increase_conversion", "sharpen_marketing_messaging"],
role: "marketing",
industries: ["eCommerce", "saas", "other"],
channels: ["link"],
description: "Find out how your subscribers like your newsletter content.",
preset: {
...surveyDefault,
@@ -2287,11 +2587,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Evaluate a Product Idea",
category: "Exploration",
objectives: ["improve_user_retention", "increase_user_adoption"],
role: "productManager",
industries: ["saas", "other"],
channels: ["link", "app"],
description: "Survey users about product or feature ideas. Get feedback rapidly.",
preset: {
...surveyDefault,
@@ -2390,11 +2691,12 @@ export const templates: TTemplate[] = [
],
},
},
{
name: "Understand Low Engagement",
category: "Product Experience",
objectives: ["improve_user_retention", "increase_user_adoption"],
role: "productManager",
industries: ["saas"],
channels: ["link"],
description: "Identify reasons for low engagement to improve user adoption.",
preset: {
...surveyDefault,

View File

@@ -9,12 +9,15 @@ export const ZProductStyling = ZBaseStyling.extend({
export type TProductStyling = z.infer<typeof ZProductStyling>;
export const ZProductConfigIndustry = z.enum(["eCommerce", "saas", "other"]).nullable();
export type TProductConfigIndustry = z.infer<typeof ZProductConfigIndustry>;
export const ZProductConfigChannel = z.enum(["link", "app", "website"]).nullable();
export type TProductConfigChannel = z.infer<typeof ZProductConfigChannel>;
export const ZProductConfig = z.object({
channel: ZProductConfigChannel,
industry: z.enum(["eCommerce", "saas"]).nullable(),
industry: ZProductConfigIndustry,
});
export type TProductConfig = z.infer<typeof ZProductConfig>;

View File

@@ -1,15 +1,19 @@
import { z } from "zod";
import { ZLegacySurveyQuestions, ZLegacySurveyThankYouCard, ZLegacySurveyWelcomeCard } from "./LegacySurvey";
import { ZProductConfigChannel, ZProductConfigIndustry } from "./product";
import { ZSurveyHiddenFields, ZSurveyQuestions, ZSurveyThankYouCard, ZSurveyWelcomeCard } from "./surveys";
import { ZUserObjective } from "./user";
export const ZTemplateRole = z.enum(["productManager", "customerSuccess", "marketing", "sales"]);
export type TTemplateRole = z.infer<typeof ZTemplateRole>;
export const ZTemplate = z.object({
name: z.string(),
description: z.string(),
icon: z.any().optional(),
category: z
.enum(["Product Experience", "Exploration", "Growth", "Increase Revenue", "Customer Success"])
.optional(),
role: ZTemplateRole.optional(),
channels: z.array(z.enum(["link", "app", "website"])).optional(),
industries: z.array(z.enum(["eCommerce", "saas", "other"])).optional(),
objectives: z.array(ZUserObjective).optional(),
preset: z.object({
name: z.string(),
@@ -33,3 +37,12 @@ export const ZLegacyTemplate = ZTemplate.extend({
});
export type TLegacyTemplate = z.infer<typeof ZLegacyTemplate>;
export const ZTemplateFilter = z.union([
ZProductConfigChannel,
ZProductConfigIndustry,
ZTemplateRole,
z.null(),
]);
export type TTemplateFilter = z.infer<typeof ZTemplateFilter>;

View File

@@ -2,7 +2,7 @@ import { Search } from "lucide-react";
import * as React from "react";
import { cn } from "@formbricks/lib/cn";
export interface InputProps
export interface SearchBoxProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "crossOrigin" | "dangerouslySetInnerHTML"> {
crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
dangerouslySetInnerHTML?: {
@@ -10,7 +10,7 @@ export interface InputProps
};
}
const SearchBox = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
const SearchBox = React.forwardRef<HTMLInputElement, SearchBoxProps>(({ className, ...props }, ref) => {
return (
<div className="relative">
<input

View File

@@ -0,0 +1,57 @@
import { PlusCircleIcon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
import { customSurvey } from "@formbricks/lib/templates";
import { TProduct } from "@formbricks/types/product";
import { TTemplate } from "@formbricks/types/templates";
import { Button } from "../../Button";
import { replacePresetPlaceholders } from "../lib/utils";
interface StartFromScratchTemplateProps {
activeTemplate: TTemplate | null;
setActiveTemplate: (template: TTemplate) => void;
onTemplateClick: (template: TTemplate) => void;
product: TProduct;
createSurvey: (template: TTemplate) => void;
loading: boolean;
}
export const StartFromScratchTemplate = ({
activeTemplate,
setActiveTemplate,
onTemplateClick,
product,
createSurvey,
loading,
}: StartFromScratchTemplateProps) => {
return (
<button
type="button"
onClick={() => {
const newTemplate = replacePresetPlaceholders(customSurvey, product);
onTemplateClick(newTemplate);
setActiveTemplate(newTemplate);
}}
className={cn(
activeTemplate?.name === customSurvey.name
? "ring-brand border-transparent ring-2"
: "hover:border-brand-dark border-dashed border-slate-300",
"duration-120 group relative rounded-lg border-2 bg-transparent p-6 transition-colors duration-150"
)}>
<PlusCircleIcon className="text-brand-dark h-8 w-8 transition-all duration-150 group-hover:scale-110" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 ">{customSurvey.name}</h3>
<p className="text-left text-xs text-slate-600 ">{customSurvey.description}</p>
{activeTemplate?.name === customSurvey.name && (
<div className="text-left">
<Button
variant="darkCTA"
className="mt-6 px-6 py-3"
disabled={activeTemplate === null}
loading={loading}
onClick={() => createSurvey(activeTemplate)}>
Create survey
</Button>
</div>
)}
</button>
);
};

View File

@@ -0,0 +1,58 @@
import { cn } from "@formbricks/lib/cn";
import { TProduct } from "@formbricks/types/product";
import { TTemplate, TTemplateFilter } from "@formbricks/types/templates";
import { Button } from "../../Button";
import { replacePresetPlaceholders } from "../lib/utils";
import { TemplateTags } from "./TemplateTags";
interface TemplateProps {
template: TTemplate;
activeTemplate: TTemplate | null;
setActiveTemplate: (template: TTemplate) => void;
onTemplateClick?: (template: TTemplate) => void;
product: TProduct;
createSurvey: (template: TTemplate) => void;
loading: boolean;
selectedFilter: TTemplateFilter[];
}
export const Template = ({
template,
activeTemplate,
setActiveTemplate,
onTemplateClick = () => {},
product,
createSurvey,
loading,
selectedFilter,
}: TemplateProps) => {
return (
<div
onClick={() => {
const newTemplate = replacePresetPlaceholders(template, product);
onTemplateClick(newTemplate);
setActiveTemplate(newTemplate);
}}
key={template.name}
className={cn(
activeTemplate?.name === template.name && "ring-2 ring-slate-400",
"duration-120 group relative cursor-pointer rounded-lg bg-white p-6 shadow transition-all duration-150 hover:ring-2 hover:ring-slate-300"
)}>
<TemplateTags template={template} selectedFilter={selectedFilter} />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700">{template.name}</h3>
<p className="text-left text-xs text-slate-600">{template.description}</p>
{activeTemplate?.name === template.name && (
<div className="flex justify-start">
<Button
variant="darkCTA"
className="mt-6 px-6 py-3"
disabled={activeTemplate === null}
loading={loading}
onClick={() => createSurvey(activeTemplate)}>
Use this template
</Button>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,64 @@
import { cn } from "@formbricks/lib/cn";
import { TTemplateFilter } from "@formbricks/types/templates";
import { channelMapping, industryMapping, roleMapping } from "../lib/utils";
interface TemplateFiltersProps {
selectedFilter: TTemplateFilter[];
setSelectedFilter: (filter: TTemplateFilter[]) => void;
templateSearch?: string;
prefilledFilters: TTemplateFilter[];
}
export const TemplateFilters = ({
selectedFilter,
setSelectedFilter,
templateSearch,
prefilledFilters,
}: TemplateFiltersProps) => {
const handleFilterSelect = (filterValue: TTemplateFilter, index: number) => {
// If the filter value at a particular index is null, it indicates that no filter has been chosen, therefore all results are displayed
const newFilter = [...selectedFilter];
newFilter[index] = filterValue;
setSelectedFilter(newFilter);
};
const allFilters = [channelMapping, industryMapping, roleMapping];
return (
<div className="mb-6 gap-3">
{allFilters.map((filters, index) => {
if (prefilledFilters[index] !== null) return;
return (
<div className="mt-2 flex flex-wrap gap-1 last:border-r-0">
<button
key={index}
type="button"
onClick={() => handleFilterSelect(null, index)}
disabled={templateSearch && templateSearch.length > 0 ? true : false}
className={cn(
selectedFilter[index] === null
? " bg-slate-800 font-semibold text-white"
: " bg-white text-slate-700 hover:bg-slate-100 focus:scale-105 focus:bg-slate-100 focus:outline-none focus:ring-0",
"rounded border border-slate-800 px-2 py-1 text-xs transition-all duration-150"
)}>
{index === 0 ? "All channels" : index === 1 ? "All industries" : "All roles"}
</button>
{filters.map((filter) => (
<button
key={filter.value}
type="button"
onClick={() => handleFilterSelect(filter.value, index)}
disabled={templateSearch && templateSearch.length > 0 ? true : false}
className={cn(
selectedFilter[index] === filter.value
? " bg-slate-800 font-semibold text-white"
: " bg-white text-slate-700 hover:bg-slate-100 focus:scale-105 focus:bg-slate-100 focus:outline-none focus:ring-0",
"rounded border border-slate-800 px-2 py-1 text-xs transition-all duration-150"
)}>
{filter.label}
</button>
))}
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,103 @@
import { SplitIcon } from "lucide-react";
import { useMemo } from "react";
import { cn } from "@formbricks/lib/cn";
import { TProductConfigIndustry } from "@formbricks/types/product";
import { TSurveyType } from "@formbricks/types/surveys";
import { TTemplate, TTemplateFilter, TTemplateRole } from "@formbricks/types/templates";
import { TooltipRenderer } from "../../Tooltip";
import { channelMapping, industryMapping, roleMapping } from "../lib/utils";
interface TemplateTagsProps {
template: TTemplate;
selectedFilter: TTemplateFilter[];
}
const getRoleBasedStyling = (role: TTemplateRole | undefined): string => {
switch (role) {
case "productManager":
return "border-blue-300 bg-blue-50 text-blue-500";
case "marketing":
return "border-orange-300 bg-orange-50 text-orange-500";
case "sales":
return "border-emerald-300 bg-emerald-50 text-emerald-500";
case "customerSuccess":
return "border-violet-300 bg-violet-50 text-violet-500";
default:
return "border-slate-300 bg-slate-50 text-slate-500";
}
};
const getChannelTag = (channels: TSurveyType[] | undefined): string | undefined => {
if (!channels) return undefined;
const getLabel = (channelValue: TSurveyType) =>
channelMapping.find((channel) => channel.value === channelValue)?.label;
const labels = channels.map((channel) => getLabel(channel)).sort();
const removeSurveySuffix = (label: string | undefined) => label?.replace(" Survey", "");
switch (channels.length) {
case 1:
return labels[0];
case 2:
// Return labels for two channels concatenated with "or", removing "Survey"
return labels.map(removeSurveySuffix).join(" or ") + " Survey";
case 3:
return "All Channels";
default:
return undefined;
}
};
export const TemplateTags = ({ template, selectedFilter }: TemplateTagsProps) => {
const roleBasedStyling = useMemo(() => getRoleBasedStyling(template.role), [template.role]);
const roleTag = useMemo(
() => roleMapping.find((roleMap) => roleMap.value === template.role)?.label,
[template.role]
);
const channelTag = useMemo(() => getChannelTag(template.channels), [template.channels]);
const getIndustryTag = (industries: TProductConfigIndustry[] | undefined): string | undefined => {
// if user selects an industry e.g. eCommerce than the tag should not say "Multiple industries" anymore but "E-Commerce".
if (selectedFilter[1] !== null)
return industryMapping.find((industry) => industry.value === selectedFilter[1])?.label;
if (!industries || industries.length === 0) return undefined;
return industries.length > 1
? "Multiple Industries"
: industryMapping.find((industry) => industry.value === industries[0])?.label;
};
const industryTag = useMemo(
() => getIndustryTag(template.industries),
[template.industries, selectedFilter]
);
return (
<div className="flex flex-wrap gap-1.5">
<div className={cn("rounded border px-1.5 py-0.5 text-xs", roleBasedStyling)}>{roleTag}</div>
{industryTag && (
<div
className={cn("rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500")}>
{industryTag}
</div>
)}
{channelTag && (
<div
className={cn(
"flex-nowrap rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500"
)}>
{channelTag}
</div>
)}
{template.preset.questions.some((question) => question.logic && question.logic.length > 0) && (
<TooltipRenderer tooltipContent="This survey uses branching logic." shouldRender={true}>
<SplitIcon className="h-5 w-5 rounded border border-slate-300 bg-slate-50 p-0.5 text-slate-400" />
</TooltipRenderer>
)}
</div>
);
};

View File

@@ -1,69 +1,41 @@
"use client";
import { PlusCircleIcon, SparklesIcon, SplitIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { customSurvey, templates, testTemplate } from "@formbricks/lib/templates";
import { useMemo, useState } from "react";
import { templates } from "@formbricks/lib/templates";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct } from "@formbricks/types/product";
import { TSurveyInput } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import { type TProduct, ZProductConfigIndustry } from "@formbricks/types/product";
import { TSurveyInput, ZSurveyType } from "@formbricks/types/surveys";
import { TTemplate, TTemplateFilter, ZTemplateRole } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
import { Button } from "../Button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../Tooltip";
import { createSurveyAction } from "./actions";
import { replacePresetPlaceholders } from "./lib/utils";
import { StartFromScratchTemplate } from "./components/StartFromScratchTemplate";
import { Template } from "./components/Template";
import { TemplateFilters } from "./components/TemplateFilters";
interface TemplateList {
environmentId: string;
interface TemplateListProps {
user: TUser;
onTemplateClick?: (template: TTemplate) => void;
environment: TEnvironment;
product: TProduct;
templateSearch?: string;
prefilledFilters: TTemplateFilter[];
onTemplateClick?: (template: TTemplate) => void;
}
const ALL_CATEGORY_NAME = "All";
const RECOMMENDED_CATEGORY_NAME = "For you";
export const TemplateList = ({
environmentId,
user,
onTemplateClick = () => {},
product,
environment,
templateSearch,
}: TemplateList) => {
prefilledFilters,
onTemplateClick = () => {},
}: TemplateListProps) => {
const router = useRouter();
const [activeTemplate, setActiveTemplate] = useState<TTemplate | null>(null);
const [loading, setLoading] = useState(false);
const [selectedFilter, setSelectedFilter] = useState(RECOMMENDED_CATEGORY_NAME);
const [selectedFilter, setSelectedFilter] = useState<TTemplateFilter[]>(prefilledFilters);
const [categories, setCategories] = useState<Array<string>>([]);
useEffect(() => {
const defaultCategories = [
/* ALL_CATEGORY_NAME, */
...(Array.from(new Set(templates.map((template) => template.category))) as string[]),
];
const fullCategories =
!!user?.objective && user.objective !== "other"
? [RECOMMENDED_CATEGORY_NAME, ALL_CATEGORY_NAME, ...defaultCategories]
: [ALL_CATEGORY_NAME, ...defaultCategories];
setCategories(fullCategories);
const activeFilter = templateSearch
? ALL_CATEGORY_NAME
: !!user?.objective && user.objective !== "other"
? RECOMMENDED_CATEGORY_NAME
: ALL_CATEGORY_NAME;
setSelectedFilter(activeFilter);
}, [user, templateSearch]);
const addSurvey = async (activeTemplate: TTemplate) => {
const createSurvey = async (activeTemplate: TTemplate) => {
setLoading(true);
const surveyType = environment?.appSetupCompleted
? "app"
@@ -75,141 +47,75 @@ export const TemplateList = ({
type: surveyType,
createdBy: user.id,
};
const survey = await createSurveyAction(environmentId, augmentedTemplate);
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
const survey = await createSurveyAction(environment.id, augmentedTemplate);
router.push(`/environments/${environment.id}/surveys/${survey.id}/edit`);
};
const filteredTemplates = templates.filter((template) => {
const matchesCategory =
selectedFilter === ALL_CATEGORY_NAME ||
template.category === selectedFilter ||
(user.objective &&
selectedFilter === RECOMMENDED_CATEGORY_NAME &&
template.objectives?.includes(user.objective));
const filteredTemplates = useMemo(() => {
return templates.filter((template) => {
if (templateSearch) {
return template.name.toLowerCase().startsWith(templateSearch.toLowerCase());
}
// Parse and validate the filters
const channelParseResult = ZSurveyType.nullable().safeParse(selectedFilter[0]);
const industryParseResult = ZProductConfigIndustry.nullable().safeParse(selectedFilter[1]);
const roleParseResult = ZTemplateRole.nullable().safeParse(selectedFilter[2]);
const templateName = template.name?.toLowerCase();
const templateDescription = template.description?.toLowerCase();
const searchQuery = templateSearch?.toLowerCase() ?? "";
const searchWords = searchQuery.split(" ");
// Ensure all validations are successful
if (!channelParseResult.success || !industryParseResult.success || !roleParseResult.success) {
// If any validation fails, skip this template
return true;
}
const matchesSearch = searchWords.every(
(word) => templateName?.includes(word) || templateDescription?.includes(word)
);
// Access the validated data from the parse results
const validatedChannel = channelParseResult.data;
const validatedIndustry = industryParseResult.data;
const validatedRole = roleParseResult.data;
return matchesCategory && matchesSearch;
});
// Perform the filtering
const channelMatch = validatedChannel === null || template.channels?.includes(validatedChannel);
const industryMatch = validatedIndustry === null || template.industries?.includes(validatedIndustry);
const roleMatch = validatedRole === null || template.role === validatedRole;
return channelMatch && industryMatch && roleMatch;
});
}, [selectedFilter, templateSearch]);
return (
<main className="relative z-0 flex-1 overflow-y-auto px-6 pb-6 pt-3 focus:outline-none">
<div className="mb-6 flex flex-wrap gap-1">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setSelectedFilter(category)}
disabled={templateSearch && templateSearch.length > 0 ? true : false}
className={cn(
selectedFilter === category
? " bg-slate-800 font-semibold text-white"
: " bg-white text-slate-700 hover:bg-slate-100 focus:scale-105 focus:bg-slate-100 focus:outline-none focus:ring-0",
"mt-2 rounded border border-slate-800 px-2 py-1 text-xs transition-all duration-150"
)}>
{category}
{category === RECOMMENDED_CATEGORY_NAME && <SparklesIcon className="ml-1 inline h-5 w-5" />}
</button>
))}
</div>
<main className="relative z-0 flex-1 overflow-y-auto px-6 pb-6 focus:outline-none">
{!templateSearch && (
<TemplateFilters
selectedFilter={selectedFilter}
setSelectedFilter={setSelectedFilter}
templateSearch={templateSearch}
prefilledFilters={prefilledFilters}
/>
)}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<button
type="button"
onClick={() => {
const newTemplate = replacePresetPlaceholders(customSurvey, product);
onTemplateClick(newTemplate);
setActiveTemplate(newTemplate);
}}
className={cn(
activeTemplate?.name === customSurvey.name
? "ring-brand border-transparent ring-2"
: "hover:border-brand-dark border-dashed border-slate-300",
"duration-120 group relative rounded-lg border-2 bg-transparent p-6 transition-colors duration-150"
)}>
<PlusCircleIcon className="text-brand-dark h-8 w-8 transition-all duration-150 group-hover:scale-110" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 ">{customSurvey.name}</h3>
<p className="text-left text-xs text-slate-600 ">{customSurvey.description}</p>
{activeTemplate?.name === customSurvey.name && (
<div className="text-left">
<Button
variant="darkCTA"
className="mt-6 px-6 py-3"
disabled={activeTemplate === null}
<StartFromScratchTemplate
activeTemplate={activeTemplate}
setActiveTemplate={setActiveTemplate}
onTemplateClick={onTemplateClick}
product={product}
createSurvey={createSurvey}
loading={loading}
/>
{(process.env.NODE_ENV === "development" ? [...filteredTemplates] : filteredTemplates).map(
(template: TTemplate) => {
return (
<Template
template={template}
activeTemplate={activeTemplate}
setActiveTemplate={setActiveTemplate}
onTemplateClick={onTemplateClick}
product={product}
createSurvey={createSurvey}
loading={loading}
onClick={() => addSurvey(activeTemplate)}>
Create survey
</Button>
</div>
)}
</button>
{(process.env.NODE_ENV === "development"
? [...filteredTemplates, testTemplate]
: filteredTemplates
).map((template: TTemplate) => (
<div
onClick={() => {
const newTemplate = replacePresetPlaceholders(template, product);
onTemplateClick(newTemplate);
setActiveTemplate(newTemplate);
}}
key={template.name}
className={cn(
activeTemplate?.name === template.name && "ring-2 ring-slate-400",
"duration-120 group relative cursor-pointer rounded-lg bg-white p-6 shadow transition-all duration-150 hover:ring-2 hover:ring-slate-300"
)}>
<div className="flex">
<div
className={`rounded border px-1.5 py-0.5 text-xs ${
template.category === "Product Experience"
? "border-blue-300 bg-blue-50 text-blue-500"
: template.category === "Exploration"
? "border-pink-300 bg-pink-50 text-pink-500"
: template.category === "Growth"
? "border-orange-300 bg-orange-50 text-orange-500"
: template.category === "Increase Revenue"
? "border-emerald-300 bg-emerald-50 text-emerald-500"
: template.category === "Customer Success"
? "border-violet-300 bg-violet-50 text-violet-500"
: "border-slate-300 bg-slate-50 text-slate-500" // default color
}`}>
{template.category}
</div>
{template.preset.questions.some((question) => question.logic && question.logic.length > 0) && (
<TooltipProvider delayDuration={80}>
<Tooltip>
<TooltipTrigger tabIndex={-1}>
<div>
<SplitIcon className="ml-1.5 h-5 w-5 rounded border border-slate-300 bg-slate-50 p-0.5 text-slate-400" />
</div>
</TooltipTrigger>
<TooltipContent>This survey uses branching logic.</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700">{template.name}</h3>
<p className="text-left text-xs text-slate-600">{template.description}</p>
{activeTemplate?.name === template.name && (
<div className="flex justify-start">
<Button
variant="darkCTA"
className="mt-6 px-6 py-3"
disabled={activeTemplate === null}
loading={loading}
onClick={() => addSurvey(activeTemplate)}>
Use this template
</Button>
</div>
)}
</div>
))}
selectedFilter={selectedFilter}
/>
);
}
)}
</div>
</main>
);

View File

@@ -1,8 +1,8 @@
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { TProduct } from "@formbricks/types/product";
import { TSurveyQuestion } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import { TProduct, TProductConfigIndustry } from "@formbricks/types/product";
import { TSurveyQuestion, TSurveyType } from "@formbricks/types/surveys";
import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
export const replaceQuestionPresetPlaceholders = (
question: TSurveyQuestion,
@@ -35,3 +35,22 @@ export const replacePresetPlaceholders = (template: TTemplate, product: any) =>
});
return { ...template, preset };
};
export const channelMapping: { value: TSurveyType; label: string }[] = [
{ value: "website", label: "Website Survey" },
{ value: "app", label: "App Survey" },
{ value: "link", label: "Link Survey" },
];
export const industryMapping: { value: TProductConfigIndustry; label: string }[] = [
{ value: "eCommerce", label: "E-Commerce" },
{ value: "saas", label: "SaaS" },
{ value: "other", label: "Other" },
];
export const roleMapping: { value: TTemplateRole; label: string }[] = [
{ value: "productManager", label: "Product Manager" },
{ value: "customerSuccess", label: "Customer Success" },
{ value: "marketing", label: "Marketing" },
{ value: "sales", label: "Sales" },
];