Add Best Practices to Landingpage (#281)

* Update landingpage
This commit is contained in:
Johannes
2023-05-12 14:25:17 +02:00
committed by GitHub
parent 9b38f9bf9a
commit b5928e71e5
107 changed files with 3068 additions and 1142 deletions

View File

@@ -22,10 +22,10 @@ export const DocsFeedback: React.FC = () => {
return (
<div className="mt-6 inline-flex cursor-default items-center rounded-md border border-slate-200 bg-white p-4 text-slate-800 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
{!sharedFeedback ? (
<div>
<div className="text-center md:text-left">
Was this page helpful?
<Popover open={isOpen} onOpenChange={setIsOpen}>
<div className="ml-4 inline-flex space-x-3">
<div className="mt-2 inline-flex space-x-3 md:ml-4 md:mt-0">
{["Yes 👍", " No 👎"].map((option) => (
<PopoverTrigger
key={option}

View File

@@ -161,7 +161,7 @@ export const Layout: React.FC<LayoutProps> = ({ children, meta }) => {
)}
</dl>
<div className="mt-16 rounded-xl border-2 border-slate-200 bg-slate-300 p-8 dark:border-slate-700/50 dark:bg-slate-800">
<h4 className="text-3xl font-semibold text-slate-500 dark:text-slate-50">Need help?</h4>
<h4 className="text-3xl font-semibold text-slate-500 dark:text-slate-50">Need help? 🤓</h4>
<p className="my-4 text-slate-500 dark:text-slate-400">
Join our Discord and ask away. We&apos;re happy to help where we can!
</p>

View File

@@ -0,0 +1,45 @@
import type { CTAQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
interface CTAQuestionProps {
question: CTAQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function CTAQuestion({ question, onSubmit, lastQuestion, brandColor }: CTAQuestionProps) {
return (
<div>
<Headline headline={question.headline} questionId={question.id} />
<HtmlBody htmlString={question.html || ""} questionId={question.id} />
<div className="mt-4 flex w-full justify-end">
<div></div>
{!question.required && (
<button
type="button"
onClick={() => {
onSubmit({ [question.id]: "dismissed" });
}}
className="mr-4 flex items-center rounded-md px-3 py-3 text-base font-medium leading-4 text-slate-500 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 dark:border-slate-400 dark:text-slate-400">
{question.dismissButtonLabel || "Skip"}
</button>
)}
<button
type="button"
onClick={() => {
if (question.buttonExternal && question.buttonUrl) {
window?.open(question.buttonUrl, "_blank")?.focus();
}
onSubmit({ [question.id]: "clicked" });
}}
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import clsx from "clsx";
interface ContentWrapperProps {
children: React.ReactNode;
className?: string;
}
export default function ContentWrapper({ children, className }: ContentWrapperProps) {
return <div className={clsx("mx-auto max-w-7xl p-6", className)}>{children}</div>;
}

View File

@@ -0,0 +1,44 @@
// DemoPreview.tsx
import React, { useEffect, useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import { findTemplateByName } from "./templates";
import type { Template } from "@formbricks/types/templates";
interface DemoPreviewProps {
template: string;
}
const DemoPreview: React.FC<DemoPreviewProps> = ({ template }) => {
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const selectedTemplate: Template | undefined = findTemplateByName(template);
useEffect(() => {
if (selectedTemplate) {
setActiveQuestionId(selectedTemplate.preset.questions[0].id);
}
}, [selectedTemplate]);
if (!selectedTemplate) {
return <div>Template not found.</div>;
}
return (
<div className="mx-2 flex items-center justify-center rounded-xl border-2 border-slate-300 bg-slate-200 py-6 transition-transform duration-150 dark:border-slate-500 dark:bg-slate-700 md:mx-0">
<div className="flex flex-col items-center justify-around">
<p className="my-3 text-sm text-slate-500 dark:text-slate-300">Preview</p>
<div className="">
{selectedTemplate && (
<PreviewSurvey
activeQuestionId={activeQuestionId}
questions={selectedTemplate.preset.questions}
brandColor="#94a3b8"
setActiveQuestionId={setActiveQuestionId}
/>
)}
</div>
</div>
</div>
);
};
export default DemoPreview;

View File

@@ -0,0 +1,41 @@
import type { Template } from "@formbricks/types/templates";
import { useEffect, useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import TemplateList from "./TemplateList";
import { templates } from "./templates";
export default function SurveyTemplatesPage({}) {
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
useEffect(() => {
if (templates.length > 0) {
setActiveTemplate(templates[0]);
setActiveQuestionId(templates[0]?.preset.questions[0]?.id || null);
}
}, []);
return (
<div className="flex h-screen flex-col overflow-x-auto">
<div className="relative z-0 flex flex-1 overflow-hidden">
<TemplateList
activeTemplate={activeTemplate}
onTemplateClick={(template) => {
setActiveQuestionId(template.preset.questions[0].id);
setActiveTemplate(template);
}}
/>
<aside className="group relative h-full flex-1 flex-shrink-0 overflow-hidden rounded-r-lg bg-slate-200 shadow-inner dark:bg-slate-700 md:flex md:flex-col">
{activeTemplate && (
<PreviewSurvey
activeQuestionId={activeQuestionId}
questions={activeTemplate.preset.questions}
brandColor="#94a3b8"
setActiveQuestionId={setActiveQuestionId}
/>
)}
</aside>
</div>
</div>
);
}

View File

@@ -2,7 +2,7 @@ export const Headline: React.FC<{ headline: string; questionId: string }> = ({ h
return (
<label
htmlFor={questionId}
className="block text-base font-semibold leading-6 text-slate-900 dark:text-slate-100">
className="mb-1.5 block text-base font-semibold leading-6 text-slate-900 dark:text-slate-100">
{headline}
</label>
);

View File

@@ -0,0 +1,11 @@
/* import { cleanHtml } from "../../lib/cleanHtml"; */
import { cleanHtml } from "@formbricks/lib/cleanHtml";
export default function HtmlBody({ htmlString, questionId }: { htmlString: string; questionId: string }) {
return (
<label
htmlFor={questionId}
className="fb-block fb-font-normal fb-leading-6 text-sm text-slate-500 dark:text-slate-300"
dangerouslySetInnerHTML={{ __html: cleanHtml(htmlString) }}></label>
);
}

View File

@@ -1,25 +1,31 @@
import clsx from "clsx";
import { ReactNode, useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
export const Modal: React.FC<{ children: ReactNode; isOpen: boolean }> = ({ children, isOpen }) => {
export default function Modal({
children,
isOpen,
}: {
children: ReactNode;
isOpen: boolean;
reset: () => void;
}) {
const [show, setShow] = useState(false);
useEffect(() => {
setShow(isOpen);
}, [isOpen]);
return (
<div aria-live="assertive" className="pointer-events-none flex items-end px-4 py-6 sm:p-6">
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
<div aria-live="assertive" className="flex items-end">
<div className="flex w-full flex-col items-center p-4 sm:items-end md:min-w-[390px]">
<div
className={clsx(
className={cn(
show ? "translate-x-0 opacity-100" : "translate-x-28 opacity-0",
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white px-4 py-6 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out dark:bg-slate-700 sm:p-6"
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white px-4 py-6 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out dark:bg-slate-900 sm:p-6"
)}>
{children}
</div>
</div>
</div>
);
};
export default Modal;
}

View File

@@ -0,0 +1,107 @@
import { useState, useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceMultiProps {
question: MultipleChoiceMultiQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function MultipleChoiceMultiQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: MultipleChoiceMultiProps) {
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
const [isAtLeastOneChecked, setIsAtLeastOneChecked] = useState(false);
useEffect(() => {
setIsAtLeastOneChecked(selectedChoices.length > 0);
}, [selectedChoices]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (question.required && selectedChoices.length <= 0) {
return;
}
const data = {
[question.id]: selectedChoices,
};
onSubmit(data);
setSelectedChoices([]); // reset value
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="relative space-y-2 rounded-md bg-white dark:bg-slate-900">
{question.choices &&
question.choices.map((choice) => (
<label
key={choice.id}
className={cn(
selectedChoices.includes(choice.label)
? "z-10 border-slate-400 bg-slate-50 dark:border-slate-400 dark:bg-slate-600"
: "border-slate-200 dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600",
"relative flex cursor-pointer flex-col rounded-md border p-4 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
type="checkbox"
id={choice.id}
name={question.id}
value={choice.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0 dark:border-slate-600 dark:bg-slate-500"
aria-labelledby={`${choice.id}-label`}
checked={selectedChoices.includes(choice.label)}
onChange={(e) => {
if (e.currentTarget.checked) {
setSelectedChoices([...selectedChoices, e.currentTarget.value]);
} else {
setSelectedChoices(
selectedChoices.filter((label) => label !== e.currentTarget.value)
);
}
}}
style={{ borderColor: brandColor, color: brandColor }}
/>
<span
id={`${choice.id}-label`}
className="ml-3 font-medium text-slate-900 dark:text-slate-200">
{choice.label}
</span>
</span>
</label>
))}
</div>
</fieldset>
</div>
<input
type="text"
className="clip-[rect(0,0,0,0)] absolute m-[-1px] h-1 w-1 overflow-hidden whitespace-nowrap border-0 p-0 text-transparent caret-transparent focus:border-transparent focus:ring-0"
required={question.required}
value={isAtLeastOneChecked ? "checked" : ""}
onChange={() => {}}
/>
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</form>
);
}

View File

@@ -1,22 +1,22 @@
import clsx from "clsx";
import type { MultipleChoiceSingleQuestion as MultipleChoiceSingleQuestionType } from "./questionTypes";
import { cn } from "@formbricks/lib/cn";
import type { MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceSingleProps {
question: MultipleChoiceSingleQuestionType;
question: MultipleChoiceSingleQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> = ({
export default function MultipleChoiceSingleQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}) => {
}: MultipleChoiceSingleProps) {
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
return (
<form
@@ -26,9 +26,8 @@ export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> =
[question.id]: e.currentTarget[question.id].value,
};
e.currentTarget[question.id].value = "";
onSubmit(data);
// reset form
setSelectedChoice(null); // reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
@@ -37,14 +36,14 @@ export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> =
<legend className="sr-only">Options</legend>
<div className="relative space-y-2 rounded-md">
{question.choices &&
question.choices.map((choice) => (
question.choices.map((choice, idx) => (
<label
key={choice.id}
className={clsx(
className={cn(
selectedChoice === choice.label
? "z-10 border-slate-400 bg-slate-50 dark:border-slate-600 dark:bg-slate-600"
: "border-gray-200 dark:border-slate-500",
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-50 focus:outline-none dark:hover:bg-slate-600"
? "z-10 border-slate-400 bg-slate-50 dark:border-slate-400 dark:bg-slate-600"
: "border-slate-200 dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600",
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-50 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
@@ -52,16 +51,18 @@ export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> =
id={choice.id}
name={question.id}
value={choice.label}
className="h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0 dark:bg-slate-500"
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0 dark:border-slate-600 dark:bg-slate-500"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);
}}
checked={selectedChoice === choice.label}
style={{ borderColor: brandColor, color: brandColor }}
required={question.required && idx === 0}
/>
<span
id={`${choice.id}-label`}
className="ml-3 font-medium text-slate-800 dark:text-slate-300">
className="ml-3 font-medium text-slate-900 dark:text-slate-200">
{choice.label}
</span>
</span>
@@ -81,6 +82,4 @@ export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> =
</div>
</form>
);
};
export default MultipleChoiceSingleQuestion;
}

View File

@@ -0,0 +1,84 @@
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import type { NPSQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface NPSQuestionProps {
question: NPSQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function NPSQuestion({ question, onSubmit, lastQuestion, brandColor }: NPSQuestionProps) {
const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
const handleSelect = (number: number) => {
setSelectedChoice(number);
if (question.required) {
onSubmit({
[question.id]: number,
});
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = {
[question.id]: selectedChoice,
};
onSubmit(data);
// reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="my-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="flex">
{Array.from({ length: 11 }, (_, i) => i).map((number) => (
<label
key={number}
className={cn(
selectedChoice === number
? "z-10 bg-slate-50 dark:bg-slate-500"
: "dark:bg-slate-700 dark:hover:bg-slate-500",
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 text-slate-900 first:rounded-l-md last:rounded-r-md hover:bg-gray-100 focus:outline-none dark:border-slate-600 dark:text-white "
)}>
<input
type="radio"
name="nps"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
{number}
</label>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<p>{question.lowerLabel}</p>
<p>{question.upperLabel}</p>
</div>
</fieldset>
</div>
{!question.required && (
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
)}
</form>
);
}

View File

@@ -1,27 +1,33 @@
import type { OpenTextQuestion as OpenTextQuestionType } from "./questionTypes";
import type { OpenTextQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface OpenTextQuestionProps {
question: OpenTextQuestionType;
onSubmit: (id: string) => void;
question: OpenTextQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export const OpenTextQuestion: React.FC<OpenTextQuestionProps> = ({
export default function OpenTextQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}) => {
}: OpenTextQuestionProps) {
const [value, setValue] = useState<string>("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = "Pupsi";
const data = {
[question.id]: value,
};
setValue(""); // reset value
onSubmit(data);
// reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
@@ -30,9 +36,11 @@ export const OpenTextQuestion: React.FC<OpenTextQuestionProps> = ({
rows={3}
name={question.id}
id={question.id}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={question.placeholder}
required={question.required}
className="block w-full rounded-md border border-slate-100 bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:ring-0 dark:bg-slate-500 dark:text-white sm:text-sm"></textarea>
className="block w-full rounded-md border border-slate-100 bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:ring-0 dark:border-slate-500 dark:bg-slate-700 dark:text-white sm:text-sm"></textarea>
</div>
<div className="mt-4 flex w-full justify-between">
<div></div>
@@ -45,6 +53,4 @@ export const OpenTextQuestion: React.FC<OpenTextQuestionProps> = ({
</div>
</form>
);
};
export default OpenTextQuestion;
}

View File

@@ -1,78 +1,89 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import Modal from "./Modal";
import type { Question } from "./questionTypes";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import OpenTextQuestion from "./OpenTextQuestion";
import QuestionConditional from "./QuestionConditional";
import type { Question } from "@formbricks/types/questions";
import { Survey } from "@formbricks/types/surveys";
import ThankYouCard from "./ThankYouCard";
interface PreviewSurveyProps {
localSurvey?: Survey;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId?: string | null;
questions: Question[];
brandColor: string;
}
export const PreviewSurvey: React.FC<PreviewSurveyProps> = ({ activeQuestionId, questions, brandColor }) => {
export default function PreviewSurvey({
localSurvey,
setActiveQuestionId,
activeQuestionId,
questions,
brandColor,
}: PreviewSurveyProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null);
useEffect(() => {
if (activeQuestionId) {
if (currentQuestion && currentQuestion.id === activeQuestionId) {
setCurrentQuestion(questions.find((q) => q.id === activeQuestionId) || null);
return;
}
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions.find((q) => q.id === activeQuestionId) || null);
setIsModalOpen(true);
}, 300);
} else {
if (questions && questions.length > 0) {
setCurrentQuestion(questions[0]);
}
}
}, [activeQuestionId, questions]);
const gotoNextQuestion = () => {
if (currentQuestion) {
const currentIndex = questions.findIndex((q) => q.id === currentQuestion.id);
if (currentIndex < questions.length - 1) {
setCurrentQuestion(questions[currentIndex + 1]);
const currentIndex = questions.findIndex((q) => q.id === activeQuestionId);
if (currentIndex < questions.length - 1) {
setActiveQuestionId(questions[currentIndex + 1].id);
} else {
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
// start over
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
}
}
}
};
if (!currentQuestion) {
const resetPreview = () => {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
};
if (!activeQuestionId) {
return null;
}
const lastQuestion = currentQuestion.id === questions[questions.length - 1].id;
return (
<Modal isOpen={isModalOpen}>
{currentQuestion.type === "openText" ? (
<OpenTextQuestion
question={currentQuestion}
onSubmit={() => gotoNextQuestion()}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : currentQuestion.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={currentQuestion}
onSubmit={() => gotoNextQuestion()}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : null}
</Modal>
<>
<Modal isOpen={isModalOpen} reset={resetPreview}>
{activeQuestionId == "thank-you-card" ? (
<ThankYouCard
brandColor={brandColor}
headline={localSurvey?.thankYouCard?.headline || ""}
subheader={localSurvey?.thankYouCard?.subheader || ""}
/>
) : (
questions.map(
(question, idx) =>
activeQuestionId === question.id && (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
)
)
)}
</Modal>
</>
);
};
export default PreviewSurvey;
}

View File

@@ -0,0 +1,65 @@
import type { Question } from "@formbricks/types/questions";
import OpenTextQuestion from "./OpenTextQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
import NPSQuestion from "./NPSQuestion";
import CTAQuestion from "./CTAQuestion";
import RatingQuestion from "./RatingQuestion";
interface QuestionConditionalProps {
question: Question;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function QuestionConditional({
question,
onSubmit,
lastQuestion,
brandColor,
}: QuestionConditionalProps) {
return question.type === "openText" ? (
<OpenTextQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "multipleChoiceMulti" ? (
<MultipleChoiceMultiQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "nps" ? (
<NPSQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "cta" ? (
<CTAQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "rating" ? (
<RatingQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : null;
}

View File

@@ -0,0 +1,91 @@
import type { RatingQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface RatingQuestionProps {
question: RatingQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function RatingQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: RatingQuestionProps) {
const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
const handleSelect = (number: number) => {
setSelectedChoice(number);
if (question.required) {
onSubmit({
[question.id]: number,
});
setSelectedChoice(null); // reset choice
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = {
[question.id]: selectedChoice,
};
setSelectedChoice(null); // reset choice
onSubmit(data);
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="my-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="flex">
{Array.from({ length: question.range }, (_, i) => i + 1).map((number) => (
<label
key={number}
className={cn(
selectedChoice === number
? "z-10 border-slate-400 bg-slate-50"
: "bg-white hover:bg-gray-100 dark:bg-slate-700 dark:hover:bg-slate-500",
"relative h-10 flex-1 cursor-pointer border border-slate-100 text-center text-sm leading-10 text-slate-800 first:rounded-l-md last:rounded-r-md focus:outline-none dark:border-slate-500 dark:text-slate-200 "
)}>
<input
type="radio"
name="rating"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
{number}
</label>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<p>{question.lowerLabel}</p>
<p>{question.upperLabel}</p>
</div>
</fieldset>
</div>
{!question.required && (
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
)}
</form>
);
}

View File

@@ -1,218 +1,93 @@
import { OnboardingIcon } from "@formbricks/ui";
import { PlusCircleIcon } from "@heroicons/react/24/outline";
import { createId } from "@paralleldrive/cuid2";
import clsx from "clsx";
import { useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import type { Template } from "./templateTypes";
import type { Template } from "@formbricks/types/templates";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { templates } from "./templates";
export const TemplateList: React.FC = () => {
const onboardingSegmentation: Template = {
name: "Onboarding Segmentation",
icon: OnboardingIcon,
category: "Product Management",
description: "Learn more about who signed up to your product and why.",
preset: {
name: "Onboarding Segmentation",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What is your role?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Founder",
},
{
id: createId(),
label: "Executive",
},
{
id: createId(),
label: "Product Manager",
},
{
id: createId(),
label: "Product Owner",
},
{
id: createId(),
label: "Software Engineer",
},
],
},
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What's your company size?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "only me",
},
{
id: createId(),
label: "1-5 employees",
},
{
id: createId(),
label: "6-10 employees",
},
{
id: createId(),
label: "11-100 employees",
},
{
id: createId(),
label: "over 100 employees",
},
],
},
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How did you hear about us first?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Recommendation",
},
{
id: createId(),
label: "Social Media",
},
{
id: createId(),
label: "Ads",
},
{
id: createId(),
label: "Google Search",
},
{
id: createId(),
label: "in a Podcast",
},
],
},
],
},
};
const [activeTemplate, setActiveTemplate] = useState<Template | null>(onboardingSegmentation);
const categories = [
"All",
...(Array.from(new Set(templates.map((template) => template.category))) as string[]),
];
const [selectedFilter, setSelectedFilter] = useState(categories[0]);
const customSurvey: Template = {
name: "Custom Survey",
description: "Create your survey from scratch.",
icon: null,
preset: {
name: "New Survey",
questions: [
{
id: createId(),
type: "openText",
headline: "What's poppin?",
subheader: "This can help us improve your experience.",
placeholder: "Type your answer here...",
required: true,
},
],
},
};
return (
<div>
<div className="mt-6 hidden flex-col md:flex">
<div className="z-0 flex min-h-[90vh] overflow-hidden">
<main className="relative z-0 max-h-[90vh] flex-1 overflow-y-auto px-6 pb-6 focus:outline-none dark:bg-slate-700">
<div className="mb-6 flex space-x-2">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setSelectedFilter(category)}
className={clsx(
selectedFilter === category
? "text-brand-dark border-brand-dark font-semibold"
: "border-slate-300 text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-400",
"rounded border bg-slate-50 px-3 py-1 text-xs transition-all duration-150 dark:bg-slate-800 "
)}>
{category}
</button>
))}
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{templates
.filter((template) => selectedFilter === "All" || template.category === selectedFilter)
.map((template: Template) => (
<button
type="button"
onClick={() => setActiveTemplate(template)}
key={template.name}
className={clsx(
activeTemplate?.name === template.name && "ring-brand ring-2",
"duration-120 group relative rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105 dark:bg-slate-600"
)}>
<div className="absolute right-6 top-6 rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500 dark:border-slate-500 dark:bg-slate-700 dark:text-slate-300">
{template.category}
</div>
<template.icon className="h-8 w-8" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 dark:text-slate-200">
{template.name}
</h3>
<p className="text-left text-xs text-slate-600 dark:text-slate-400">
{template.description}
</p>
</button>
))}
<button
type="button"
onClick={() => setActiveTemplate(customSurvey)}
className={clsx(
activeTemplate?.name === customSurvey.name && "ring-brand ring-2",
"duration-120 hover:border-brand-dark group relative rounded-lg border-2 border-dashed border-slate-300 bg-transparent p-8 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 dark:text-slate-200">
{customSurvey.name}
</h3>
<p className="text-left text-xs text-slate-600 dark:text-slate-400">
{customSurvey.description}
</p>
</button>
</div>
</main>
<aside className="group relative hidden max-h-[90vh] flex-1 flex-shrink-0 overflow-hidden rounded-r-lg border-l border-slate-200 bg-slate-200 shadow-inner dark:border-slate-700 dark:bg-slate-800 md:flex md:flex-col">
{activeTemplate && (
<PreviewSurvey
activeQuestionId={null}
questions={activeTemplate.preset.questions}
brandColor="#00C4B8"
/>
)}
</aside>
</div>
</div>
<div className="flex items-center justify-center pt-36 text-slate-600 md:hidden">
This demo is not yet optimized for smartphones.
</div>
</div>
);
type TemplateList = {
onTemplateClick: (template: Template) => void;
activeTemplate: Template | null;
};
export default TemplateList;
const ALL_CATEGORY_NAME = "All";
export default function TemplateList({ onTemplateClick, activeTemplate }: TemplateList) {
const [selectedFilter, setSelectedFilter] = useState(ALL_CATEGORY_NAME);
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 = [ALL_CATEGORY_NAME, ...defaultCategories];
setCategories(fullCategories);
const activeFilter = ALL_CATEGORY_NAME;
setSelectedFilter(activeFilter);
}, []);
return (
<main className="relative z-0 flex-1 overflow-y-auto rounded-l-lg bg-slate-100 px-8 py-6 focus:outline-none dark:bg-slate-800">
<div className="mb-6 flex flex-wrap space-x-1">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setSelectedFilter(category)}
className={cn(
selectedFilter === category
? "text-brand-dark border-brand-dark font-semibold"
: "border-slate-300 text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-300",
"mt-2 rounded border bg-slate-50 px-3 py-1 text-xs transition-all duration-150 dark:bg-slate-600 dark:hover:bg-slate-500"
)}>
{category}
</button>
))}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* <button
type="button"
onClick={() => {
onTemplateClick(activeTemplate);
setActiveTemplate(activeTemplate);
}}
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-8 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>
</button> */}
{templates
.filter((template) => selectedFilter === ALL_CATEGORY_NAME || template.category === selectedFilter)
.map((template: Template) => (
<button
type="button"
onClick={() => {
onTemplateClick(template); // Pass the 'template' object instead of 'activeTemplate'
}}
key={template.name}
className={cn(
activeTemplate?.name === template.name && "ring-brand ring-2",
"duration-120 group relative rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105 dark:bg-slate-700"
)}>
<div className="absolute right-6 top-6 rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500 dark:border-slate-400 dark:bg-slate-800 dark:text-slate-400">
{template.category}
</div>
<template.icon className="h-8 w-8" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 dark:text-slate-300">
{template.name}
</h3>
<p className="text-left text-xs text-slate-600 dark:text-slate-400">{template.description}</p>
</button>
))}
</div>
</main>
);
}

View File

@@ -0,0 +1,52 @@
import Headline from "./Headline";
import Subheader from "./Subheader";
interface ThankYouCardProps {
headline: string;
subheader: string;
brandColor: string;
}
export default function ThankYouCard({ headline, subheader, brandColor }: ThankYouCardProps) {
return (
<div className="text-center">
<div className="flex items-center justify-center" style={{ color: brandColor }}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-24 w-24">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<span className="mb-[10px] inline-block h-1 w-16 rounded-[100%] bg-slate-300"></span>
<div>
<Headline headline={headline} questionId="thankYouCard" />
<Subheader subheader={subheader} questionId="thankYouCard" />
</div>
{/* <span
className="mb-[10px] mt-[35px] inline-block h-[2px] w-4/5 rounded-full opacity-25"
style={{ backgroundColor: brandColor }}></span>
<div>
<p className="text-xs text-slate-500">
Powered by{" "}
<b>
<a href="https://formbricks.com" target="_blank" className="hover:text-slate-700">
Formbricks
</a>
</b>
</p>
</div> */}
</div>
);
}

View File

@@ -1,26 +0,0 @@
export type Question = OpenTextQuestion | MultipleChoiceSingleQuestion;
export interface OpenTextQuestion {
id: string;
type: "openText";
headline: string;
subheader?: string;
placeholder?: string;
buttonLabel?: string;
required: boolean;
}
export interface MultipleChoiceSingleQuestion {
id: string;
type: "multipleChoiceSingle";
headline: string;
subheader?: string;
required: boolean;
buttonLabel?: string;
choices: Choice[];
}
export interface Choice {
id: string;
label: string;
}

View File

@@ -1,12 +0,0 @@
import { Question } from "./questionTypes";
export interface Template {
name: string;
icon: any;
description: string;
category?: "All" | "Product Management" | "Growth Marketing" | "Increase Revenue";
preset: {
name: string;
questions: Question[];
};
}

View File

@@ -1,27 +1,144 @@
import {
AppPieChartIcon,
ArrowRightCircleIcon,
ArrowUpRightIcon,
BaseballIcon,
CancelSubscriptionIcon,
CashCalculatorIcon,
CheckMarkIcon,
CodeBookIcon,
DashboardIcon,
DogChaserIcon,
DoorIcon,
FeedbackIcon,
GaugeSpeedFastIcon,
HeartCommentIcon,
InterviewPromptIcon,
LoadingBarIcon,
OnboardingIcon,
PMFIcon,
TaskListSearchIcon,
UserSearchGlasIcon,
VideoTabletAdjustIcon,
} from "@formbricks/ui";
import { createId } from "@paralleldrive/cuid2";
import type { Template } from "./templateTypes";
import type { Template } from "@formbricks/types/templates";
const thankYouCardDefault = {
enabled: true,
headline: "Thank you!",
subheader: "We appreciate your time and insight.",
};
export const customSurvey: Template = {
name: "Custom Survey",
description: "Create your survey from scratch.",
icon: null,
preset: {
name: "New Survey",
questions: [
{
id: createId(),
type: "openText",
headline: "What's poppin?",
subheader: "This can help us improve your experience.",
placeholder: "Type your answer here...",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
};
export const templates: Template[] = [
{
name: "Product Market Fit (Superhuman)",
icon: PMFIcon,
category: "Product Experience",
description: "Measure PMF by assessing how disappointed users would be if your product disappeared.",
preset: {
name: "Product Market Fit (Superhuman)",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How disappointed would you be if you could no longer use Formbricks?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Not at all disappointed",
},
{
id: createId(),
label: "Somewhat disappointed",
},
{
id: createId(),
label: "Very disappointed",
},
],
},
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What is your role?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Founder",
},
{
id: createId(),
label: "Executive",
},
{
id: createId(),
label: "Product Manager",
},
{
id: createId(),
label: "Product Owner",
},
{
id: createId(),
label: "Software Engineer",
},
],
},
{
id: createId(),
type: "openText",
headline: "What type of people do you think would most benefit from Formbricks?",
required: true,
},
{
id: createId(),
type: "openText",
headline: "What is the main benefit your receive from Formbricks?",
required: true,
},
{
id: createId(),
type: "openText",
headline: "How can we improve our service for you?",
subheader: "Please be as specific as possible.",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Onboarding Segmentation",
icon: OnboardingIcon,
category: "Product Management",
category: "Product Experience",
description: "Learn more about who signed up to your product and why.",
preset: {
name: "Onboarding Segmentation",
@@ -114,209 +231,13 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Product Market Fit Survey",
icon: PMFIcon,
category: "Product Management",
description: "Measure PMF by assessing how disappointed users would be if your product disappeared.",
preset: {
name: "Product Market Fit Survey",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How disappointed would you be if you could no longer use Formbricks?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Not at all disappointed",
},
{
id: createId(),
label: "Somewhat disappointed",
},
{
id: createId(),
label: "Very disappointed",
},
],
},
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What is your role?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Founder",
},
{
id: createId(),
label: "Executive",
},
{
id: createId(),
label: "Product Manager",
},
{
id: createId(),
label: "Product Owner",
},
{
id: createId(),
label: "Software Engineer",
},
],
},
{
id: createId(),
type: "openText",
headline: "How can we improve our service for you?",
subheader: "Please be as specific as possible.",
required: true,
},
],
},
},
{
name: "Pre-Churn Survey",
icon: CancelSubscriptionIcon,
category: "Product Management",
description: "Find out why people cancel you. These insights are pure gold!",
preset: {
name: "Churn Survey",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Why do you cancel your subscription?",
subheader: "We're sorry to see you leave. Please help us do better:",
required: true,
choices: [
{
id: createId(),
label: "I don't get much value out of it",
},
{
id: createId(),
label: "It's too expensive",
},
{
id: createId(),
label: "I am missing a feature",
},
{
id: createId(),
label: "Poor customer service",
},
{
id: createId(),
label: "I just don't need you anymore",
},
],
},
{
id: createId(),
type: "openText",
headline: "Is there something we can do to win you back?",
subheader: "Feel free to speak your mind, we do too.",
required: false,
},
],
},
},
{
name: "Feature Chaser",
icon: DogChaserIcon,
category: "Product Management",
description: "Follow up with users who just used a specific feature.",
preset: {
name: "Feature Chaser",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How easy was it to achieve your goal?",
required: true,
choices: [
{
id: createId(),
label: "Extremely difficult",
},
{
id: createId(),
label: "It took a while, but I got it",
},
{
id: createId(),
label: "It was alright",
},
{
id: createId(),
label: "Quite easy",
},
{
id: createId(),
label: "Very easy, love it!",
},
],
},
{
id: createId(),
type: "openText",
headline: "Wanna add something?",
subheader: "This really helps us do better!",
required: false,
},
],
},
},
{
name: "Feedback Box",
icon: FeedbackIcon,
category: "Product Management",
description: "Give your users the chance to seamlessly share what's on their minds.",
preset: {
name: "Feedback Box",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What's on your mind, boss?",
subheader: "Thanks for sharing. We'll get back to you asap.",
required: true,
choices: [
{
id: createId(),
label: "Bug report 🐞",
},
{
id: createId(),
label: "Feature Request 💡",
},
],
},
{
id: createId(),
type: "openText",
headline: "Give us the juicy details:",
required: true,
},
],
},
},
{
name: "Uncover Strengths & Weaknesses",
icon: TaskListSearchIcon,
category: "Growth Marketing",
category: "Growth",
description: "Find out what users like and don't like about your product or offering.",
preset: {
name: "Uncover Strengths & Weaknesses",
@@ -379,12 +300,13 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Marketing Attribution",
icon: AppPieChartIcon,
category: "Growth Marketing",
category: "Growth",
description: "How did you first hear about us?",
preset: {
name: "Marketing Attribution",
@@ -419,22 +341,70 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Missed Trial Conversion",
name: "Churn Survey",
icon: CancelSubscriptionIcon,
category: "Increase Revenue",
description: "Find out why people cancel their subscriptions. These insights are pure gold!",
preset: {
name: "Churn Survey",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Why did you cancel your subscription?",
subheader: "We're sorry to see you leave. Please help us do better:",
required: true,
choices: [
{
id: createId(),
label: "I didn't get much value out of it",
},
{
id: createId(),
label: "It's too expensive",
},
{
id: createId(),
label: "I am missing a feature",
},
{
id: createId(),
label: "Poor customer service",
},
{
id: createId(),
label: "I just didn't need it anymore",
},
],
},
{
id: createId(),
type: "openText",
headline: "How can we win you back?",
subheader: "Feel free to speak your mind, we do too.",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Improve Trial Conversion",
icon: BaseballIcon,
category: "Increase Revenue",
description: "Find out why people stopped their trial. These insights help you improve your funnel.",
preset: {
name: "Missed Trial Conversion",
name: "Improve Trial Conversion",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Why did you stop your trial?",
subheader: "Help us understand you better. Choose one option:",
subheader: "Help us understand you better:",
required: true,
choices: [
{
@@ -462,10 +432,18 @@ export const templates: Template[] = [
{
id: createId(),
type: "openText",
headline: "Did you find a better alternative? Please name it:",
headline: "Any details to share?",
required: false,
},
{
id: createId(),
type: "openText",
headline: "How are you solving your problem instead?",
subheader: "Please name alternative tools:",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -525,12 +503,13 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Measure Task Accomplishment",
icon: CheckMarkIcon,
category: "Product Management",
category: "Product Experience",
description: "See if people get their 'Job To Be Done' done. Successful people are better customers.",
preset: {
name: "Measure Task Accomplishment",
@@ -555,6 +534,16 @@ export const templates: Template[] = [
},
],
},
{
id: createId(),
type: "rating",
headline: "How easy was it to achieve your goal?",
required: true,
lowerLabel: "Very difficult",
upperLabel: "Very easy",
range: 5,
scale: "number",
},
{
id: createId(),
type: "openText",
@@ -562,12 +551,13 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Identify Customer Goals",
icon: ArrowRightCircleIcon,
category: "Product Management",
category: "Product Experience",
description:
"Better understand if your messaging creates the right expectations of the value your product provides.",
preset: {
@@ -598,45 +588,140 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Feature Chaser",
icon: DogChaserIcon,
category: "Product Experience",
description: "Follow up with users who just used a specific feature.",
preset: {
name: "Feature Chaser",
questions: [
{
id: createId(),
type: "rating",
headline: "How easy was it to achieve your goal?",
required: true,
lowerLabel: "Very difficult",
upperLabel: "Very easy",
range: 5,
scale: "number",
},
{
id: createId(),
type: "openText",
headline: "Wanna add something?",
subheader: "This really helps us do better!",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Fake Door Follow-Up",
icon: DoorIcon,
category: "Product Management",
category: "Exploration",
description: "Follow up with users who ran into one of your Fake Door experiments.",
preset: {
name: "Fake Door Follow-Up",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
type: "rating",
headline: "How important is this feature for you?",
required: true,
lowerLabel: "Not important",
upperLabel: "Very important",
range: 5,
scale: "number",
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Product Market Fit Survey (short)",
icon: PMFIcon,
category: "Product Experience",
description: "Measure PMF by assessing how disappointed users would be if your product disappeared.",
preset: {
name: "Product Market Fit Survey (short)",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How disappointed would you be if you could no longer use Formbricks?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Very important",
label: "Not at all disappointed",
},
{
id: createId(),
label: "Not so important",
label: "Somewhat disappointed",
},
{
id: createId(),
label: "I was just looking around",
label: "Very disappointed",
},
],
},
{
id: createId(),
type: "openText",
headline: "How can we improve our service for you?",
subheader: "Please be as specific as possible.",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Feedback Box",
icon: FeedbackIcon,
category: "Product Experience",
description: "Give your users the chance to seamlessly share what's on their minds.",
preset: {
name: "Feedback Box",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What's on your mind, boss?",
subheader: "Thanks for sharing. We'll get back to you asap.",
required: true,
choices: [
{
id: createId(),
label: "Bug report 🐞",
},
{
id: createId(),
label: "Feature Request 💡",
},
],
},
{
id: createId(),
type: "openText",
headline: "Give us the juicy details:",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Integration usage survey",
icon: DashboardIcon,
category: "Product Management",
category: "Product Experience",
description: "Evaluate how easily users can add integrations to your product. Find blind spots.",
preset: {
name: "Integration Usage Survey",
@@ -677,24 +762,300 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
/* {
name: "In-app Interview Prompt",
icon: OnboardingIcon,
description: "Invite a specific subset of your users to schedule an interview with your product team.",
{
name: "New integration survey",
icon: DashboardIcon,
category: "Exploration",
description: "Find out which integrations your users would like to see next.",
preset: {
name: "In-app Interview Prompt",
name: "New integration survey",
questions: [
{
id: createId(),
type: "prompt",
headline: "Wanna do a short 15m interview with Charly?",
subheader: "That would really help us",
buttonLabel: "Book slot",
buttonUrl: "https://cal.com/formbricks",
type: "multipleChoiceSingle",
headline: "Which other tools are you using?",
required: true,
choices: [
{
id: createId(),
label: "PostHog",
},
{
id: createId(),
label: "Segment",
},
{
id: createId(),
label: "Hubspot",
},
{
id: createId(),
label: "Twilio",
},
{
id: createId(),
label: "Other",
},
],
},
{
id: createId(),
type: "openText",
headline: "If you chose other, please clarify:",
required: false,
},
],
},s
}, */
thankYouCard: thankYouCardDefault,
},
},
{
name: "Docs Feedback",
icon: CodeBookIcon,
category: "Product Experience",
description: "Measure how clear each page of your developer documentation is.",
preset: {
name: "Formbricks Docs Feedback",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Was this page helpful?",
required: true,
choices: [
{
id: createId(),
label: "Yes 👍",
},
{
id: createId(),
label: "No 👎",
},
],
},
{
id: createId(),
type: "openText",
headline: "Please elaborate:",
required: false,
},
{
id: createId(),
type: "openText",
headline: "Page URL",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Interview Prompt",
icon: InterviewPromptIcon,
category: "Exploration",
description: "Invite a specific subset of your users to schedule an interview with your product team.",
preset: {
name: "Interview Prompt",
questions: [
{
id: createId(),
type: "cta",
headline: "Do you have 15 min to talk to us? 🙏",
html: "You're one of our power users. We would love to interview you briefly!",
buttonLabel: "Book interview",
buttonUrl: "https://cal.com/johannes/onboarding?duration=25",
buttonExternal: true,
required: false,
dismissButtonLabel: "Maybe later",
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Review Prompt",
icon: HeartCommentIcon,
category: "Growth",
description: "Invite users who love your product to review it publicly.",
preset: {
name: "Review Prompt",
questions: [
{
id: createId(),
type: "cta",
headline: "You're one of our most valued customers! Please write a review for us.",
buttonLabel: "Write review",
buttonUrl: "https://formbricks.com/github",
buttonExternal: true,
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Net Promoter Score (NPS)",
icon: GaugeSpeedFastIcon,
category: "Customer Success",
description: "Measure the Net Promoter Score of your product.",
preset: {
name: "Formbricks NPS",
questions: [
{
id: createId(),
type: "nps",
headline: "How likely are you to recommend Formbricks to a friend or colleague?",
required: false,
lowerLabel: "Not likely",
upperLabel: "Very likely",
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Identify upsell opportunities",
icon: ArrowUpRightIcon,
category: "Increase Revenue",
description: "Find out how much time your product saves your user. Use it to upsell.",
preset: {
name: "Identify upsell opportunities",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How many hours does your team save per week by using Formbricks?",
required: true,
choices: [
{
id: createId(),
label: "Less than 1 hour",
},
{
id: createId(),
label: "1 to 2 hours",
},
{
id: createId(),
label: "3 to 5 hours",
},
{
id: createId(),
label: "5+ hours",
},
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Build Product Roadmap",
icon: LoadingBarIcon,
category: "Product Experience",
description: "Ask how users rate your product. Identify blind spots to build your roadmap.",
preset: {
name: "Build Product Roadmap",
questions: [
{
id: createId(),
type: "rating",
headline: "How satisfied are you with the features of Formbricks?",
required: true,
lowerLabel: "Not satisfied",
upperLabel: "Very satisfied",
scale: "number",
range: 5,
},
{
id: createId(),
type: "openText",
headline: "What's the #1 thing you'd like to change in Formbricks?",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Gauge Feature Satisfaction",
icon: UserSearchGlasIcon,
category: "Product Experience",
description: "Evaluate the satisfaction of specific features of your product.",
preset: {
name: "Gauge Feature Satisfaction",
questions: [
{
id: createId(),
type: "rating",
headline: "How easy was it to achieve ... ?",
required: true,
lowerLabel: "Not easy",
upperLabel: "Very easy",
scale: "number",
range: 5,
},
{
id: createId(),
type: "openText",
headline: "What is one thing we could do better?",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Marketing Site Clarity",
icon: VideoTabletAdjustIcon,
category: "Growth",
description: "Identify users dropping off your marketing site. Improve your messaging.",
preset: {
name: "Marketing Site Clarity",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Do you have all the info you need to give Formbricks a try?",
required: true,
choices: [
{
id: createId(),
label: "Yes, totally",
},
{
id: createId(),
label: "Kind of...",
},
{
id: createId(),
label: "No, not at all",
},
],
},
{
id: createId(),
type: "openText",
headline: "Whats missing or unclear to you about Formbricks?",
required: false,
},
{
id: createId(),
type: "cta",
headline: "Thanks for your answer! Get 25% off your first 6 months:",
required: false,
buttonLabel: "Get discount",
buttonUrl: "https://app.formbricks.com/auth/signup",
buttonExternal: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
];
export const findTemplateByName = (name: string): Template | undefined => {
return templates.find((template) => template.name === name);
};

View File

@@ -38,63 +38,63 @@ export const Hero: React.FC = ({}) => {
</span>
</p>
<div className="mx-auto mt-5 max-w-3xl items-center space-x-8 sm:flex sm:justify-center md:mt-8">
<div className="mx-auto mt-5 max-w-3xl items-center px-4 sm:flex sm:justify-center md:mt-8 md:space-x-8 md:px-0">
<p className="hidden whitespace-nowrap pt-3 text-xs text-slate-400 dark:text-slate-500 md:block">
Trusted by
</p>
<div className="grid grid-cols-5 items-center gap-8 pt-2">
<div className="grid grid-cols-3 items-center gap-8 pt-2 md:grid-cols-5">
<Image
src={CalLogoLight}
alt="Cal Logo"
className="block rounded-lg opacity-50 hover:opacity-100 dark:hidden"
className="block rounded-lg hover:opacity-100 dark:hidden md:opacity-50"
width={170}
/>
<Image
src={CalLogoDark}
alt="Cal Logo"
className="hidden rounded-lg opacity-50 hover:opacity-100 dark:block"
className="hidden rounded-lg hover:opacity-100 dark:block md:opacity-50"
width={170}
/>
<Image
src={CrowdLogoLight}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 opacity-50 hover:opacity-100 dark:hidden"
className="block rounded-lg pb-1 hover:opacity-100 dark:hidden md:opacity-50"
width={200}
/>
<Image
src={CrowdLogoDark}
alt="Crowd.dev Logo"
className="hidden rounded-lg pb-1 opacity-50 hover:opacity-100 dark:block"
className="hidden rounded-lg pb-1 hover:opacity-100 dark:block md:opacity-50"
width={200}
/>
<Image
src={ClovyrLogo}
alt="Clovyr Logo"
className="rounded-lg pb-1 opacity-50 hover:opacity-100"
className="rounded-lg pb-1 hover:opacity-100 md:opacity-50"
width={200}
/>
<Image
src={NILogoDark}
alt="Neverinstall Logo"
className="block pb-1 opacity-50 hover:opacity-100 dark:hidden"
className="block pb-1 hover:opacity-100 dark:hidden md:opacity-50"
width={200}
/>
<Image
src={NILogoLight}
alt="Neverinstall Logo"
className="hidden pb-1 opacity-50 hover:opacity-100 dark:block"
className="hidden pb-1 hover:opacity-100 dark:block md:opacity-50"
width={200}
/>
<Image
src={StackOceanLogoLight}
alt="StackOcean Logo"
className="block pb-1 opacity-50 hover:opacity-100 dark:hidden"
className="block pb-1 hover:opacity-100 dark:hidden md:opacity-50"
width={200}
/>
<Image
src={StackOceanLogoDark}
alt="StakcOcean Logo"
className="hidden pb-1 opacity-50 hover:opacity-100 dark:block"
className="hidden pb-1 hover:opacity-100 dark:block md:opacity-50"
width={200}
/>
</div>
@@ -120,7 +120,7 @@ export const Hero: React.FC = ({}) => {
</Button>
</div>
</div>
<div className="relative">
<div className="relative px-2 md:px-0">
<HeroAnimation fallbackImage={AnimationFallback} />
</div>
</div>

View File

@@ -1,3 +1,4 @@
import DemoPreview from "@/components/dummyUI/DemoPreview";
import DashboardMockupDark from "@/images/dashboard-mockup-dark.png";
import DashboardMockup from "@/images/dashboard-mockup.png";
import { Button } from "@formbricks/ui";
@@ -6,71 +7,9 @@ import Image from "next/image";
import { useState } from "react";
import AddEventDummy from "../dummyUI/AddEventDummy";
import AddNoCodeEventModalDummy from "../dummyUI/AddNoCodeEventModalDummy";
import PreviewSurvey from "../dummyUI/PreviewSurvey";
import type { Question } from "../dummyUI/questionTypes";
import HeadingCentered from "../shared/HeadingCentered";
import SetupTabs from "./SetupTabs";
const questions: Question[] = [
{
id: "1",
type: "multipleChoiceSingle",
headline: "How disappointed would you be if you could no longer use Formbricks?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: "2",
label: "Not at all disappointed",
},
{
id: "3",
label: "Somewhat disappointed",
},
{
id: "4",
label: "Very disappointed",
},
],
},
{
id: "5",
type: "multipleChoiceSingle",
headline: "What is your role?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: "6",
label: "Founder",
},
{
id: "7",
label: "Executive",
},
{
id: "8",
label: "Product Manager",
},
{
id: "9",
label: "Product Owner",
},
{
id: "10",
label: "Software Engineer",
},
],
},
{
id: "11",
type: "openText",
headline: "How can we improve Formbricks for you?",
subheader: "Please be as specific as possible.",
required: true,
},
];
export const Steps: React.FC = () => {
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
@@ -108,7 +47,7 @@ export const Steps: React.FC = () => {
<div className="flex h-40 items-center justify-center">
<Button
variant="primary"
className="animate-bounce transition-all duration-150 hover:scale-105"
className=""
onClick={() => {
setAddEventModalOpen(true);
}}>
@@ -143,8 +82,8 @@ export const Steps: React.FC = () => {
adjust the look and feel of your survey.
</p>
</div>
<div className="relative w-full rounded-lg bg-slate-100 p-1 dark:bg-slate-800 sm:p-8">
<PreviewSurvey questions={questions} brandColor="#00C4B8" />
<div className="relative w-full rounded-lg p-1 dark:bg-slate-800 sm:p-8">
<DemoPreview template="Product Market Fit Survey (short)" />
</div>
</div>
</div>

View File

@@ -0,0 +1,114 @@
import {
CancelSubscriptionIcon,
DogChaserIcon,
FeedbackIcon,
InterviewPromptIcon,
OnboardingIcon,
PMFIcon,
BaseballIcon,
CodeBookIcon,
} from "@formbricks/ui";
import clsx from "clsx";
import Link from "next/link";
export default function BestPracticeNavigation() {
const BestPractices = [
{
name: "Interview Prompt",
href: "/interview-prompt",
status: true,
icon: InterviewPromptIcon,
description: "Ask only power users users to book a time in your calendar. Get those juicy details.",
category: "Understand Users",
},
{
name: "Product-Market Fit Survey",
href: "/measure-product-market-fit",
status: true,
icon: PMFIcon,
description: "Find out how disappointed people would be if they could not use your service any more.",
category: "Understand Users",
},
{
name: "Onboarding Segments",
href: "/onboarding-segmentation",
status: false,
icon: OnboardingIcon,
description:
"Get to know your users right from the start. Ask a few questions early, let us enrich the profile.",
category: "Understand Users",
},
{
name: "Learn from Churn",
href: "/learn-from-churn",
status: true,
icon: CancelSubscriptionIcon,
description: "Churn is hard, but insightful. Learn from users who changed their mind.",
category: "Increase Revenue",
},
{
name: "Improve Trial CR",
href: "/improve-trial-conversion",
status: true,
icon: BaseballIcon,
description: "Take guessing out, convert more trials to paid users with insights.",
category: "Increase Revenue",
},
{
name: "Docs Feedback",
href: "/docs-feedback",
status: true,
icon: CodeBookIcon,
description: "Clear docs lead to more adoption. Understand granularly what's confusing.",
category: "Boost Retention",
},
{
name: "Feature Chaser",
href: "/feature-chaser",
status: true,
icon: DogChaserIcon,
description: "Show a survey about a new feature shown only to people who used it.",
category: "Boost Retention",
},
{
name: "Feedback Box",
href: "/feedback-box",
status: true,
icon: FeedbackIcon,
description: "Give users the chance to share feedback in a single click.",
category: "Boost Retention",
},
];
return (
<div className=" mx-auto grid grid-cols-1 gap-6 px-2 sm:grid-cols-3">
{BestPractices.map((bestPractice) => (
<Link href={bestPractice.href} key={bestPractice.name}>
<div className="drop-shadow-card duration-120 relative rounded-lg bg-slate-100 p-8 transition-all ease-in-out hover:scale-105 hover:cursor-pointer dark:bg-slate-800">
<div
className={clsx(
// base styles independent what type of button it is
"absolute right-10 rounded-full px-3 py-1",
// different styles depending on type
bestPractice.category === "Boost Retention" &&
"bg-pink-100 text-pink-500 dark:bg-pink-800 dark:text-pink-200",
bestPractice.category === "Increase Revenue" &&
"bg-blue-100 text-blue-500 dark:bg-blue-800 dark:text-blue-200",
bestPractice.category === "Understand Users" &&
"bg-orange-100 text-orange-500 dark:bg-orange-800 dark:text-orange-200"
)}>
{bestPractice.category}
</div>
<div className="h-12 w-12">
<bestPractice.icon className="h-12 w-12 " />
</div>
<h3 className="mb-1 mt-3 text-xl font-bold text-slate-700 dark:text-slate-200">
{bestPractice.name}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">{bestPractice.description}</p>
</div>
</Link>
))}
</div>
);
}

View File

@@ -1,71 +1,7 @@
import { Button } from "@formbricks/ui";
import {
AngryBirdRageIcon,
CancelSubscriptionIcon,
DogChaserIcon,
DoorIcon,
FeedbackIcon,
InterviewPromptIcon,
OnboardingIcon,
PMFIcon,
} from "@formbricks/ui";
import clsx from "clsx";
import { usePlausible } from "next-plausible";
import { useRouter } from "next/router";
const BestPractices = [
{
title: "Onboarding Segmentation",
description:
"Get to know your users right from the start. Ask a few questions early, let us enrich the profile.",
category: "Boost Retention",
icon: OnboardingIcon,
},
{
title: "Product-Market Fit Survey",
description: "Find out how disappointed people would be if they could not use your service any more.",
category: "Boost Retention",
icon: PMFIcon,
href: "/pmf",
},
{
title: "Feature Chaser",
description: "Show a survey about a new feature shown only to people who used it.",
category: "Boost Retention",
icon: DogChaserIcon,
},
{
title: "Cancel Subscription Flow",
description: "Request users going through a cancel subscription flow before cancelling.",
category: "Boost Retention",
icon: CancelSubscriptionIcon,
},
{
title: "Interview Prompt",
description: "Ask high-interest users to book a time in your calendar to get all the juicy details.",
category: "Exploration",
icon: InterviewPromptIcon,
},
{
title: "Fake Door Follow-Up",
description: "Running a fake door experiment? Catch users right when they are full of expectations.",
category: "Exploration",
icon: DoorIcon,
},
{
title: "Feedback Box",
description: "Give users the chance to share feedback in a single click.",
category: "Retain Users",
icon: FeedbackIcon,
},
{
title: "Rage Click Survey",
description: "Sometimes things dont work. Trigger this rage click survey to catch users in rage.",
category: "Retain Users",
icon: AngryBirdRageIcon,
},
];
import BestPracticeNavigation from "./BestPracticeNavigation";
export default function InsightOppos() {
const plausible = usePlausible();
@@ -83,40 +19,9 @@ export default function InsightOppos() {
Run battle-tested approaches for qualitative user research in minutes.
</p>
</div>
<div>
<div className=" mx-auto grid max-w-5xl grid-cols-1 gap-6 px-2 sm:grid-cols-2">
{BestPractices.map((bestPractice) => {
const IconComponent: React.ElementType = bestPractice.icon;
return (
<div
key={bestPractice.title}
className="drop-shadow-card duration-120 relative rounded-lg bg-slate-100 p-8 transition-all ease-in-out hover:scale-105 dark:bg-slate-800">
<div
className={clsx(
// base styles independent what type of button it is
"absolute right-10 rounded-full px-3 py-1",
// different styles depending on type
bestPractice.category === "Boost Retention" &&
"bg-pink-100 text-pink-500 dark:bg-pink-800 dark:text-pink-200",
bestPractice.category === "Exploration" &&
"bg-blue-100 text-blue-500 dark:bg-blue-800 dark:text-blue-200",
bestPractice.category === "Retain Users" &&
"bg-orange-100 text-orange-500 dark:bg-orange-800 dark:text-orange-200"
)}>
{bestPractice.category}
</div>
<div className="h-12 w-12">
<IconComponent className="h-12 w-12 " />
</div>
<h3 className="mb-1 mt-3 text-xl font-bold text-slate-700 dark:text-slate-200">
{bestPractice.title}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">{bestPractice.description}</p>
</div>
);
})}
</div>
</div>
<BestPracticeNavigation />
<div className="mx-auto mt-4 w-fit px-4 py-2 text-center">
<Button
variant="highlight"

View File

@@ -4,7 +4,7 @@ import { Icon } from "@/components/shared/Icon";
const styles = {
note: {
container: "bg-slate-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10",
container: "bg-slate-100 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10",
title: "text-slate-900 dark:text-slate-400",
body: "text-slate-800 [--tw-prose-background:theme(colors.slate.50)] prose-a:text-slate-900 prose-code:text-slate-900 dark:text-slate-300 dark:prose-code:text-slate-300",
},

View File

@@ -15,7 +15,7 @@ export default function EarlyBirdDeal() {
</h2>
<h2 className="text-xl font-semibold tracking-tight text-slate-200 sm:text-lg">
Limited deal: Only{" "}
<span className="bg- rounded-sm bg-slate-200/40 px-2 py-0.5 text-slate-100">17</span> left.
<span className="bg- rounded-sm bg-slate-200/40 px-2 py-0.5 text-slate-100">14</span> left.
</h2>
<div className="mt-6">

View File

@@ -1,17 +1,100 @@
import { Button } from "@formbricks/ui";
import {
BaseballIcon,
Button,
CancelSubscriptionIcon,
CodeBookIcon,
DogChaserIcon,
FeedbackIcon,
InterviewPromptIcon,
OnboardingIcon,
PMFIcon,
} from "@formbricks/ui";
import { Popover, Transition } from "@headlessui/react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import { Bars3Icon, ChevronDownIcon, ChevronRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { StarIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import { usePlausible } from "next-plausible";
import Link from "next/link";
import { useRouter } from "next/router";
import { Fragment } from "react";
import { Fragment, useState } from "react";
import { FooterLogo } from "./Logo";
import { ThemeSelector } from "./ThemeSelector";
function GitHubIcon(props: any) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
</svg>
);
}
const UnderstandUsers = [
{
name: "Interview Prompt",
href: "/interview-prompt",
status: true,
icon: InterviewPromptIcon,
description: "Interview invites on auto-pilot",
},
{
name: "Measure PMF",
href: "/measure-product-market-fit",
status: true,
icon: PMFIcon,
description: "Improve Product-Market Fit",
},
{
name: "Onboarding Segments",
href: "/onboarding",
status: false,
icon: OnboardingIcon,
description: "Get it right from the start",
},
];
const IncreaseRevenue = [
{
name: "Learn from Churn",
href: "/learn-from-churn",
status: true,
icon: CancelSubscriptionIcon,
description: "Churn is hard, but insightful",
},
{
name: "Improve Trial CR",
href: "/improve-trial-conversion",
status: true,
icon: BaseballIcon,
description: "Take guessing out, hit it right",
},
];
const BoostRetention = [
{
name: "Feedback Box",
href: "/feedback-box",
status: true,
icon: FeedbackIcon,
description: "Always keep an ear open",
},
{
name: "Docs Feedback",
href: "/docs-feedback",
status: true,
icon: CodeBookIcon,
description: "Clear docs, more adoption",
},
{
name: "Feature Chaser",
href: "/feature-chaser",
status: true,
icon: DogChaserIcon,
description: "Follow up, improve",
},
];
export default function Header() {
/*
const [videoModal, setVideoModal] = useState(false); */
const [mobileSubOpen, setMobileSubOpen] = useState(false);
const plausible = usePlausible();
const router = useRouter();
return (
@@ -30,12 +113,140 @@ export default function Header() {
</Popover.Button>
</div>
<Popover.Group as="nav" className="hidden space-x-10 md:flex">
<Link
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={clsx(
open
? "text-slate-600 dark:text-slate-400 "
: "text-slate-400 hover:text-slate-900 dark:hover:text-slate-100",
"group inline-flex items-center rounded-md text-base font-medium hover:text-slate-300 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 dark:hover:text-slate-50"
)}>
<span>Best Practices</span>
<ChevronDownIcon
className={clsx(
open ? "text-slate-600" : "text-slate-400",
"ml-2 h-5 w-5 group-hover:text-slate-500"
)}
aria-hidden="true"
/>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1">
<Popover.Panel className="absolute z-10 -ml-4 mt-3 w-screen max-w-lg transform lg:left-1/2 lg:ml-0 lg:max-w-4xl lg:-translate-x-1/2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<div className="relative grid gap-6 bg-white px-5 py-6 dark:bg-slate-700 sm:gap-6 sm:p-8 lg:grid-cols-3">
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Understand Users
</h4>
{UnderstandUsers.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
"font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Increase Revenue
</h4>
{IncreaseRevenue.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
" font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Boost Retention
</h4>
{BoostRetention.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
" font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
{/* <Link
href="/community"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Community
</Link>
*/}
<Link
href="https://formbricks.com/#pricing"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
@@ -49,8 +260,15 @@ export default function Header() {
<Link
href="/blog"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Blog{/* <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p> */}
Blog <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p>
</Link>
{/* <Link
href="/community"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Community
</Link>
*/}
</Popover.Group>
<div className="hidden flex-1 items-center justify-end md:flex">
<ThemeSelector className="relative z-10 mr-5" />
@@ -81,7 +299,7 @@ export default function Header() {
router.push("https://app.formbricks.com");
plausible("NavBar_CTA_Login");
}}>
Login
Go to app
</Button>
</div>
</div>
@@ -113,17 +331,45 @@ export default function Header() {
</div>
<div className="px-5 py-6">
<div className="flex flex-col space-y-5 text-center text-sm dark:text-slate-300">
<div>
{mobileSubOpen ? (
<ChevronDownIcon className="mr-2 inline h-4 w-4" />
) : (
<ChevronRightIcon className="mr-2 inline h-4 w-4" />
)}
<button onClick={() => setMobileSubOpen(!mobileSubOpen)}>Best Practices</button>
</div>
{mobileSubOpen && (
<div className="flex flex-col space-y-5 text-center text-sm dark:text-slate-300">
{UnderstandUsers.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
{IncreaseRevenue.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
{BoostRetention.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
<hr className="mx-20 my-6 opacity-25" />
</div>
)}
<Link href="/community">Community</Link>
<Link href="#pricing">Pricing</Link>
<Link href="/docs">Docs</Link>
<Link href="/blog">Blog</Link>
{/* <Button
<Button
variant="secondary"
EndIcon={GitHubIcon}
onClick={() => router.push("https://github.com/formbricks/formbricks")}
className="flex w-full justify-center fill-slate-800 dark:fill-slate-200">
View on Github
</Button> */}
</Button>
<Button
variant="primary"
onClick={() => router.push("https://app.formbricks.com/auth/signup")}

View File

@@ -10,7 +10,7 @@ export default function HeaderLight() {
const router = useRouter();
return (
<Popover className="relative" as="header">
<div className=" max-w-8xl mx-auto flex items-center justify-between py-6 sm:px-2 md:justify-start lg:px-8 xl:px-12 ">
<div className="mx-auto flex items-center justify-between py-6 sm:px-2 md:justify-start lg:px-8 xl:px-12 ">
<div className="flex w-0 flex-1 justify-start">
<Link href="/">
<span className="sr-only">Formbricks</span>
@@ -34,7 +34,7 @@ export default function HeaderLight() {
router.push("https://app.formbricks.com/auth/signup");
plausible("Demo_CTA_TryForFree");
}}>
Create surveys for free
Start for free
</Button>
</div>
</div>

View File

@@ -14,7 +14,7 @@ export default function Layout({ title, description, children }: LayoutProps) {
<MetaInformation title={title} description={description} />
<HeaderLight />
{
<main className="max-w-8xl relative mx-auto mb-auto flex w-full flex-col justify-center sm:px-2 lg:px-8 xl:px-12">
<main className="relative mx-auto mb-auto flex w-full flex-col justify-center sm:px-2 lg:px-8 xl:px-12">
{children}
</main>
}

View File

@@ -99,8 +99,8 @@ export default function Pricing() {
<ul className="mt-4 space-y-4">
{tier.features.map((feature, index) => (
<li key={index} className="flex items-start">
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-400" />
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:border-green-600 dark:bg-green-900">
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-300" />
</div>
<span className="ml-2 text-sm text-slate-500 dark:text-slate-400">{feature}</span>
</li>

View File

@@ -0,0 +1,29 @@
import { Button } from "@formbricks/ui";
import { useRouter } from "next/router";
interface UseCaseCTAProps {
href: string;
}
export default function UseCaseHeader({ href }: UseCaseCTAProps) {
/* const plausible = usePlausible(); */
const router = useRouter();
return (
<div className="my-8 flex space-x-2 whitespace-nowrap">
<Button variant="secondary" href={href}>
Step-by-step manual
</Button>
<div className="space-y-1 text-center">
<Button
variant="darkCTA"
onClick={() => {
router.push("https://app.formbricks.com/auth/signup");
/* plausible("BestPractice_SubPage_CTA_TryItNow"); */
}}>
Try it now
</Button>
<p className="text-xs text-slate-400">It&apos;s free</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
interface UseCaseHeaderProps {
title: string;
difficulty: string;
setupMinutes: string;
}
export default function UseCaseHeader({ title, difficulty, setupMinutes }: UseCaseHeaderProps) {
return (
<div>
<div className="mb-4 flex-wrap items-center md:space-x-2">
<h1 className="mb-2 whitespace-nowrap pr-4 text-3xl font-semibold text-slate-800 dark:text-slate-200 ">
{title}
</h1>
<div className="inline-flex items-center justify-center rounded-full bg-indigo-200 px-4 py-1 text-sm text-indigo-700 dark:bg-indigo-800 dark:text-indigo-200">
{difficulty}
</div>
<div className="ml-2 inline-flex items-center justify-center rounded-full bg-slate-300 px-4 py-1 text-sm text-slate-700 dark:bg-slate-700 dark:text-slate-200 md:ml-0">
{setupMinutes} minutes
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
/*!
* Sanitize an HTML string
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {String} str The HTML string to sanitize
* @return {String} The sanitized string
*/
export function cleanHtml(str: string): string {
/**
* Convert the string to an HTML document
* @return {Node} An HTML document
*/
function stringToHTML() {
let parser = new DOMParser();
let doc = parser.parseFromString(str, "text/html");
return doc.body || document.createElement("body");
}
/**
* Remove <script> elements
* @param {Node} html The HTML
*/
function removeScripts(html) {
let scripts = html.querySelectorAll("script");
for (let script of scripts) {
script.remove();
}
}
/**
* Check if the attribute is potentially dangerous
* @param {String} name The attribute name
* @param {String} value The attribute value
* @return {Boolean} If true, the attribute is potentially dangerous
*/
function isPossiblyDangerous(name, value) {
let val = value.replace(/\s+/g, "").toLowerCase();
if (["src", "href", "xlink:href"].includes(name)) {
if (val.includes("javascript:") || val.includes("data:")) return true;
}
if (name.startsWith("on")) return true;
}
/**
* Remove potentially dangerous attributes from an element
* @param {Node} elem The element
*/
function removeAttributes(elem) {
// Loop through each attribute
// If it's dangerous, remove it
let atts = elem.attributes;
for (let { name, value } of atts) {
if (!isPossiblyDangerous(name, value)) continue;
elem.removeAttribute(name);
}
}
/**
* Remove dangerous stuff from the HTML document's nodes
* @param {Node} html The HTML document
*/
function clean(html) {
let nodes = html.children;
for (let node of nodes) {
removeAttributes(node);
clean(node);
}
}
// Convert the string to HTML
let html = stringToHTML();
// Sanitize it
removeScripts(html);
clean(html);
// If the user wants HTML nodes back, return them
// Otherwise, pass a sanitized string back
return html.innerHTML;
}

View File

@@ -0,0 +1,6 @@
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -15,14 +15,6 @@ const navigation = [
{ title: "Setup with Vue.js", href: "/docs/getting-started/vuejs" },
],
},
{
title: "Best Practices",
links: [
/* { title: "Feedback Box", href: "/docs/best-practices/feedback-box" }, */
{ title: "Docs Feedback", href: "/docs/best-practices/docs-feedback" },
/* { title: "In-app Interview Prompt", href: "/docs/best-practices/interview-prompt" }, */
],
},
{
title: "Attributes",
links: [
@@ -39,6 +31,18 @@ const navigation = [
{ title: "Code Actions", href: "/docs/actions/code" },
],
},
{
title: "Best Practices",
links: [
{ title: "Learn from Churn", href: "/docs/best-practices/cancel-subscription" },
{ title: "Interview Prompt", href: "/docs/best-practices/interview-prompt" },
{ title: "Product-Market Fit", href: "/docs/best-practices/pmf-survey" },
{ title: "Trial Conversion", href: "/docs/best-practices/improve-trial-cr" },
{ title: "Feature Chaser", href: "/docs/best-practices/feature-chaser" },
{ title: "Feedback Box", href: "/docs/best-practices/feedback-box" },
{ title: "Docs Feedback", href: "/docs/best-practices/docs-feedback" },
],
},
{
title: "API",
links: [

View File

@@ -8,7 +8,7 @@ import rehypePrism from "@mapbox/rehype-prism";
const nextConfig = {
reactStrictMode: true,
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
transpilePackages: ["@formbricks/ui"],
transpilePackages: ["@formbricks/ui", "@formbricks/lib"],
async redirects() {
return [
{

View File

@@ -13,6 +13,8 @@
"dependencies": {
"@docsearch/react": "^3.3.3",
"@formbricks/ui": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/lib": "workspace:*",
"@headlessui/react": "^1.7.14",
"@heroicons/react": "^2.0.17",
"@mapbox/rehype-prism": "^0.8.0",

View File

@@ -1,12 +1,12 @@
import LayoutWaitlist from "@/components/shared/LayoutLight";
import TemplateList from "@/components/dummyUI/TemplateList";
import DemoView from "@/components/dummyUI/DemoView";
export default function DemoPage() {
return (
<LayoutWaitlist
title="Formbricks Demo"
description="Leverage 30+ templates to kick-start your experience management.">
<TemplateList />
<DemoView />
</LayoutWaitlist>
);
}

View File

@@ -0,0 +1,44 @@
import Layout from "@/components/shared/Layout";
import UseCaseHeader from "@/components/shared/UseCaseHeader";
import UseCaseCTA from "@/components/shared/UseCaseCTA";
import DocsFeedback from "@/components/docs/DocsFeedback";
import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function DocsFeedbackPage() {
return (
<Layout
title="Feedback Box"
description="The better your docs, the higher your user adoption. Measure granularly how clear your documentation is.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Docs Feedback" difficulty="Intermediate" setupMinutes="60" />
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
Why is it useful?
</h3>
<p className="text-slate-600 dark:text-slate-400">
You want to know if your Developer Docs are clear and concise. When engineers dont understand
your technology or find the answer to their question, they are unlikely to use it. Docs Feedback
opens a window into how clear your Docs are. Have a look!
</p>
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
How to get started:
</h3>
<p className="text-slate-600 dark:text-slate-400">
As of now, Docs Feedback uses custom UI in the frontend and Formbricks in the backend. A partial
submission is sent when the user answers YES / NO and is enriched when the open text field is
filled out and submitted.
</p>
<UseCaseCTA href="/docs/best-practices/docs-feedback" />
</div>
<div className="mx-6 my-6 flex flex-col items-center justify-center rounded-xl border-2 border-slate-300 bg-slate-200 p-4 pb-36 transition-transform duration-150 dark:border-slate-500 dark:bg-slate-700 md:mx-0">
<p className="my-3 text-sm text-slate-500">Preview</p>
<DocsFeedback />
</div>
</div>
<h2 className="mb-6 ml-4 mt-12 text-2xl font-semibold text-slate-700 dark:text-slate-400 md:mt-0">
Other Best Practices
</h2>
<BestPracticeNavigation />
</Layout>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -0,0 +1,125 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import Image from "next/image";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import CreateChurnFlow from "./create-cancel-flow.png";
import ChangeText from "./change-text.png";
import TriggerInnerText from "./trigger-inner-text.png";
import TriggerCSS from "./trigger-css-selector.png";
import TriggerPageUrl from "./trigger-page-url.png";
import RecontactOptions from "./recontact-options.png";
import PublishSurvey from "./publish-survey.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "Learn from Churn",
description: "To know how to decrease churn, you have to understand it. Use a micro-survey.",
};
Churn is hard, but can teach you a lot. Whenever a user decides that your product isnt worth it anymore, you have a unique opportunity to get deep insights. These insights are pure gold to reduce churn.
## Purpose
The Churn Survey is among the most effective ways to identify weaknesses in you offering. People were willing to pay but now are not anymore: What changed? Lets find out!
## Preview
<DemoPreview template="Churn Survey" />
## Formbricks Approach
- Ask at exactly the right point in time
- Follow-up to prevent bad reviews
- Coming soon: Make survey mandatory
## Overview
To run the Churn Survey in your app you want to proceed as follows:
1. Create new Churn Survey at [app.formbricks.com](http://app.formbricks.com/)
2. Set up the user action to display survey at right point in time
3. Choose correct recontact options to never miss a feedback
4. Prevent that churn!
<Callout title="Formbricks Widget running?" type="note">
We assume that you have already installed the Formbricks Widget in your web app. Its required to display
messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins
max.)](/docs/getting-started/quickstart)
</Callout>
### 1. Create new Churn Survey
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Click on "Create Survey" and choose the template “Churn Survey”:
<Image src={CreateChurnFlow} alt="Create churn survey by template" quality="100" className="rounded-lg" />
### 2. Update questions (if you like)
Youre free to update the question and answer options. However, based on our experience, we suggest giving the provided template a go 😊
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
_Want to change the button color? You can do so in the product settings._
Save, and move over to the “Audience” tab.
### 3. Pre-segment your audience
In this case, you dont really need to pre-segment your audience. You likely want to ask everyone who hits the “Cancel subscription” button.
### 4. Set up a trigger
To create the trigger for your Churn Survey, you have two options to choose from:
1. **Trigger by innerText:** You likely have a “Cancel Subscription” button in your app. You can setup a user Action with the according `innerText` to trigger the survey, like so:
<Image src={TriggerInnerText} alt="Set the trigger by inner Text" quality="100" className="rounded-lg" />
2. **Trigger by CSS Selector:** In case you have more than one button saying “Cancel Subscription” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“cancel-subscription”` and set your user action up like so:
<Image src={TriggerCSS} alt="Set the trigger by CSS Selector" quality="100" className="rounded-lg" />
3. **Trigger by pageURL:** Lastly, you could also display your survey on a subpage “/subscription-cancelled” where you forward users once they cancelled the trial subscription. You can then create a user Action with the type `pageURL` with the following settings:
<Image src={TriggerPageUrl} alt="Set the trigger by page URL" quality="100" className="rounded-lg" />
Whenever a user visits this page, matches the filter conditions above and the recontact options (below) the survey will be displayed ✅
Here is our complete [Actions manual](/docs/actions/why) covering [Code](/docs/actions/code) and [No-Code](/docs/actions/no-code) Actions.
<Callout title="Pre-churn flow coming soon" type="note">
Were currently building full-screen survey pop-ups. Youll be able to prevent users from closing the survey unless they respond to it. Its certainly debatable if you want that but you could force them to click through the survey before letting them cancel 🤷
</Callout>
### 5. Select Action in the “When to ask” card
<Image src={SelectAction} alt="Select feedback button action" quality="100" className="rounded-lg" />
### 6. Last step: Set Recontact Options correctly
Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure you milk these super valuable insights. You want to make sure that this survey is always displayed, no matter if the user has already seen a survey in the past days:
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
These settings make sure the survey is always displayed, when a user wants to Cancel their subscription.
### 7. Congrats! Youre ready to publish your survey 💃
<Image src={PublishSurvey} alt="Publish survey" quality="100" className="rounded-lg" />
<Callout title="Formbricks Widget running?" type="warning">
You need to have the Formbricks Widget installed to display the Churn Survey in your app. Please follow
[this tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
###
# Get those insights! 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -15,6 +15,7 @@ import CopyIds from "./copy-ids.png";
export const meta = {
title: "Docs Feedback",
description: "Docs Feedback allows you to measure how clear your documentation is.",
};
Docs Feedback allows you to measure how clear your documentation is.
@@ -42,15 +43,33 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
2. In the Menu (top right) you see that you can switch between a “Development” and a “Production” environment. These are two separate environments so your test data doesnt mess up the insights from prod. Switch to “Development”:
<Image src={SwitchToDev} alt="switch to dev environment" quality="100" className="rounded" />
<Image
src={SwitchToDev}
alt="switch to dev environment"
quality="100"
className="rounded-lg"
className="rounded"
/>
3. Then, create a survey using the template “Docs Feedback”:
<Image src={DocsTemplate} alt="select docs template" quality="100" className="rounded" />
<Image
src={DocsTemplate}
alt="select docs template"
quality="100"
className="rounded-lg"
className="rounded"
/>
4. Change the Internal Question ID of the first question to **“isHelpful”** to make your life easier 😉
<Image src={ChangeId} alt="switch to dev environment" quality="100" className="rounded" />
<Image
src={ChangeId}
alt="switch to dev environment"
quality="100"
className="rounded-lg"
className="rounded"
/>
5. In the same way, you can change the Internal Question ID of the _Please elaborate_ question to **“additionalFeedback”** and the one of the _Page URL_ question to **“pageUrl”**.
@@ -61,15 +80,21 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
6. Click on “Continue to Audience” or select the audience tab manually. Scroll down to “When to ask” and create a new Action:
<Image src={WhenToAsk} alt="set up when to ask card" quality="100" className="rounded" />
<Image
src={WhenToAsk}
alt="set up when to ask card"
quality="100"
className="rounded-lg"
className="rounded"
/>
7. Our goal is to create an event that never fires. This is a bit nonsensical because it is a workaround. Stick with me 😃 Fill the action out like on the screenshot:
<Image src={AddAction} alt="add action" quality="100" className="rounded" />
<Image src={AddAction} alt="add action" quality="100" className="rounded-lg" className="rounded" />
8. Select the Non-Event in the dropdown. Now you see that the “Publish survey” button is active. Publish your survey 🤝
<Image src={SelectNonevent} alt="select nonevent" quality="100" className="rounded" />
<Image src={SelectNonevent} alt="select nonevent" quality="100" className="rounded-lg" className="rounded" />
**Youre all setup in Formbricks Cloud for now 👍**
@@ -82,7 +107,7 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
Before we start, lets talk about the widget. It works like this:
- Once the user selects yes/no, a partial response is created in Formbricks. It includes the feedback and the current page url.
- Once the user selects yes/no, a partial response is sent to the Formbricks API. It includes the feedback and the current page url.
- Then the user is presented with an additional open text field to further explain their choice. Once it's submitted, the previous response is updated with the additional feedback.
This allows us to capture and analyze partial feedback where the user is not willing to provide additional information.
@@ -93,7 +118,7 @@ This allows us to capture and analyze partial feedback where the user is not wil
2. Likely, you have a template file or similar which renders the navigation at the bottom of the page:
<Image src={DocsNavi} alt="doc navigation" quality="100" className="rounded" />
<Image src={DocsNavi} alt="doc navigation" quality="100" className="rounded-lg" className="rounded" />
Locate that file. We are using the [Tailwind Template “Syntax”](https://tailwindui.com/templates/syntax) for our docs. Here is our [Layout.tsx](https://github.com/formbricks/formbricks/blob/main/apps/formbricks-com/components/docs/Layout.tsx) file.
@@ -348,7 +373,7 @@ Before you roll it out in production, you want to test it. To do so, you need tw
When you are on the survey detail page, youll find both of them in the URL:
<Image src={CopyIds} alt="copy IDs" quality="100" className="rounded" />
<Image src={CopyIds} alt="copy IDs" quality="100" className="rounded-lg" className="rounded" />
Now, you have to replace the IDs and the API host accordingly in your `handleFeedbackSubmit`:

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,106 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import ActionCSS from "./action-css.png";
import ActionText from "./action-text.png";
import ChangeText from "./change-text.png";
import CreateSurvey from "./create-survey.png";
import Publish from "./publish.png";
import RecontactOptions from "./recontact-options.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "Feature Chaser",
description: "Follow up with users who used a specific feature. Gather feedback and improve your product.",
};
Following up on specific features only makes sense with very targeted surveys. Formbricks is built for that.
## Purpose
Product analytics never tell you why a feature is used - and why not. Following up on specfic features with highly relevant questions is a great way to gather feedback and improve your product.
## Preview
<DemoPreview template="Feature Chaser" />
## Formbricks Approach
- Trigger survey at exactly the right point in the user journey
- Never ask twice, keep your data clean
- Prevent survey fatigue with global waiting period
## Overview
To run the Feature Chaser survey in your app you want to proceed as follows:
1. Create new Feature Chaser survey at [app.formbricks.com](http://app.formbricks.com/)
2. Setup a user action to display survey at the right point in time
<Callout title="Formbricks Widget running?" type="note">
We assume that you have already installed the Formbricks Widget in your web app. Its required to display
messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins
max.)](/docs/getting-started/quickstart)
</Callout>
### 1. Create new Feature Chaser
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Click on "Create Survey" and choose the template “Feature Chaser”:
<Image src={CreateSurvey} alt="Create survey by template" quality="100" className="rounded-lg" />
### 2. Update questions
The questions you want to ask are dependent on your feature and can be very specific. In the template, we suggest a high-level check on how easy it was for the user to achieve their goal. We also add an opportunity to provide context:
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
Save, and move over to where the magic happens: The “Audience” tab.
### 3. Set up a trigger for the Feature Chaser survey:
Before setting the right trigger, you need to identify a user action in your app which signals, that they have just used the feature you want to understand better. In most cases, it is clicking a specific button in your product.
You can create [Code Actions](/docs/actions/code) and [No Code Actions](/docs/actions/no-code) to follow users through your app. In this example, we will create a No Code Action.
There are two ways to track a button:
1. **Trigger by innerText:** You might have a button with a unique text at the end of your feature e.g. "Export Report". You can setup a user Action with the according `innerText` to trigger the survey, like so:
<Image src={ActionText} alt="Set the trigger by inner Text" quality="100" className="rounded-lg" />
2. **Trigger by CSS Selector:** In case you have more than one button saying “Export Report” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“export-report-featurename”` and set your user action up like so:
<Image src={ActionCSS} alt="Set the trigger by CSS Selector" quality="100" className="rounded-lg" />
Please follow our [Actions manual](/docs/actions/why) for an in-depth description of how Actions work.
### 4. Select Action in the “When to ask” card
<Image src={SelectAction} alt="Select PMF trigger button action" quality="100" className="rounded-lg" />
### 5. Last step: Set Recontact Options correctly
Lastly, scroll down to “Recontact Options”. Here you have full freedom to decide who you want to ask. Generally, you only want to ask every user once and prevent survey fatigue. It's up to you to decide if you want to ask again, when the user did not yet reply:
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
### 7. Congrats! Youre ready to publish your survey 💃
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg" />
<Callout title="Formbricks Widget running?" type="warning">
You need to have the Formbricks Widget installed to display the Feature Chaser in your app. Please follow
[this tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
###
# Get those insights! 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,209 +1,105 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import Link from "next/link";
import NewFB from "@/images/docs/create-feedback-box.png";
import FBID from "@/images/docs/fb-id.png";
import Image from "next/image";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import AddAction from "./add-action.png";
import AddCSSAction from "./add-css-action.png";
import AddHTMLAction from "./add-html-action.png";
import ChangeTextContent from "./change-text-content.png";
import CreateFeedbackBox from "./create-feedback-box-by-template.png";
import PublishSurvey from "./publish-survey.png";
import SelectAction from "./select-feedback-button-action.png";
import RecontactOptions from "./set-recontact-options.png";
export const meta = {
title: "Feedback Box",
description: "The Feedback Box gives your users a direct channel to share their feedback and feel heard.",
};
The Feedback Box gives your user a direct channel to share their feedback and feel heard.
The Feedback Box gives your users a direct channel to share their feedback and feel heard.
## Purpose
Allow users to share feedback with 2 clicks. A low friction way to gather feedback helps catching even the smallest points of annoyance / frustration in user experiences.
A low friction way to gather feedback helps catching even the smallest points of frustration in user experiences. Use automations to react rapidly and make users feel heard.
## Preview
<DemoPreview template="Feedback Box" />
## Formbricks Approach
- Make it **easy**: 2 clicks to share feedback
- **Pre-sort** feedback:
- Bug → Pipe into Bug channel for devs
- Feedback → Pipe into Feedback channel for PMs
## Preview
- Make it easy: 2 clicks to share feedback
- Pipe insights where team can see them and react quickly
## Installation
To add the Feedback Box to your app, you need to perform these steps:
1. Create new Feedback Box at app.formbricks.com
2. Setup Feedback Box with template
3. Use NPM to install or embed JS snippet in `<head>`
4. Test
2. Add user action to trigger Feedback Box
3. Update recontact settings to display correctly
### 1. Create new Feedback Box
Create an account at [app.formbricks.com](https://app.formbricks.com/auth/signup)
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Then, create a new Feedback Box:
Then, create a new survey and look for the "Feedback Box" template:
<Image src={NewFB} alt="new feedback box" quality="100" />
<Image src={CreateFeedbackBox} alt="Create feedback box by template" quality="100" className="rounded-lg" />
Go to the "Setup instructions" tab and locate your Feedback Box ID. You'll need it in a minute:
### 2. Update question content
<Image src={FBID} alt="copy feedback box id" quality="100" />
Change the questions and answer options according to your preference:
### 2. Embed Formbricks snippet in `<head>`
<Image src={ChangeTextContent} alt="Change text content" quality="100" className="rounded-lg" />
Embed the following Feedback Box script in your HTML `<head>` tag.
### 3. Create user action to trigger Feedback Box:
```tsx
<script src="https://cdn.jsdelivr.net/npm/@formbricks/feedback@0.3" defer></script>
Go to the “Audience” tab, find the “When to send” card and choose “Add Action”. We will now use our super cool No-Code User Action Tracker:
<script>
window.formbricks = {
...window.formbricks,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "YOUR FEEDBACK BOX ID HERE", // copy from Formbricks dashboard
contact: {
name: "Matti",
position: "Co-Founder",
imgUrl: "https://avatars.githubusercontent.com/u/675065?s=128&v=4",
},
},
}
</script>
```
<Image src={AddAction} alt="Add action" quality="100" className="rounded-lg" />
Afterwards you need to embed the feedback box into your app. The standard ways are either a <Link href="/docs/wrappers/pop-over">pop-over on button click</Link> or <Link href="/docs/wrappers/inline">inline inserted into a div</Link>.
<Callout title="You can also add actions in your code" type="note">
You can also create [Code Actions](/docs/actions/code) using `formbricks.track("Eventname")` - they will
automatically appear in your Actions overview as long as the SDK is embedded.
</Callout>
For example for a pop-over on button click you need to add the following code to your app:
We have two options to track the Feedback Button in your application: innerText and CSS-Selector:
```html
<button data-formbricks-button>Feedback</button>
```
1. **innerText:** This means that whenever a user clicks any HTML item in your app which has an `innerText` of `Feedback` the Feedback Box will be displayed.
2. **CSS-Selector:** This means that when an element with a specific CSS-Selector like `#feedback-button` is clicked, your Feedback Box is triggered.
### 3. Configure Feedback Box
<div className="grid grid-cols-2 space-x-2">
<Image src={AddHTMLAction} alt="Add HTML action" quality="100" className="rounded-lg" />
<Image src={AddCSSAction} alt="Add CSS action" quality="100" className="rounded-lg" />
</div>
You can change the content behaviour of the Feedback Box with the config object.
### 4. Select action in the “When to ask” card
**Basic config**
<Image src={SelectAction} alt="Select feedback button action" quality="100" className="rounded-lg" />
Add your Formbricks form ID and the formbricks server address to the config object.
### 5. Set Recontact Options correctly
```html
<script>
window.formbricks = {
...window.formbricks,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "cldipnvz80002le0ha2a3zhgl",
},
};
</script>
```
Scroll down to “Recontact Options”. Here you have to choose the right settings so that the Feedback Box pops up every time the user action is performed. (Our default is that every user sees every survey only once):
**Personalizing**
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
Add your name, position and image link to give users the impression that you care about their feedback :)
### 7. Youre ready publish your survey!
```javascript
window.formbricks = {
...window.formbricks,
config: {
// ...
contact: {
name: "Matti",
position: "Co-Founder",
imgUrl: "https://avatars.githubusercontent.com/u/675065?s=128&v=4",
},
},
};
```
<Image src={PublishSurvey} alt="Publish survey" quality="100" className="rounded-lg" />
<div id="add-user-email">**Sending user data with feedback**</div>
## Setting up the Widget
The feedback box is built for in-app experiences. We assume that you already have user properties stored in a session object.
<Callout title="Formbricks Widget running?" type="warning">
You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow
[this tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
Here is an example of how to pass it to Formbricks. However, it might differ in your specific case.
### &nbsp;
```javascript
window.formbricks = {
...window.formbricks,
config: {
// ...
customer: {
email: "", // fill dynamically
name: "", // fill dynamically
},
};
},
```
Note: the `email` field must be present in the customer object
**Styling**
You can style the Feedback Box to match your UI. We recommend to at least replace the brand color with your main color.
```javascript
window.formbricks = {
...window.formbricks,
config: {
// ...
style: {
brandColor: "#00c4b8",
},
};
},
```
Here are all variables you can set with the current defaults:
```javascript
style: {
brandColor: "#00c4b8",
borderRadius: "0.4rem",
headerBGColor: "#1e293b",
headerTitleColor: "#111111",
boxBGColor: "#cbd5e1",
textColor: "#0f172a",
buttonHoverColor: "#e2e8f0",
},
```
### Example config
Here is an example of a full config object:
```javascript
window.formbricks = {
...window.formbricks,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "cldipcgat0000mn0g31a8pdse",
containerId: "formbricks-feedback-box", // only needed for modal & inline
contact: {
name: "Johannes",
position: "Co-Founder",
imgUrl: "https://avatars.githubusercontent.com/u/72809645?v=4",
},
customer: {
id: "", // fill dynamically
name: "", // fill dynamically
email: "", // fill dynamically
},
style: {
brandColor: "#0E1420",
headerBGColor: "#E5E7EB",
headerTitleColor: "#4B5563",
boxBGColor: "#F9FAFB",
textColor: "#374151",
buttonHoverColor: "#F3F4F6",
},
},
};
```
### 4. Render survey in your app
To add the Feedback Box to your UI, you can use different wrappers. Please follow the instructions linked below:
1. <Link href="/docs/wrappers/pop-over">In-app Pop-over</Link>
2. <Link href="/docs/wrappers/modal">Modal</Link>
3. <Link href="/docs/wrappers/inline">Inline</Link>
# Thats it! 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -0,0 +1,114 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import ActionText from "./action-innertext.png";
import ActionPageurl from "./action-pageurl.png";
import ChangeText from "./change-text.png";
import CreateSurvey from "./create-survey.png";
import Publish from "./publish.png";
import RecontactOptions from "./recontact-options.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "Improve Trial Conversion",
description: "Understand how to improve the trial conversions to get more paying customers.",
};
When a user doesn't convert, you want to know why. A micro-survey displayed at exactly the right time gives you a window into understanding the most relevant question: To pay or not to pay?
## Purpose
The better you understand why free users dont convert to paid users, the higher your revenue. You can make an informed decision about what to change in your offering to make more people pay for your service.
## Preview
<DemoPreview template="Improve Trial Conversion" />
## Formbricks Approach
- Ask at exactly the right point in time
- Ask to understand the problem, dont ask for solutions
## Installation
To display the Trial Conversion Survey in your app you want to proceed as follows:
1. Create new Trial Conversion Survey at [app.formbricks.com](http://app.formbricks.com/)
2. Set up the user action to display survey at right point in time
3. Print that 💸
<Callout title="Formbricks Widget running?" type="note">
We assume that you have already installed the Formbricks Widget in your web app. Its required to display
messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins
max.)](/docs/getting-started/quickstart)
</Callout>
### 1. Create new Trial Conversion Survey
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Click on "Create Survey" and choose the template “Improve Trial Conversion”:
<Image src={CreateSurvey} alt="Create survey by template" quality="100" className="rounded-lg" />
### 2. Update questions (if you like)
Youre free to update the questions and answer options. However, based on our experience, we suggest giving the provided template a go 😊
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
_Want to change the button color? You can do so in the product settings!_
Save, and move over to the “Audience” tab.
### 3. Pre-segment your audience (coming soon)
<Callout title="Filter by attribute coming soon" type="note">
We're working on pre-segmenting users by attributes. We will update this manual in the next days.
</Callout>
Pre-segmentation isn't relevant for this survey because you likely want to solve all people who cancel their trial. You probably have a specific user action e.g. clicking on "Cancel Trial" you can use to only display the survey to users trialing your product.
### 4. Set up a trigger for the Trial Conversion Survey:
How you trigger your survey depends on your product. There are two options:
1. **Trigger by pageURL:** Lets say you have a page under “/trial-cancelled” where you forward users once they cancelled the trial subscription. You can then create an user Action with the type `pageURL` with the following settings:
<Image src={ActionPageurl} alt="Change text content" quality="100" className="rounded-lg" />
Whenever a user visits this page, the survey will be displayed ✅
2. **Trigger by Button Click:** In a different case, you have a “Cancel Trial button in your app. You can setup a user Action with the according `innerText` like so:
<Image src={ActionText} alt="Change text content" quality="100" className="rounded-lg" />
Please have a look at our complete [Actions manual](/docs/actions/why) if you have questions.
### 5. Select Action in the “When to ask” card
<Image src={SelectAction} alt="Select feedback button action" quality="100" className="rounded-lg" />
### 6. Last step: Set Recontact Options correctly
Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure you gather as many insights as possible. You want to make sure that this survey is always displayed, no matter if the user has already seen a survey in the past days:
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
### 7. Congrats! Youre ready to publish your survey 💃
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg" />
<Callout title="Formbricks Widget running?" type="warning">
You need to have the Formbricks Widget installed to display the Improve Trial Conversion Survey in your app.
Please follow [this tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
###
# Go get 'em 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,14 +1,133 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import ActionCSS from "./action-css.png";
import ActionInner from "./action-innertext.png";
import ActionPageurl from "./action-pageurl.png";
import AddAction from "./add-action.png";
import ChangeText from "./change-text.png";
import CreatePrompt from "./create-prompt.png";
import InterviewExample from "./interview-example.png";
import Publish from "./publish-survey.png";
import RecontactOptions from "./recontact-options.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "In-app Interview Prompt",
description: "Invite only power users to schedule an interview with your product team.",
};
<Callout title="Closed Beta" type="note">
In-app interview prompts are only available in closed beta. Want to become a design partner? Reach out at
hola@formbricks.com
The Interview Prompt allows you to pick a specific user segment (e.g. Power Users) and invite them to a user interview. Bye, bye spammy email invites, benefit from up to 6x more respondents.
## Purpose
Product analytics and in-app surveys are incomplete without user interviews. Set the scheduling on autopilot for a continuous stream of interviews.
## Preview
<DemoPreview template="Interview Prompt" />
## Formbricks Approach
- Pre-segment users with custom attributes. Only invite highly relevant users.
- In-app prompts have a 6x higher conversion rate than email invites.
- Set scheduling user interviews on auto pilot.
- Soon: Integrate directly with your [Cal.com](http://Cal.com) account.
## Installation
To display an Interview Prompt in your app you want to proceed as follows:
1. Create new Interview Prompt at [app.formbricks.com](http://app.formbricks.com/)
2. Adjust content and settings
3. Thats it! 🎉
<Callout title="Formbricks Widget running?" type="note">
We assume that you have already installed the Formbricks Widget in your web app. Its required to display
messages and surveys in your app. If not, please follow the [Quick Start Guide
(15mins).](/docs/getting-started/quickstart)
</Callout>
### 1. Create new Interview Prompt
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Click on "Create Survey" and choose the template “Interview Prompt”:
<Image src={CreatePrompt} alt="Create interview prompt by template" quality="100" className="rounded-lg" />
### 2. Update prompt and CTA
Update the prompt, description and button text to match your products tonality. You can also update the button color in the Product Settings.
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
In the button settings you have to make sure it is set to “External URL”. In the URL field, copy your booking link (e.g. https://cal.com/company/user-interview). If you dont have a booking link yet, head over to [cal.com](http://cal.com) and get one - they have the best free plan out there!
<Image src={InterviewExample} alt="Add CSS action" quality="100" className="rounded-lg" />
Save, and move over to the “Audience” tab.
### 3. Pre-segment your audience (coming soon)
<Callout title="Filter by attribute coming soon" type="note">
We're working on pre-segmenting users by attributes. We will update this manual in the next few days.
</Callout>
Once you clicked over to the “Audience” tab you can change the settings. In the **Who To Send** card, select “Filter audience by attribute”. This allows you to only show the prompt to a specific segment of your user base.
In our case, we want to select users who we have assigned the attribute “Power User”. To learn how to assign attributes to your users, please [follow this guide](/docs/attributes/why).
Great, now only the “Power User” segment will see our Interview Prompt. But when will they see it?
### 4. Set up a trigger for the Interview Prompt:
To create the trigger to show your Interview Prompt, go to the “Audience” tab, find the “When to send” card and choose “Add Action”. We will now use our super cool No-Code User Action Tracker:
<Image src={AddAction} alt="Add action" quality="100" className="rounded-lg" />
<Callout title="You can also add actions in your code" type="note">
You can also create [Code Actions](/docs/actions/code) using `formbricks.track("Eventname")` - they will
automatically appear in your Actions overview as long as the SDK is embedded.
</Callout>
Generally, we have two types of user actions: Page views and clicks. The Interview Prompt, youll likely want to display on a page visit since you already filter who sees the prompt by attributes.
1. **pageURL:** Whenever a user visits a page the survey will be displayed, as long as the other conditions match. Other conditions are pre-segmentation, if this user has seen a survey in the past 2 weeks, etc.
<Image src={ActionPageurl} alt="Add page URL action" quality="100" className="rounded-lg" />
2. **innerText & CSS-Selector:** When a user clicks an element (like a button) with a specific text content or CSS selector, the prompt will be displayed as long as the other conditions also match.
<div className="grid grid-cols-2 space-x-2">
<Image src={ActionCSS} alt="Add CSS action" quality="100" className="rounded-lg" />
<Image src={ActionInner} alt="Add inner text action" quality="100" className="rounded-lg" />
</div>
### 5. Select action in the “When to ask” card
<Image src={SelectAction} alt="Select feedback button action" quality="100" className="rounded-lg" />
### 6. Set Recontact Options correctly
Scroll down to “Recontact Options”. Here you have to choose the correct settings to strike the right balance between asking for user feedback and preventing survey fatigue. Your settings also depend on the size of your user base or segment. If you e.g. have thousands of “Power Users” you can easily afford to only display the prompt once. If you have a smaller user base you might want to ask twice to get a sufficient amount of bookings:
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
### 7. Congrats! Youre ready to publish your survey 💃 🤸
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg" />
<Callout title="Formbricks widget running?" type="note">
You need to have the Formbricks Widget installed to display the Interview Prompt in your app. Please follow
[this tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
###
# Learn about them users! 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -1,175 +1,116 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import Link from "next/link";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import NewPMF from "@/images/docs/new-pmf.png";
import ID from "@/images/docs/copy-id.png";
import ActionCSS from "./action-css.png";
import ActionPageurl from "./action-pageurl.png";
import ChangeText from "./change-text.png";
import CreateSurvey from "./create-survey.png";
import Publish from "./publish.png";
import RecontactOptions from "./recontact-options.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "Product-Market Fit Survey",
description: "The Product-Market Fit survey helps you measure, well, Product-Market Fit (PMF).",
};
The Product-Market Fit survey (or Sean Ellis Test) is a method to measure Product-Market Fit.
## Purpose
By assessing how disappointed users would be if they could no longer use your service you get a good idea of how well your current product fits your target market.
Measuring it allows you to optimize it.
## Formbricks Approach
- Higher conversion: In-app surveys **convert significantly better** than email surveys
- **Pre-segment** user base: Only ask users who experienced the value of your product
- **Specific** dashboard: Understand your data right, separate signal from noise by design
- Targeted approach: **Personally address** users with their name (if you have it)
- Measure continuously: Feel the pulse of your user base **consistently**
- No UI clutter: **Natively embed** the survey for best possible UX
- **Never** ask twice: Assure to not survey users twice
Measuring and understanding your PMF is essential to build a large, successful business. It helps you understand what users like, what theyre missing and what to build next. This survey is perfectly suited to measure PMF like [Superhuman](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit).
## Preview
<div className="max-w-md"></div>
<DemoPreview template="Product Market Fit (Superhuman)" />
## Installation
## Formbricks Approach
To add the Product-Market Fit Survey to your app, you need to perform these steps:
- Pre-segment users to only survey users who have experienced your products value
- Never ask twice, keep your data clean
- Run on autopilot: Set up once, keep surveying users continuously
1. Create new survey at app.formbricks.com
2. Embed JS snippet in `<head>`
3. Configure survey
4. Render in-app
## Overview
### 1. Create new survey
To display the Product-Market Fit survey in your app you want to proceed as follows:
Create an account at [app.formbricks.com](https://app.formbricks.com/auth/signup)
1. Create new Product-Market Fit survey at [app.formbricks.com](http://app.formbricks.com/)
2. Setup pre-segmentation to assure high data quality
3. Setup the user action to display survey at good point in time
Then, create a new Product-Market Fit Survey:
<Callout title="Formbricks Widget running?" type="note">
We assume that you have already installed the Formbricks Widget in your web app. Its required to display
messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins
max.)](/docs/getting-started/quickstart)
</Callout>
<Image src={NewPMF} alt="create pmf survey" quality="100" />
### 1. Create new PMF survey
Go to the "Setup instructions" tab and locate your survey ID. You'll need it in a minute:
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
<Image src={ID} alt="copy survey id" quality="100" />
Click on "Create Survey" and choose one of the PMF survey templates. The first one is rather short, the latter builds on the ["Product-Market Fit Engine"](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit) developed by Superhuman:
### 2. Embed Formbricks snippet in `<head>`
<Image src={CreateSurvey} alt="Create survey by template" quality="100" className="rounded-lg" />
Embed the following Product-Market Fit Survey script in your HTML `<head>` tag.
### 2. Update questions (if you like)
Replace the `formId` with survey Id from the Formbricks dashboard:
Youre free to update the question and answer options. However, based on our experience, we suggest giving the provided template a go 😊 Here is a very [detailed description](https://coda.io/@rahulvohra/superhuman-product-market-fit-engine) of what to do with the data youre collecting.
```tsx
<script src="https://cdn.jsdelivr.net/npm/@formbricks/pmf@0.1" defer></script>
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
<script>
window.formbricksPmf = {
...window.formbricksPmf,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "SURVEY ID HERE", // paste your survey ID here
containerId: "formbricks-pmf", // required to render survey in your page
},
};
</script>
```
_Want to change the button color? You can do so in the product settings!_
All you have to do now is assigning the `containerId` to the div where you want to render your survey (detailed instructions linked at the bottom):
Save, and move over to where the magic happens: The “Audience” tab.
```html
<div id="formbricks-pmf"></div>
```
### 3. Pre-segment your audience (coming soon)
### 3. Configure survey
<Callout title="Filter by attribute coming soon" type="note">
We're working on pre-segmenting users by attributes. We will update this manual in the next days.
</Callout>
**Sending user metadata with submission**
To run this survey properly, you should pre-segment your user base. As touched upon earlier: if you ask every user youll get lots of opinions which are often misleading. You only want to gather feedback from people who invested the time to get to know and use your product:
The Product-Market Fit Survey is built for in-app experiences. We assume that you already have user properties stored in a session object. It makes sense to send them to Formbricks to enrich the user profile in the user view. Later on, you will be able to create cohorts to survey based on user properties.
**Filter by attribute**: You can keep the logic to decide if a user has (or has not) experienced value in your application. This makes most sense if you want to use historic usage data to decide if a user qualifies or not. Create your logic and if it applies, send an attribute to Formbricks by e.g. `formbricks.setAttribute("Loyalty", "Experienced Value");` Here is the full manual on how to [set attributes](/docs/attributes/custom-attributes).
Here is an example of how to take metadata from the next.js Session Object and pass it to Formbricks:
**Filter by actions (coming soon)**: Later, you can also segment users based on events tracked with Formbricks. However, this makes it impossible to use historic usage data (pre Formbricks usage). Here we will have a few options to achieve that:
```javascript
window.formbricksPmf = {
...window.formbricksPmf,
config: {
// ...
customer: {
email: "", // fill dynamically
name: "", // fill dynamically
},
};
},
```
- Check the time passed since sign-up (e.g. signed up 4 weeks ago)
- User has performed a specific action a certain number of times or (e.g. created 5 reports)
- User has performed a combination of actions (e.g. created a report **and** invited a team member)
Note: the `email` field must be present in the customer object
This way you make sure that you separate potentially misleading opinions from valuable insights.
**Styling**
### 4. Set up a trigger for the Product-Market Fit survey:
You can style the Product-Market Fit Survey to match your UI. We recommend to at least replace the brand color with your main color.
You need a trigger to display the survey but in this case, the filtering does all the work. Its up to you to decide to display the survey after the user viewed a specific subpage (pageURL) or after clicking an element. Have a look at the [Actions manual](/docs/actions/why) if you are not sure how to set them up:
```javascript
window.formbricksPmf = {
...window.formbricksPmf,
config: {
// ...
style: {
brandColor: "#00c4b8",
},
};
},
```
<div className="grid grid-cols-2 space-x-2">
<Image src={ActionCSS} alt="Add CSS action" quality="100" className="rounded-lg" />
<Image src={ActionPageurl} alt="Add inner text action" quality="100" className="rounded-lg" />
</div>
Here are all variables you can set with the current defaults:
### 5. Select Action in the “When to ask” card
```javascript
style: {
brandColor: "#00c4b8",
borderRadius: "0.4rem",
containerBgColor: "#f8fafc",
textColor: "#0f172a",
buttonTextColor: "#ffffff",
textareaBorderColor: "#e2e8f0",
},
```
<Image src={SelectAction} alt="Select PMF trigger button action" quality="100" className="rounded-lg" />
### Example config
### 6. Last step: Set Recontact Options correctly
Here is an example of a full config object:
Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure your data remains of high quality. You want to make sure that this survey is only responded to once per user. It is up to you to decide if you want to display it several times until the user responds:
```javascript
window.formbricksPmf = {
...window.formbricksPmf,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "cldetkpre0000nr0hku986hio",
containerId: "formbricks-pmf-survey", // always needed
customer: {
id: "", // fill dynamically
name: "", // fill dynamically
email: "", // fill dynamically
},
style: {
brandColor: "#00c4b8",
borderRadius: "0.4rem",
containerBgColor: "#f8fafc",
textColor: "#0f172a",
buttonTextColor: "#ffffff",
textareaBorderColor: "#e2e8f0",
},
},
};
```
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
### 4. Render survey in your app
### 7. Congrats! Youre ready to publish your survey 💃
To add the Product-Market Fit Survey to your UI, you can use different wrappers. Please follow the instructions linked below:
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg" />
1. <Link href="/docs/wrappers/modal">Modal</Link>
2. <Link href="/docs/wrappers/inline">Inline</Link>
<Callout title="Formbricks Widget running?" type="warning">
You need to have the Formbricks Widget installed to display the PMF Survey in your app. Please follow [this
tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
###
# Get those insights!
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,43 @@
import Layout from "@/components/shared/Layout";
import UseCaseHeader from "@/components/shared/UseCaseHeader";
import UseCaseCTA from "@/components/shared/UseCaseCTA";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function FeatureChaserPage() {
return (
<Layout
title="Feature Chaser"
description="Show a survey about a new feature shown only to people who used it.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Feature Chaser" difficulty="Easy" setupMinutes="10" />
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
Why is it useful?
</h3>
<p className="text-slate-600 dark:text-slate-400">
You don&apos;t always know how well a feature works. Product analytics don&apos;t tell you why it
is used - and why not. Especially in complex products it can be difficult to gather reliable
experience data. The Feature Chaser allows you to granularly survey users at exactly the right
point in the user journey.
</p>
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
How to get started:
</h3>
<p className="text-slate-600 dark:text-slate-400">
Once you&apos;ve embedded the Formbricks Widget in your application, you can start following user
actions. Simply use our No-Code Action wizard to keep track of different actions users perfrom -
100% GPDR compliant.
</p>
<UseCaseCTA href="/docs/best-practices/feature-chaser" />
</div>
<DemoPreview template="Feature Chaser" />
</div>
<h2 className="mb-6 ml-4 mt-12 text-2xl font-semibold text-slate-700 dark:text-slate-400 md:mt-0">
Other Best Practices
</h2>
<BestPracticeNavigation />
</Layout>
);
}

View File

@@ -1,14 +1,42 @@
import Layout from "@/components/shared/Layout";
import UseCaseHeader from "@/components/shared/UseCaseHeader";
import UseCaseCTA from "@/components/shared/UseCaseCTA";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function FeedbackBoxPage() {
return (
<Layout
title="Feedback Box"
description="Open a direct channel to your users by allowing them to share feedback with your team.">
<div className="grid grid-cols-2">
{/*
<UseCaseHeader title="Feedback Box" description="Direct channel for users to provide feedback to your team." difficulty="Easy" setupMinutes="5"/> */}
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Feedback Box" difficulty="Easy" setupMinutes="5" />
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
Why is it useful?
</h3>
<p className="text-slate-600 dark:text-slate-400">
Offering a direct channel for feedback helps you build a better product. Users feel heared and
with Formbricks automations, you&apos;ll be able to react to feedback rapidly. Lastly, critical
feedback can be acted upon quickly so that it doesn&apos;t end up on social media.
</p>
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
How to get started:
</h3>
<p className="text-slate-600 dark:text-slate-400">
Setting up a Feedback Widget with Formbricks takes just a few minutes. Create an account,
customize your widget and choose the right settings so that it always pops up when a user hits
&quot;Feedback&quot;.
</p>
<UseCaseCTA href="/docs/best-practices/feedback-box" />
</div>
<DemoPreview template="Feedback Box" />
</div>
<h2 className="mb-6 ml-4 mt-12 text-2xl font-semibold text-slate-700 dark:text-slate-400 md:mt-0">
Other Best Practices
</h2>
<BestPracticeNavigation />
</Layout>
);
}

View File

@@ -0,0 +1,41 @@
import Layout from "@/components/shared/Layout";
import UseCaseHeader from "@/components/shared/UseCaseHeader";
import UseCaseCTA from "@/components/shared/UseCaseCTA";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function MissedTrialPagePage() {
return (
<Layout
title="Improve Trial Conversion"
description="Take the guessing out, convert more trials to paid users with insights.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Improve Trial Conversion" difficulty="Easy" setupMinutes="15" />
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
Why is it useful?
</h3>
<p className="text-slate-600 dark:text-slate-400">
People who tried your product have the problem you&apos;re solving. That&apos;s good!
Understanding why they didn&apos;t convert to a paying user is crucial to improve your conversion
rate - and grow the bottom line of your company.
</p>
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
How to get started:
</h3>
<p className="text-slate-600 dark:text-slate-400">
Once a user signed up for a trial, you can pass this info as an attribute to Formbricks. This
allows you to pre-segment your user base and only survey users in the trial stage. This granular
segmentation leads to better data and minimal survey fatigue.
</p>
<UseCaseCTA href="/docs/best-practices/improve-trial-cr" />
</div>
<DemoPreview template="Improve Trial Conversion" />
</div>
<h2 className="mb-6 ml-4 mt-12 text-2xl font-semibold text-slate-700 dark:text-slate-400 md:mt-0">
Other Best Practices
</h2>
<BestPracticeNavigation />
</Layout>
);
}

View File

@@ -0,0 +1,41 @@
import Layout from "@/components/shared/Layout";
import UseCaseHeader from "@/components/shared/UseCaseHeader";
import UseCaseCTA from "@/components/shared/UseCaseCTA";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function InterviewPromptPage() {
return (
<Layout
title="Interview Prompt"
description="Ask only power users users to book a time in your calendar. Get those juicy details.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Interview Prompt" difficulty="Easy" setupMinutes="15" />
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
Why is it useful?
</h3>
<p className="text-slate-600 dark:text-slate-400">
Interviews are the best way to understand your customers needs. But there is so much overhead
involved, especially when your team and customer base grow. Automate scheduling interviews with
Formbricks with ease.
</p>
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
How to get started:
</h3>
<p className="text-slate-600 dark:text-slate-400">
Once you have setup the Formbricks Widget, you have two ways to pre-segment your user base: Based
on events and based on attributes. Soon, you will also be able to import cohorts from PostHog with
just a few clicks.
</p>
<UseCaseCTA href="/docs/best-practices/interview-prompt" />
</div>
<DemoPreview template="Interview Prompt" />
</div>
<h2 className="mb-6 ml-4 mt-12 text-2xl font-semibold text-slate-700 dark:text-slate-400 md:mt-0">
Other Best Practices
</h2>
<BestPracticeNavigation />
</Layout>
);
}

View File

@@ -0,0 +1,41 @@
import Layout from "@/components/shared/Layout";
import UseCaseHeader from "@/components/shared/UseCaseHeader";
import UseCaseCTA from "@/components/shared/UseCaseCTA";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function LearnFromChurnPage() {
return (
<Layout
title="Learn from Churn"
description="Churn is hard, but insightful. Learn from users who changed their mind.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Learn from Churn" difficulty="Easy" setupMinutes="15" />
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
Why is it useful?
</h3>
<p className="text-slate-600 dark:text-slate-400">
Churn is hard. Users decided to pay for your service and changed their mind. Don&apos;t let them
get away with these knowledge nuggets about the shortcomings of your product! Find out to prevent
churn in the future.
</p>
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
How to get started:
</h3>
<p className="text-slate-600 dark:text-slate-400">
Once you&apos;ve setup Formbricks, you have two ways to run this survey: Before users cancel or
after. If you guide them through the survey before they can cancel, you might add to their
frustration. But getting feedback from every user gets you there faster.
</p>
<UseCaseCTA href="/docs/best-practices/cancel-subscription" />
</div>
<DemoPreview template="Churn Survey" />
</div>
<h2 className="mb-6 ml-4 mt-12 text-2xl font-semibold text-slate-700 dark:text-slate-400 md:mt-0">
Other Best Practices
</h2>
<BestPracticeNavigation />
</Layout>
);
}

View File

@@ -0,0 +1,164 @@
import DemoPreview from "@/components/dummyUI/DemoPreview";
import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
import BreakerCTA from "@/components/shared/BreakerCTA";
import Layout from "@/components/shared/Layout";
import UseCaseCTA from "@/components/shared/UseCaseCTA";
import UseCaseHeader from "@/components/shared/UseCaseHeader";
import DashboardMockupDark from "@/images/dashboard-mockup-dark.png";
import DashboardMockup from "@/images/dashboard-mockup.png";
import PipelinesDark from "@/images/pipelines-dark.png";
import Pipelines from "@/images/pipelines.png";
import PreSegmentationDark from "@/images/pre-segmentation-dark.png";
import PreSegmentation from "@/images/pre-segmentation.png";
import Image from "next/image";
export default function MeasurePMFPage() {
return (
<Layout
title="Product-Market Fit Survey"
description="Measure Product-Market Fit to understand how to develop your product further.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Product-Market Fit" difficulty="Intermediate" setupMinutes="30" />
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
Why is it useful?
</h3>
<p className="text-slate-600 dark:text-slate-400">
The Product-Market Fit survey is a proven method to get a continuous understanding of how users
value your product. This helps you prioritize features to increase your PMF. To run it properly,
you need granular control over who to ask when. Formbricks makes this possible.
</p>
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
How to get started:
</h3>
<p className="text-slate-600 dark:text-slate-400">
In a nutshell: Decide what constitutes a &quot;Power User&quot; in your product. Set and send the
corresponding attribute to Formbricks and use it to pre-segment your user base. Formbricks
automatically asks a predetermined amount of users weekly, bi-weekly or monthly. The continuous
stream of insights help you develop your product with the core user needs front and center.
</p>
<UseCaseCTA href="/docs/best-practices/pmf-survey" />
</div>
<DemoPreview template="Product Market Fit (Superhuman)" />
</div>
{/* Steps */}
<div id="howitworks" className="mx-auto mt-8 max-w-lg md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 1</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200">
1. Pre-Segmentation
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Signed up for more than 4 weeks? Used a specific feature? Set up a custom condition to{" "}
<strong>only survey the right subset</strong> of your user base.
</p>
</div>
<div className="rounded-lg bg-slate-100 p-4 dark:bg-slate-800 sm:p-8">
<Image
src={PreSegmentation}
quality="100"
alt="Pre Segmentation"
className="block dark:hidden"
/>
<Image
src={PreSegmentationDark}
quality="100"
alt="Pre Segmentation"
className="hidden dark:block"
/>
</div>
</div>
</div>
</div>
<div className="mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last rounded-lg sm:py-8 md:order-first md:p-4">
<DemoPreview template="Product Market Fit Survey (short)" />
</div>
<div className="pb-8 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 2</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-100 sm:text-3xl">
Survey users in-app
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
On average, in-app surveys convert 6x better than email surveys. Get significant results even
from smaller user bases.
</p>
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 3</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-3xl">
Loop in your team
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Pipe insights to where your team works: Slack, Discord, Email. Use the webhook and Zapier to
pipe survey data where you want it.
</p>
</div>
<div className="w-full rounded-lg bg-slate-100 p-8 dark:bg-slate-800">
<Image
src={Pipelines}
quality="100"
alt="Data Pipelines"
className="block rounded-lg dark:hidden"
/>
<Image src={PipelinesDark} quality="100" alt="Data Pipelines" className="hidden dark:block" />
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last sm:scale-125 sm:p-8 md:order-first">
<Image
src={DashboardMockup}
quality="100"
alt="PMF Dashboard Mockup"
className="block dark:hidden"
/>
<Image
src={DashboardMockupDark}
quality="100"
alt="PMF Dashboard Mockup"
className="hidden dark:block"
/>
</div>
<div className="pb-8 pl-4 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 4</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-100 sm:text-3xl">
Make better decisions
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Down the line we will allow you to build a custom dashboard specifically to gauge
Product-Market Fit. Beat confirmation bias and
<strong> build conviction for the next product decision.</strong>
</p>
</div>
</div>
</div>
</div>
<BreakerCTA
teaser="READY to measure PMF?"
headline="Get started in minutes."
subheadline="Measure Product-Market Fit with a survey that converts 6x better than email."
cta="Sign up for free"
href="https://app.formbricks.com/auth/signup"
/>
<h2 className="mb-6 ml-4 mt-32 text-2xl font-semibold text-slate-700 dark:text-slate-300">
Other Best Practices
</h2>
<BestPracticeNavigation />
</Layout>
);
}

Some files were not shown because too many files have changed in this diff Show More