mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-20 13:59:23 -06:00
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:
committed by
GitHub
parent
cd40e655fb
commit
a473719eee
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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're all set! Time to create your first survey.
|
||||
</h1>
|
||||
|
||||
<TemplateList
|
||||
environmentId={environmentId}
|
||||
/* onTemplateClick={(template) => {
|
||||
newSurveyFromTemplate(template);
|
||||
}} */
|
||||
environment={environment}
|
||||
product={product}
|
||||
user={user}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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're all set! Time to create your first survey.
|
||||
</h1>
|
||||
<TemplateList
|
||||
environment={environment}
|
||||
product={product}
|
||||
user={session.user}
|
||||
prefilledFilters={prefilledFilters}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
e.parentNode.insertBefore(t, e),
|
||||
setTimeout(function () {
|
||||
formbricks.init({
|
||||
environmentId: "clw6ehzd5008zrx0nmrix2pnw",
|
||||
environmentId: "clxjz87mb001v13nh5vxkerjs",
|
||||
apiHost: "http://localhost:3000",
|
||||
});
|
||||
}, 500);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
58
packages/ui/TemplateList/components/Template.tsx
Normal file
58
packages/ui/TemplateList/components/Template.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
64
packages/ui/TemplateList/components/TemplateFilters.tsx
Normal file
64
packages/ui/TemplateList/components/TemplateFilters.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
103
packages/ui/TemplateList/components/TemplateTags.tsx
Normal file
103
packages/ui/TemplateList/components/TemplateTags.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user