outsource form-engine in own package

This commit is contained in:
Matthias Nannt
2023-02-06 14:53:48 +01:00
parent a9b9f022b5
commit c898af0390
18 changed files with 1271 additions and 653 deletions
@@ -0,0 +1,28 @@
import { Button } from "@formbricks/ui";
interface EngineButtonsProps {
allowSkip: boolean;
skipAction: () => void;
autoSubmit: boolean;
}
export function EngineButtons({ allowSkip, skipAction, autoSubmit }: EngineButtonsProps) {
return (
<div className="mx-auto mt-8 flex w-full max-w-xl justify-end">
{allowSkip && (
<Button
variant="secondary"
type="button"
className="transition-all ease-in-out hover:scale-105"
onClick={() => skipAction()}>
Skip
</Button>
)}
{!autoSubmit && (
<Button variant="primary" type="submit" className="ml-2 transition-all ease-in-out hover:scale-105">
Next
</Button>
)}
</div>
);
}
@@ -1,3 +1,6 @@
import clsx from "clsx";
import { useMemo } from "react";
import { EngineButtons } from "./EngineButtons";
import { SurveyElement } from "./engineTypes";
interface FeatureSelectionProps {
@@ -7,21 +10,38 @@ interface FeatureSelectionProps {
control: any;
onSubmit: () => void;
disabled: boolean;
allowSkip: boolean;
skipAction: () => void;
autoSubmit: boolean;
loading: boolean;
}
export default function FeatureSelection({ element, field, register }: FeatureSelectionProps) {
export default function FeatureSelection({
element,
field,
register,
allowSkip,
skipAction,
autoSubmit,
loading,
}: FeatureSelectionProps) {
const shuffledOptions = useMemo(
() => (element.options ? getShuffledArray(element.options) : []),
[element.options]
);
return (
<div className="flex flex-col justify-center">
<label
htmlFor={element.id}
className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</label>
<fieldset className="space-y-5">
<legend className="sr-only">{element.label}</legend>
<div className=" mx-auto grid max-w-5xl grid-cols-1 gap-6 px-2 sm:grid-cols-2">
{element.options &&
element.options.map((option) => (
<div className={clsx(loading && "formbricks-pulse-animation")}>
<div className="flex flex-col justify-center">
<label
htmlFor={element.id}
className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</label>
<fieldset className="space-y-5">
<legend className="sr-only">{element.label}</legend>
<div className=" mx-auto grid max-w-5xl grid-cols-1 gap-6 px-2 sm:grid-cols-2">
{shuffledOptions.map((option) => (
<label htmlFor={`${element.id}-${option.value}`} key={`${element.id}-${option.value}`}>
<div className="drop-shadow-card duration-120 relative cursor-default rounded-lg border border-gray-200 bg-white p-6 transition-all ease-in-out hover:scale-105 dark:border-slate-700 dark:bg-slate-700">
<div className="absolute right-10">
@@ -48,8 +68,19 @@ export default function FeatureSelection({ element, field, register }: FeatureSe
</div>
</label>
))}
</div>
</fieldset>
</div>
</fieldset>
</div>
<EngineButtons allowSkip={allowSkip} skipAction={skipAction} autoSubmit={autoSubmit} />
</div>
);
}
function getShuffledArray(array: any[]) {
const shuffledArray = [...array];
for (let i = shuffledArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
}
return shuffledArray;
}
@@ -3,6 +3,7 @@ import { CheckCircleIcon } from "@heroicons/react/20/solid";
import clsx from "clsx";
import { useEffect, useState } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { EngineButtons } from "./EngineButtons";
import { SurveyElement } from "./engineTypes";
interface IconRadioProps {
@@ -11,9 +12,22 @@ interface IconRadioProps {
control: any;
onSubmit: () => void;
disabled: boolean;
allowSkip: boolean;
skipAction: () => void;
autoSubmit: boolean;
loading: boolean;
}
export default function IconRadio({ element, control, onSubmit, disabled }: IconRadioProps) {
export default function IconRadio({
element,
control,
onSubmit,
disabled,
allowSkip,
autoSubmit,
skipAction,
loading,
}: IconRadioProps) {
const value = useWatch({
control,
name: element.name!!,
@@ -26,78 +40,81 @@ export default function IconRadio({ element, control, onSubmit, disabled }: Icon
}, [value, onSubmit, disabled]);
return (
<Controller
name={element.name!}
control={control}
rules={{ required: true }}
render={({ field }: { field: any }) => (
<RadioGroup className="flex flex-col justify-center" {...field}>
<RadioGroup.Label className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</RadioGroup.Label>
<div className="mx-auto -mt-3 mb-3 text-center text-sm text-slate-500 dark:text-slate-300 md:max-w-lg">
{element.help}
</div>
<div className={clsx(loading && "formbricks-pulse-animation")}>
<Controller
name={element.name!}
control={control}
rules={{ required: true }}
render={({ field }: { field: any }) => (
<RadioGroup className="flex flex-col justify-center" {...field}>
<RadioGroup.Label className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</RadioGroup.Label>
<div className="mx-auto -mt-3 mb-3 text-center text-sm text-slate-500 dark:text-slate-300 md:max-w-lg">
{element.help}
</div>
<div
className={clsx(
element.options && element.options.length >= 4
? "lg:grid-cols-4"
: element.options?.length === 3
? "lg:grid-cols-3"
: element.options?.length === 2
? "lg:grid-cols-2"
: "lg:grid-cols-1",
"mt-4 grid w-full gap-y-6 sm:gap-x-4"
)}>
{element.options &&
element.options.map((option) => (
<RadioGroup.Option
key={option.value}
value={option.value}
className={({ checked, active }) =>
clsx(
checked ? "border-transparent" : "border-slate-200 dark:border-slate-700",
active ? "border-brand ring-brand ring-2" : "",
"relative flex cursor-pointer rounded-lg border bg-white py-8 shadow-sm transition-all ease-in-out hover:scale-105 focus:outline-none dark:bg-slate-700"
)
}>
{({ checked, active }) => (
<>
<div className="flex flex-1 flex-col justify-center text-slate-500 hover:text-slate-700 dark:text-slate-400 hover:dark:text-slate-200">
{option.frontend?.icon && (
<option.frontend.icon
className="text-brand mx-auto mb-3 h-8 w-8"
aria-hidden="true"
/>
)}
<RadioGroup.Label as="span" className="mx-auto text-sm font-medium ">
{option.label}
</RadioGroup.Label>
</div>
<div
className={clsx(
element.options && element.options.length >= 4
? "lg:grid-cols-4"
: element.options?.length === 3
? "lg:grid-cols-3"
: element.options?.length === 2
? "lg:grid-cols-2"
: "lg:grid-cols-1",
"mt-4 grid w-full gap-y-6 sm:gap-x-4"
)}>
{element.options &&
element.options.map((option) => (
<RadioGroup.Option
key={option.value}
value={option.value}
className={({ checked, active }) =>
clsx(
checked ? "border-transparent" : "border-slate-200 dark:border-slate-700",
active ? "border-brand ring-brand ring-2" : "",
"relative flex cursor-pointer rounded-lg border bg-white py-8 shadow-sm transition-all ease-in-out hover:scale-105 focus:outline-none dark:bg-slate-700"
)
}>
{({ checked, active }) => (
<>
<div className="flex flex-1 flex-col justify-center text-slate-500 hover:text-slate-700 dark:text-slate-400 hover:dark:text-slate-200">
{option.frontend?.icon && (
<option.frontend.icon
className="text-brand mx-auto mb-3 h-8 w-8"
aria-hidden="true"
/>
)}
<RadioGroup.Label as="span" className="mx-auto text-sm font-medium ">
{option.label}
</RadioGroup.Label>
</div>
<CheckCircleIcon
className={clsx(
!checked ? "invisible" : "",
"text-brand absolute -right-2 -top-2 z-10 h-5 w-5 rounded-full bg-white"
)}
aria-hidden="true"
/>
<span
className={clsx(
active ? "border" : "border-2",
checked ? "border-brand" : "border-transparent",
"pointer-events-none absolute -inset-px rounded-lg"
)}
aria-hidden="true"
/>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
)}
/>
<CheckCircleIcon
className={clsx(
!checked ? "invisible" : "",
"text-brand absolute -right-2 -top-2 z-10 h-5 w-5 rounded-full bg-white"
)}
aria-hidden="true"
/>
<span
className={clsx(
active ? "border" : "border-2",
checked ? "border-brand" : "border-transparent",
"pointer-events-none absolute -inset-px rounded-lg"
)}
aria-hidden="true"
/>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
)}
/>
<EngineButtons allowSkip={allowSkip} skipAction={skipAction} autoSubmit={autoSubmit} />
</div>
);
}
+34 -15
View File
@@ -1,3 +1,5 @@
import clsx from "clsx";
import { EngineButtons } from "./EngineButtons";
import { SurveyElement } from "./engineTypes";
interface TextareaProps {
@@ -5,25 +7,42 @@ interface TextareaProps {
field: any;
register: any;
disabled: boolean;
allowSkip: boolean;
skipAction: () => void;
onSubmit: () => void;
autoSubmit: boolean;
loading: boolean;
}
export default function Input({ element, field, register, disabled, onSubmit }: TextareaProps) {
export default function Input({
element,
field,
register,
disabled,
onSubmit,
skipAction,
allowSkip,
autoSubmit,
loading,
}: TextareaProps) {
return (
<div className="flex flex-col justify-center">
<label
htmlFor={element.id}
className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</label>
<input
type={element.frontend?.type || "text"}
onBlur=""
className="focus:border-brand focus:ring-brand mx-auto mt-4 block w-full max-w-xl rounded-md border-gray-300 text-slate-700 shadow-sm dark:bg-slate-700 dark:text-slate-200 dark:placeholder:text-slate-400 dark:focus:bg-slate-700 dark:active:bg-slate-700 sm:text-sm"
placeholder={element.frontend?.placeholder || ""}
required={!!element.frontend?.required}
{...register(element.name!)}
/>
<div className={clsx(loading && "formbricks-pulse-animation")}>
<div className="flex flex-col justify-center">
<label
htmlFor={element.id}
className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</label>
<input
type={element.frontend?.type || "text"}
onBlur=""
className="focus:border-brand focus:ring-brand mx-auto mt-4 block w-full max-w-xl rounded-md border-gray-300 text-slate-700 shadow-sm dark:bg-slate-700 dark:text-slate-200 dark:placeholder:text-slate-400 dark:focus:bg-slate-700 dark:active:bg-slate-700 sm:text-sm"
placeholder={element.frontend?.placeholder || ""}
required={!!element.frontend?.required}
{...register(element.name!)}
/>
</div>
<EngineButtons allowSkip={allowSkip} skipAction={skipAction} autoSubmit={autoSubmit} />
</div>
);
}
+104 -87
View File
@@ -3,6 +3,7 @@ import { CheckCircleIcon } from "@heroicons/react/20/solid";
import clsx from "clsx";
import { useEffect, useState } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { EngineButtons } from "./EngineButtons";
import { SurveyElement } from "./engineTypes";
interface IconRadioProps {
@@ -11,9 +12,22 @@ interface IconRadioProps {
control: any;
onSubmit: () => void;
disabled: boolean;
allowSkip: boolean;
skipAction: () => void;
autoSubmit: boolean;
loading: boolean;
}
export default function Scale({ element, control, onSubmit, disabled }: IconRadioProps) {
export default function Scale({
element,
control,
onSubmit,
disabled,
allowSkip,
skipAction,
autoSubmit,
loading,
}: IconRadioProps) {
const value = useWatch({
control,
name: element.name!!,
@@ -25,92 +39,95 @@ export default function Scale({ element, control, onSubmit, disabled }: IconRadi
}
}, [value, onSubmit, disabled]);
return (
<Controller
name={element.name!}
control={control}
rules={{ required: true }}
render={({ field }: { field: any }) => (
<RadioGroup className="flex flex-col justify-center" {...field}>
<RadioGroup.Label className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</RadioGroup.Label>
<div
className={clsx(
element.frontend.max &&
element.frontend.min &&
element.frontend.max - element.frontend.min + 1 >= 11
? "grid-cols-11"
: element.frontend.max - element.frontend.min + 1 === 10
? "grid-cols-10"
: element.frontend.max - element.frontend.min + 1 === 9
? "grid-cols-9"
: element.frontend.max - element.frontend.min + 1 === 8
? "grid-cols-8"
: element.frontend.max - element.frontend.min + 1 === 7
? "grid-cols-7"
: element.frontend.max - element.frontend.min + 1 === 6
? "grid-cols-6"
: element.frontend.max - element.frontend.min + 1 === 5
? "grid-cols-5"
: element.frontend.max - element.frontend.min + 1 === 4
? "grid-cols-4"
: element.frontend.max - element.frontend.min + 1 === 3
? "grid-cols-3"
: element.frontend.max - element.frontend.min + 1 === 2
? "grid-cols-2"
: "grid-cols-1",
"mt-4 grid w-full gap-x-1 sm:gap-x-2"
)}>
{Array.from(
{ length: element.frontend.max - element.frontend.min + 1 },
(_, i) => i + element.frontend.min
).map((num) => (
<RadioGroup.Option
key={num}
value={num}
className={({ checked, active }) =>
clsx(
checked ? "border-transparent" : "border-gray-200 dark:border-slate-700",
active ? "border-brand ring-brand ring-2" : "",
"xs:rounded-lg relative flex cursor-pointer rounded-md border bg-white py-3 shadow-sm transition-all ease-in-out hover:scale-105 focus:outline-none dark:bg-slate-700 sm:p-4"
)
}>
{({ checked, active }) => (
<>
<div className="flex flex-1 flex-col justify-center">
<RadioGroup.Label
as="span"
className="mx-auto text-sm font-medium text-gray-900 dark:text-gray-200">
{num}
</RadioGroup.Label>
</div>
<div className={clsx(loading && "formbricks-pulse-animation")}>
<Controller
name={element.name!}
control={control}
rules={{ required: true }}
render={({ field }: { field: any }) => (
<RadioGroup className="flex flex-col justify-center" {...field}>
<RadioGroup.Label className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</RadioGroup.Label>
<div
className={clsx(
element.frontend.max &&
element.frontend.min &&
element.frontend.max - element.frontend.min + 1 >= 11
? "grid-cols-11"
: element.frontend.max - element.frontend.min + 1 === 10
? "grid-cols-10"
: element.frontend.max - element.frontend.min + 1 === 9
? "grid-cols-9"
: element.frontend.max - element.frontend.min + 1 === 8
? "grid-cols-8"
: element.frontend.max - element.frontend.min + 1 === 7
? "grid-cols-7"
: element.frontend.max - element.frontend.min + 1 === 6
? "grid-cols-6"
: element.frontend.max - element.frontend.min + 1 === 5
? "grid-cols-5"
: element.frontend.max - element.frontend.min + 1 === 4
? "grid-cols-4"
: element.frontend.max - element.frontend.min + 1 === 3
? "grid-cols-3"
: element.frontend.max - element.frontend.min + 1 === 2
? "grid-cols-2"
: "grid-cols-1",
"mt-4 grid w-full gap-x-1 sm:gap-x-2"
)}>
{Array.from(
{ length: element.frontend.max - element.frontend.min + 1 },
(_, i) => i + element.frontend.min
).map((num) => (
<RadioGroup.Option
key={num}
value={num}
className={({ checked, active }) =>
clsx(
checked ? "border-transparent" : "border-gray-200 dark:border-slate-700",
active ? "border-brand ring-brand ring-2" : "",
"xs:rounded-lg relative flex cursor-pointer rounded-md border bg-white py-3 shadow-sm transition-all ease-in-out hover:scale-105 focus:outline-none dark:bg-slate-700 sm:p-4"
)
}>
{({ checked, active }) => (
<>
<div className="flex flex-1 flex-col justify-center">
<RadioGroup.Label
as="span"
className="mx-auto text-sm font-medium text-gray-900 dark:text-gray-200">
{num}
</RadioGroup.Label>
</div>
<CheckCircleIcon
className={clsx(
!checked ? "invisible" : "",
"text-brand absolute -right-2 -top-2 z-10 h-5 w-5 rounded-full bg-white"
)}
aria-hidden="true"
/>
<span
className={clsx(
active ? "border" : "border-2",
checked ? "border-brand" : "border-transparent",
"pointer-events-none absolute -inset-px rounded-lg"
)}
aria-hidden="true"
/>
</>
)}
</RadioGroup.Option>
))}
</div>
<div className="xs:text-sm mt-2 flex justify-between text-xs text-gray-700 dark:text-slate-400">
<p>{element.frontend.minLabel}</p>
<p>{element.frontend.maxLabel}</p>
</div>
</RadioGroup>
)}
/>
<CheckCircleIcon
className={clsx(
!checked ? "invisible" : "",
"text-brand absolute -right-2 -top-2 z-10 h-5 w-5 rounded-full bg-white"
)}
aria-hidden="true"
/>
<span
className={clsx(
active ? "border" : "border-2",
checked ? "border-brand" : "border-transparent",
"pointer-events-none absolute -inset-px rounded-lg"
)}
aria-hidden="true"
/>
</>
)}
</RadioGroup.Option>
))}
</div>
<div className="xs:text-sm mt-2 flex justify-between text-xs text-gray-700 dark:text-slate-400">
<p>{element.frontend.minLabel}</p>
<p>{element.frontend.maxLabel}</p>
</div>
</RadioGroup>
)}
/>
<EngineButtons allowSkip={allowSkip} skipAction={skipAction} autoSubmit={autoSubmit} />
</div>
);
}
@@ -1,27 +1,44 @@
import clsx from "clsx";
import { useEffect, useState } from "react";
import { EngineButtons } from "./EngineButtons";
import { SurveyElement } from "./engineTypes";
interface TextareaProps {
element: SurveyElement;
register: any;
onSubmit: () => void;
allowSkip: boolean;
skipAction: () => void;
autoSubmit: boolean;
loading: boolean;
}
export default function Textarea({ element, register, onSubmit }: TextareaProps) {
export default function Textarea({
element,
register,
onSubmit,
allowSkip,
skipAction,
autoSubmit,
loading,
}: TextareaProps) {
return (
<div className="flex flex-col justify-center">
<label
htmlFor={element.id}
className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</label>
<textarea
rows={element.frontend?.rows || 4}
className="focus:border-brand focus:ring-brand mx-auto mt-4 block w-full max-w-xl rounded-md border-gray-300 text-slate-700 shadow-sm dark:bg-slate-700 dark:text-slate-200 dark:placeholder:text-slate-400 sm:text-sm"
placeholder={element.frontend?.placeholder || ""}
required={!!element.frontend?.required}
{...register(element.name!)}
/>
<div className={clsx(loading && "formbricks-pulse-animation")}>
<div className="flex flex-col justify-center">
<label
htmlFor={element.id}
className="pb-6 text-center text-lg font-bold text-slate-600 dark:text-slate-300 sm:text-xl md:text-2xl">
{element.label}
</label>
<textarea
rows={element.frontend?.rows || 4}
className="focus:border-brand focus:ring-brand mx-auto mt-4 block w-full max-w-xl rounded-md border-gray-300 text-slate-700 shadow-sm dark:bg-slate-700 dark:text-slate-200 dark:placeholder:text-slate-400 sm:text-sm"
placeholder={element.frontend?.placeholder || ""}
required={!!element.frontend?.required}
{...register(element.name!)}
/>
</div>
<EngineButtons allowSkip={allowSkip} skipAction={skipAction} autoSubmit={autoSubmit} />
</div>
);
}
+1
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@docsearch/react": "^3.3.2",
"@formbricks/engine-react": "workspace:*",
"@formbricks/pmf": "workspace:*",
"@formbricks/react": "workspace:*",
"@formbricks/ui": "workspace:*",
+459 -450
View File
@@ -2,7 +2,7 @@ import FeatureSelection from "@/components/engine/FeatureSelection";
import IconRadio from "@/components/engine/IconRadio";
import Input from "@/components/engine/Input";
import Scale from "@/components/engine/Scale";
import { Survey } from "@/components/engine/Survey";
import { FormbricksEngine } from "@formbricks/engine-react";
import Textarea from "@/components/engine/Textarea";
import ThankYouHeading from "@/components/engine/ThankYouHeading";
import ThankYouPlans from "@/components/engine/ThankYouPlans";
@@ -29,471 +29,480 @@ import {
CrossMarkIcon,
UserCoupleIcon,
} from "@formbricks/ui";
import { usePlausible } from "next-plausible";
const WaitlistPage = () => (
<LayoutWaitlist title="Waitlist" description="Join our Waitlist today">
<div className="mx-auto w-full max-w-5xl px-6 md:w-3/4">
<div className="px-4 pt-20 pb-4">
<h1 className="text-3xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-4xl md:text-5xl">
<span className="xl:inline">Get</span>{" "}
<span className="from-brand-light to-brand-dark bg-gradient-to-b bg-clip-text text-transparent xl:inline">
early
</span>{" "}
<span className="inline ">access</span>
</h1>
<p className="mt-3 text-sm text-slate-400 dark:text-slate-300 md:text-base">
We are onboarding users continuously. Tell us more about you!
</p>
</div>
const WaitlistPage = () => {
const plausible = usePlausible();
return (
<LayoutWaitlist title="Waitlist" description="Join our Waitlist today">
<div className="mx-auto w-full max-w-5xl px-6 md:w-3/4">
<div className="px-4 pt-20 pb-4">
<h1 className="text-3xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-4xl md:text-5xl">
<span className="xl:inline">Get</span>{" "}
<span className="from-brand-light to-brand-dark bg-gradient-to-b bg-clip-text text-transparent xl:inline">
early
</span>{" "}
<span className="inline ">access</span>
</h1>
<p className="mt-3 text-sm text-slate-400 dark:text-slate-300 md:text-base">
We are onboarding users continuously. Tell us more about you!
</p>
</div>
<div className="mx-auto my-6 w-full max-w-5xl rounded-xl bg-slate-100 px-8 py-10 dark:bg-slate-800 md:my-12 md:px-16 md:py-20">
<Survey
formbricksUrl={
process.env.NODE_ENV === "production" ? "https://app.formbricks.com" : "http://localhost:3000"
}
formId={
process.env.NODE_ENV === "production" ? "cld37mt2i0000ld08p9q572bc" : "cldonm4ra000019axa4oc440z"
}
survey={{
config: {
progressBar: false,
},
pages: [
{
id: "rolePage",
config: {
autoSubmit: true,
},
elements: [
{
id: "role",
type: "radio",
label: "How would you describe your role?",
name: "role",
options: [
{ label: "Founder", value: "founder", frontend: { icon: FounderIcon } },
{
label: "Product Manager",
value: "productManager",
frontend: { icon: LaptopWorkerIcon },
},
{ label: "Engineer", value: "engineer", frontend: { icon: EngineerIcon } },
],
component: IconRadio,
},
],
<div className="mx-auto my-6 w-full max-w-5xl rounded-xl bg-slate-100 px-8 py-10 dark:bg-slate-800 md:my-12 md:px-16 md:py-20">
<FormbricksEngine
formbricksUrl={
process.env.NODE_ENV === "production" ? "https://app.formbricks.com" : "http://localhost:3000"
}
formId={
process.env.NODE_ENV === "production"
? "cld37mt2i0000ld08p9q572bc"
: "cldonm4ra000019axa4oc440z"
}
onPageSubmit={({ page }) => plausible(`waitlistSubmitPage-${page.id}`)}
onFinished={() => plausible("waitlistFinished")}
schema={{
config: {
progressBar: false,
},
{
id: "targetGroupPage",
config: {
autoSubmit: true,
},
elements: [
{
id: "targetGroup",
type: "radio",
label: "Who are you serving?",
name: "targetGroup",
options: [
{ label: "Companies", value: "companies", frontend: { icon: SkyscraperIcon } },
{ label: "Consumers", value: "consumers", frontend: { icon: UserGroupIcon } },
],
component: IconRadio,
pages: [
{
id: "rolePage",
config: {
autoSubmit: true,
},
],
},
{
id: "emailPage",
config: {
addFieldsToCustomer: ["email"],
},
elements: [
{
id: "email",
type: "text",
label: "What's your email?",
name: "email",
frontend: {
required: true,
type: "email",
placeholder: "email@example.com",
elements: [
{
id: "role",
type: "radio",
label: "How would you describe your role?",
name: "role",
options: [
{ label: "Founder", value: "founder", frontend: { icon: FounderIcon } },
{
label: "Product Manager",
value: "productManager",
frontend: { icon: LaptopWorkerIcon },
},
{ label: "Engineer", value: "engineer", frontend: { icon: EngineerIcon } },
],
component: IconRadio,
},
component: Input,
},
],
},
{
id: "featureSelectionPage",
elements: [
{
id: "featureSelection",
type: "radio",
label: "Select the Best Practices you need:",
name: "featureSelection",
options: [
{
label: "Onboarding Segmentation",
value: "onboardingSegmentation",
frontend: {
icon: OnboardingIcon,
description:
"Get to know your users right from the start. Ask a few questions early, let us enrich the profile.",
},
},
{
label: "Superhuman PMF Engine",
value: "pmf",
frontend: {
icon: PMFIcon,
description:
"Find out how disappointed people would be if they could not use your service any more.",
},
},
{
label: "Feature Chaser",
value: "featureChaser",
frontend: {
icon: DogChaserIcon,
description: "Show a survey about a new feature shown only to people who used it.",
},
},
{
label: "Cancel Subscription Flow",
value: "cancelSubscriptionFlow",
frontend: {
icon: CancelSubscriptionIcon,
description:
"Request users going through a cancel subscription flow before cancelling.",
},
},
{
label: "Interview Prompt",
value: "interviewPrompt",
frontend: {
icon: InterviewPromptIcon,
description:
"Ask high-interest users to book a time in your calendar to get all the juicy details.",
},
},
{
label: "Fake Door Follow-Up",
value: "fakeDoorFollowUp",
frontend: {
icon: DoorIcon,
description:
"Running a fake door experiment? Catch users right when they are full of expectations.",
},
},
{
label: "FeedbackBox",
value: "feedbackBox",
frontend: {
icon: FeedbackIcon,
description: "Give users the chance to share feedback in a single click.",
},
},
{
label: "Bug Report Form",
value: "bugReportForm",
frontend: {
icon: BugBlueIcon,
description: "Catch all bugs in your SaaS with easy and accessible bug reports.",
},
},
{
label: "Rage Click Survey",
value: "rageClickSurvey",
frontend: {
icon: AngryBirdRageIcon,
description:
"Sometimes things dont work. Trigger this rage click survey to catch users in rage.",
},
},
{
label: "Feature Request Widget",
value: "featureRequestWidget",
frontend: {
icon: FeatureRequestIcon,
description:
"Allow users to request features and pipe it to GitHub projects or Linear.",
},
},
],
component: FeatureSelection,
},
],
},
{
id: "wauPage",
config: {
autoSubmit: true,
],
},
elements: [
{
id: "wau",
type: "radio",
label: "How many weekly active users do you have?",
name: "wau",
options: [
{ label: "Not launched", value: "notLaunched", frontend: { icon: CrossMarkIcon } },
{ label: "10-100", value: "10-100", frontend: { icon: UserCoupleIcon } },
{ label: "100-1.000", value: "100-1000", frontend: { icon: UserGroupIcon } },
{ label: "1.000+", value: "10000+", frontend: { icon: UserGroupIcon } },
],
component: IconRadio,
{
id: "targetGroupPage",
config: {
autoSubmit: true,
},
],
},
{
id: "goalPage",
config: {
autoSubmit: true,
},
elements: [
{
id: "goal",
type: "radio",
label: "Want to become a beta user?",
help: "Answer 3 open-ended questions and get 50% off in the first year.",
name: "goal",
options: [
{
label: "No, just notify me on launch",
value: "justNotify",
frontend: { icon: BellIcon },
},
{
label: "Yes, take the survey",
value: "becomeBetaUser",
frontend: { icon: UserCommentIcon },
},
],
component: IconRadio,
},
],
branchingRules: [
{
type: "value",
name: "goal",
value: "justNotify",
nextPageId: "thankYouPageNotify",
},
],
},
{
id: "namePage",
elements: [
{
id: "name",
type: "text",
label: "First of all, whats your name?",
name: "name",
frontend: { placeholder: "First name" },
component: Input,
},
],
},
{
id: "urgencyPage",
config: {
autoSubmit: true,
},
elements: [
{
id: "urgency",
type: "radio",
label: "How urgently do you need this?",
name: "urgency",
options: [
{ label: "1", value: "1" },
{ label: "2", value: "2" },
{ label: "3", value: "3" },
{ label: "4", value: "4" },
{ label: "5", value: "5" },
{ label: "6", value: "6" },
{ label: "7", value: "7" },
{ label: "8", value: "8" },
{ label: "9", value: "9" },
{ label: "10", value: "10" },
],
frontend: {
min: 1,
max: 10,
minLabel: "Im just curious",
maxLabel: "As soon as possible",
elements: [
{
id: "targetGroup",
type: "radio",
label: "Who are you serving?",
name: "targetGroup",
options: [
{ label: "Companies", value: "companies", frontend: { icon: SkyscraperIcon } },
{ label: "Consumers", value: "consumers", frontend: { icon: UserGroupIcon } },
],
component: IconRadio,
},
component: Scale,
},
],
},
{
id: "pmfPage",
config: {
autoSubmit: true,
],
},
elements: [
{
id: "pmf",
type: "radio",
label: "Have you found Product-Market-Fit?",
name: "pmf",
options: [
{
label: "Yes",
value: "yes",
frontend: { icon: CheckMarkIcon },
{
id: "emailPage",
config: {
addFieldsToCustomer: ["email"],
},
elements: [
{
id: "email",
type: "text",
label: "What's your email?",
name: "email",
frontend: {
required: true,
type: "email",
placeholder: "email@example.com",
},
{
label: "No",
value: "no",
frontend: { icon: CrossMarkIcon },
component: Input,
},
],
},
{
id: "featureSelectionPage",
elements: [
{
id: "featureSelection",
type: "radio",
label: "Select the Best Practices you need:",
name: "featureSelection",
options: [
{
label: "Onboarding Segmentation",
value: "onboardingSegmentation",
frontend: {
icon: OnboardingIcon,
description:
"Get to know your users right from the start. Ask a few questions early, let us enrich the profile.",
},
},
{
label: "Superhuman PMF Engine",
value: "pmf",
frontend: {
icon: PMFIcon,
description:
"Find out how disappointed people would be if they could not use your service any more.",
},
},
{
label: "Feature Chaser",
value: "featureChaser",
frontend: {
icon: DogChaserIcon,
description:
"Show a survey about a new feature shown only to people who used it.",
},
},
{
label: "Cancel Subscription Flow",
value: "cancelSubscriptionFlow",
frontend: {
icon: CancelSubscriptionIcon,
description:
"Request users going through a cancel subscription flow before cancelling.",
},
},
{
label: "Interview Prompt",
value: "interviewPrompt",
frontend: {
icon: InterviewPromptIcon,
description:
"Ask high-interest users to book a time in your calendar to get all the juicy details.",
},
},
{
label: "Fake Door Follow-Up",
value: "fakeDoorFollowUp",
frontend: {
icon: DoorIcon,
description:
"Running a fake door experiment? Catch users right when they are full of expectations.",
},
},
{
label: "FeedbackBox",
value: "feedbackBox",
frontend: {
icon: FeedbackIcon,
description: "Give users the chance to share feedback in a single click.",
},
},
{
label: "Bug Report Form",
value: "bugReportForm",
frontend: {
icon: BugBlueIcon,
description: "Catch all bugs in your SaaS with easy and accessible bug reports.",
},
},
{
label: "Rage Click Survey",
value: "rageClickSurvey",
frontend: {
icon: AngryBirdRageIcon,
description:
"Sometimes things dont work. Trigger this rage click survey to catch users in rage.",
},
},
{
label: "Feature Request Widget",
value: "featureRequestWidget",
frontend: {
icon: FeatureRequestIcon,
description:
"Allow users to request features and pipe it to GitHub projects or Linear.",
},
},
],
component: FeatureSelection,
},
],
},
{
id: "wauPage",
config: {
autoSubmit: true,
},
elements: [
{
id: "wau",
type: "radio",
label: "How many weekly active users do you have?",
name: "wau",
options: [
{ label: "Not launched", value: "notLaunched", frontend: { icon: CrossMarkIcon } },
{ label: "10-100", value: "10-100", frontend: { icon: UserCoupleIcon } },
{ label: "100-1.000", value: "100-1000", frontend: { icon: UserGroupIcon } },
{ label: "1.000+", value: "10000+", frontend: { icon: UserGroupIcon } },
],
component: IconRadio,
},
],
},
{
id: "goalPage",
config: {
autoSubmit: true,
},
elements: [
{
id: "goal",
type: "radio",
label: "Want to become a beta user?",
help: "Answer 3 open-ended questions and get 50% off in the first year.",
name: "goal",
options: [
{
label: "No, just notify me on launch",
value: "justNotify",
frontend: { icon: BellIcon },
},
{
label: "Yes, take the survey",
value: "becomeBetaUser",
frontend: { icon: UserCommentIcon },
},
],
component: IconRadio,
},
],
branchingRules: [
{
type: "value",
name: "goal",
value: "justNotify",
nextPageId: "thankYouPageNotify",
},
],
},
{
id: "namePage",
elements: [
{
id: "name",
type: "text",
label: "First of all, whats your name?",
name: "name",
frontend: { placeholder: "First name" },
component: Input,
},
],
},
{
id: "urgencyPage",
config: {
autoSubmit: true,
},
elements: [
{
id: "urgency",
type: "radio",
label: "How urgently do you need this?",
name: "urgency",
options: [
{ label: "1", value: "1" },
{ label: "2", value: "2" },
{ label: "3", value: "3" },
{ label: "4", value: "4" },
{ label: "5", value: "5" },
{ label: "6", value: "6" },
{ label: "7", value: "7" },
{ label: "8", value: "8" },
{ label: "9", value: "9" },
{ label: "10", value: "10" },
],
frontend: {
min: 1,
max: 10,
minLabel: "Im just curious",
maxLabel: "As soon as possible",
},
],
component: IconRadio,
},
],
branchingRules: [
{
type: "value",
name: "pmf",
value: "no",
nextPageId: "pmfApproachPage",
},
],
},
{
id: "scalingResearchPage",
elements: [
{
id: "scalingResearch",
type: "text",
label: "What is your approach for scaling user research?",
name: "scalingResearch",
frontend: { required: true, placeholder: "Last time, I..." },
component: Textarea,
},
],
},
{
id: "userResearchHardestPartPage",
config: {
allowSkip: true,
component: Scale,
},
],
},
elements: [
{
id: "userResearchHardestPart",
type: "text",
label: "What is the hardest part about it?",
name: "userResearchHardestPart",
frontend: { required: false, placeholder: "Please tell us about your challenges." },
component: Textarea,
{
id: "pmfPage",
config: {
autoSubmit: true,
},
],
},
{
id: "toolsMaintainPmfPage",
elements: [
{
id: "toolsMaintainPmf",
type: "text",
label: "What tools help you maintain Product-Market Fit?",
name: "toolsMaintainPmf",
frontend: { required: true, placehodler: "Mixpanel, Segment, Intercom..." },
component: Textarea,
},
],
branchingRules: [
{
type: "value",
name: "pmf",
value: "yes",
nextPageId: "thankYouPageBetaUser",
},
],
},
{
id: "pmfApproachPage",
elements: [
{
id: "pmfApproach",
type: "text",
label: "What is your approach for finding Product-Market Fit?",
name: "pmfApproach",
frontend: { placeholder: "Last time, I..." },
component: Textarea,
},
],
},
{
id: "pmfHardestPartPage",
config: {
allowSkip: true,
elements: [
{
id: "pmf",
type: "radio",
label: "Have you found Product-Market-Fit?",
name: "pmf",
options: [
{
label: "Yes",
value: "yes",
frontend: { icon: CheckMarkIcon },
},
{
label: "No",
value: "no",
frontend: { icon: CrossMarkIcon },
},
],
component: IconRadio,
},
],
branchingRules: [
{
type: "value",
name: "pmf",
value: "no",
nextPageId: "pmfApproachPage",
},
],
},
elements: [
{
id: "pmfHardestPart",
type: "text",
label: "What is the hardest part about it?",
name: "pmfHardestPart",
frontend: { placeholder: "Please tell us about your challenges." },
component: Textarea,
{
id: "scalingResearchPage",
elements: [
{
id: "scalingResearch",
type: "text",
label: "What is your approach for scaling user research?",
name: "scalingResearch",
frontend: { required: true, placeholder: "Last time, I..." },
component: Textarea,
},
],
},
{
id: "userResearchHardestPartPage",
config: {
allowSkip: true,
},
],
},
{
id: "pmfFindingToolsPage",
elements: [
{
id: "pmfFindingTools",
type: "text",
label: "What tools help you finding Product-Market Fit?",
name: "pmfFindingTools",
frontend: { placeholder: "Mixpanel, Segment, Intercom..." },
component: Textarea,
elements: [
{
id: "userResearchHardestPart",
type: "text",
label: "What is the hardest part about it?",
name: "userResearchHardestPart",
frontend: { required: false, placeholder: "Please tell us about your challenges." },
component: Textarea,
},
],
},
{
id: "toolsMaintainPmfPage",
elements: [
{
id: "toolsMaintainPmf",
type: "text",
label: "What tools help you maintain Product-Market Fit?",
name: "toolsMaintainPmf",
frontend: { required: true, placehodler: "Mixpanel, Segment, Intercom..." },
component: Textarea,
},
],
branchingRules: [
{
type: "value",
name: "pmf",
value: "yes",
nextPageId: "thankYouPageBetaUser",
},
],
},
{
id: "pmfApproachPage",
elements: [
{
id: "pmfApproach",
type: "text",
label: "What is your approach for finding Product-Market Fit?",
name: "pmfApproach",
frontend: { placeholder: "Last time, I..." },
component: Textarea,
},
],
},
{
id: "pmfHardestPartPage",
config: {
allowSkip: true,
},
],
branchingRules: [
{
type: "value",
name: "pmf",
value: "no",
nextPageId: "thankYouPageBetaUser",
},
],
},
{
id: "thankYouPageNotify",
endScreen: true,
elements: [
{
id: "thankYouNotify",
type: "html",
component: ThankYouHeading,
},
],
},
{
id: "thankYouPageBetaUser",
endScreen: true,
elements: [
{
id: "thankYouBetaUser",
type: "html",
component: ThankYouHeading,
},
{
id: "thankYouBetaUser",
type: "html",
component: ThankYouPlans,
},
],
},
],
}}
/>
elements: [
{
id: "pmfHardestPart",
type: "text",
label: "What is the hardest part about it?",
name: "pmfHardestPart",
frontend: { placeholder: "Please tell us about your challenges." },
component: Textarea,
},
],
},
{
id: "pmfFindingToolsPage",
elements: [
{
id: "pmfFindingTools",
type: "text",
label: "What tools help you finding Product-Market Fit?",
name: "pmfFindingTools",
frontend: { placeholder: "Mixpanel, Segment, Intercom..." },
component: Textarea,
},
],
branchingRules: [
{
type: "value",
name: "pmf",
value: "no",
nextPageId: "thankYouPageBetaUser",
},
],
},
{
id: "thankYouPageNotify",
endScreen: true,
elements: [
{
id: "thankYouNotify",
type: "html",
component: ThankYouHeading,
},
],
},
{
id: "thankYouPageBetaUser",
endScreen: true,
elements: [
{
id: "thankYouBetaUser",
type: "html",
component: ThankYouHeading,
},
{
id: "thankYouBetaUser",
type: "html",
component: ThankYouPlans,
},
],
},
],
}}
/>
</div>
</div>
</div>
</LayoutWaitlist>
);
</LayoutWaitlist>
);
};
export default WaitlistPage;