mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-26 10:42:16 -06:00
outsource form-engine in own package
This commit is contained in:
28
apps/formbricks-com/components/engine/EngineButtons.tsx
Normal file
28
apps/formbricks-com/components/engine/EngineButtons.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@docsearch/react": "^3.3.2",
|
||||
"@formbricks/engine-react": "workspace:*",
|
||||
"@formbricks/pmf": "workspace:*",
|
||||
"@formbricks/react": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
|
||||
@@ -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 don’t 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, what’s 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: "I’m 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 don’t 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, what’s 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: "I’m 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;
|
||||
|
||||
60
packages/engine-react/package.json
Normal file
60
packages/engine-react/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@formbricks/engine-react",
|
||||
"version": "0.1.0",
|
||||
"author": "Formbricks <hola@formbricks.com>",
|
||||
"description": "Headless Form Engine for Formbricks",
|
||||
"homepage": "https://formbricks.com",
|
||||
"main": "./dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
"./styles.css": "./dist/styles.css",
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsup --dts",
|
||||
"dev": "tsup --dts --external react --watch",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"concurrently": "^7.5.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"tsup": "^6.4.0",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-hook-form": "^7.39.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"formbricks",
|
||||
"react",
|
||||
"form",
|
||||
"forms",
|
||||
"typescript",
|
||||
"survey",
|
||||
"surveys",
|
||||
"engine"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/formbricks/formbricks"
|
||||
}
|
||||
}
|
||||
148
packages/engine-react/src/components/EnginePage.tsx
Normal file
148
packages/engine-react/src/components/EnginePage.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FormPage } from "../types";
|
||||
|
||||
interface FormProps {
|
||||
page: FormPage;
|
||||
onSkip: () => void;
|
||||
onPageSubmit: (submission: any) => void;
|
||||
onFinished: ({ submission }: any) => void;
|
||||
submission: any;
|
||||
setSubmission: (v: any) => void;
|
||||
finished: boolean;
|
||||
formbricksUrl: string;
|
||||
formId: string;
|
||||
schema: any;
|
||||
}
|
||||
|
||||
export function EnginePage({
|
||||
page,
|
||||
onSkip,
|
||||
onPageSubmit,
|
||||
onFinished,
|
||||
submission,
|
||||
setSubmission,
|
||||
formbricksUrl,
|
||||
formId,
|
||||
schema,
|
||||
}: FormProps) {
|
||||
const [submissionId, setSubmissionId] = useState<string>();
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
register,
|
||||
reset,
|
||||
formState: {},
|
||||
} = useForm();
|
||||
const [submittingPage, setSubmittingPage] = useState(false);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
}, [page, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page.endScreen) {
|
||||
fetch(`${formbricksUrl}/api/capture/forms/${formId}/submissions/${submissionId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ finished: true }),
|
||||
});
|
||||
onFinished({ submission });
|
||||
}
|
||||
}, [page, formId, formbricksUrl, submissionId]);
|
||||
|
||||
const sendToFormbricks = async (partialSubmission: any) => {
|
||||
const submissionBody: any = { data: partialSubmission };
|
||||
if (page.config?.addFieldsToCustomer && Array.isArray(page.config?.addFieldsToCustomer)) {
|
||||
for (const field of page.config?.addFieldsToCustomer) {
|
||||
if (field in partialSubmission) {
|
||||
if (!("customer" in submissionBody)) {
|
||||
submissionBody.customer = {};
|
||||
}
|
||||
submissionBody.customer[field] = partialSubmission[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!submissionId) {
|
||||
const res = await Promise.all([
|
||||
fetch(`${formbricksUrl}/api/capture/forms/${formId}/submissions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(submissionBody),
|
||||
}),
|
||||
fetch(`${formbricksUrl}/api/capture/forms/${formId}/schema`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(schema),
|
||||
}),
|
||||
]);
|
||||
if (!res[0].ok || !res[1].ok) {
|
||||
alert("There was an error sending this form. Please contact us at hola@formbricks.com");
|
||||
return;
|
||||
}
|
||||
const submission = await res[0].json();
|
||||
setSubmissionId(submission.id);
|
||||
} else {
|
||||
const res = await fetch(`${formbricksUrl}/api/capture/forms/${formId}/submissions/${submissionId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(submissionBody),
|
||||
});
|
||||
if (!res.ok) {
|
||||
alert("There was an error sending this form. Please contact us at hola@formbricks.com");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const submitPage = async (data: any) => {
|
||||
setSubmittingPage(true);
|
||||
const updatedSubmission = { ...submission, ...data };
|
||||
setSubmission(updatedSubmission);
|
||||
try {
|
||||
await sendToFormbricks(data);
|
||||
setSubmittingPage(false);
|
||||
onPageSubmit({ submission: updatedSubmission, page, pageSubmission: data });
|
||||
window.scrollTo(0, 0);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("There was an error sending this form. Please contact us at hola@formbricks.com");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitElement = () => {
|
||||
if (page.config?.autoSubmit && page.elements.length == 1) {
|
||||
formRef.current?.requestSubmit();
|
||||
setSubmittingPage(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submitPage)} ref={formRef}>
|
||||
{page.elements.map((element) => {
|
||||
const ElementComponent = element.component;
|
||||
return (
|
||||
<div key={element.id}>
|
||||
{element.name ? (
|
||||
<ElementComponent
|
||||
element={element}
|
||||
control={control}
|
||||
register={register}
|
||||
onSubmit={() => handleSubmitElement()}
|
||||
disabled={submittingPage}
|
||||
allowSkip={page.config?.allowSkip}
|
||||
skipAction={onSkip}
|
||||
autoSubmit={page.config?.autoSubmit}
|
||||
loading={submittingPage}
|
||||
/>
|
||||
) : (
|
||||
<ElementComponent element={element} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
106
packages/engine-react/src/components/FormbricksEngine.tsx
Normal file
106
packages/engine-react/src/components/FormbricksEngine.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Form } from "../types";
|
||||
import { EnginePage } from "./EnginePage";
|
||||
|
||||
interface FormProps {
|
||||
schema: Form;
|
||||
formbricksUrl: string;
|
||||
formId: string;
|
||||
onFinished?: ({ submission }: any) => void;
|
||||
onPageSubmit?: ({ page }: any) => void;
|
||||
}
|
||||
|
||||
export function FormbricksEngine({
|
||||
schema,
|
||||
formbricksUrl,
|
||||
formId,
|
||||
onFinished = () => {},
|
||||
onPageSubmit = () => {},
|
||||
}: FormProps) {
|
||||
if (!schema) {
|
||||
console.error("Formbricks Engine: No form provided");
|
||||
return null;
|
||||
}
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(schema.pages[0]);
|
||||
const [submission, setSubmission] = useState<any>({});
|
||||
const [finished, setFinished] = useState(false);
|
||||
|
||||
const cleanedSchema = useMemo(() => generateSchema(schema), [schema]);
|
||||
|
||||
useEffect(() => {
|
||||
// warmup request
|
||||
fetch(`${formbricksUrl}/api/capture/forms/${formId}/submissions`, {
|
||||
method: "OPTIONS",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const navigateToNextPage = (currentSubmission: any) => {
|
||||
const nextPage = calculateNextPage(schema, currentSubmission);
|
||||
setCurrentPage(nextPage);
|
||||
if (nextPage.endScreen) {
|
||||
setFinished(true);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateNextPage = (Form: Form, submission: any) => {
|
||||
if (currentPage.branchingRules) {
|
||||
for (const rule of currentPage.branchingRules) {
|
||||
if (rule.type === "value") {
|
||||
if (rule.value === submission[rule.name]) {
|
||||
const nextPage = Form.pages.find((p) => p.id === rule.nextPageId);
|
||||
if (!nextPage) {
|
||||
throw new Error(`Next page ${rule.nextPageId} not found`);
|
||||
}
|
||||
return nextPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const currentPageIdx = Form.pages.findIndex((p) => p.id === currentPage.id);
|
||||
return Form.pages[currentPageIdx + 1];
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EnginePage
|
||||
page={currentPage}
|
||||
onSkip={() => navigateToNextPage(submission)}
|
||||
onPageSubmit={({ submission, page, pageSubmission }: any) => {
|
||||
navigateToNextPage(submission);
|
||||
onPageSubmit({ submission, page, pageSubmission });
|
||||
}}
|
||||
onFinished={onFinished}
|
||||
submission={submission}
|
||||
setSubmission={setSubmission}
|
||||
finished={finished}
|
||||
formbricksUrl={formbricksUrl}
|
||||
formId={formId}
|
||||
schema={cleanedSchema}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function generateSchema(Form: Form) {
|
||||
const schema: any = JSON.parse(JSON.stringify(Form));
|
||||
deleteProps(schema, "frontend");
|
||||
return schema;
|
||||
}
|
||||
|
||||
function deleteProps(obj: any, propName: string) {
|
||||
if (Array.isArray(obj)) {
|
||||
for (let v of obj) {
|
||||
if (v instanceof Object) {
|
||||
deleteProps(v, propName);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
delete obj[propName];
|
||||
for (let v of Object.values(obj)) {
|
||||
if (v instanceof Object) {
|
||||
deleteProps(v, propName);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
packages/engine-react/src/index.tsx
Normal file
1
packages/engine-react/src/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./components/FormbricksEngine";
|
||||
41
packages/engine-react/src/lib/schema.ts
Normal file
41
packages/engine-react/src/lib/schema.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useContext, useEffect } from "react";
|
||||
import { SchemaContext } from "../components/FormbricksEngine";
|
||||
|
||||
export const getOptionsSchema = (options: any[] | undefined) => {
|
||||
const newOptions = [];
|
||||
if (options) {
|
||||
for (const option of options) {
|
||||
if (typeof option === "string") {
|
||||
newOptions.push({ label: option, value: option });
|
||||
}
|
||||
if (typeof option === "object" && "value" in option && "label" in option) {
|
||||
newOptions.push({ label: option.label, value: option.value });
|
||||
}
|
||||
}
|
||||
}
|
||||
return newOptions;
|
||||
};
|
||||
|
||||
export const useSchema = () => {
|
||||
const { schema } = useContext(SchemaContext);
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const useEffectUpdateSchema = (props: any, type: string) => {
|
||||
const { setSchema } = useContext(SchemaContext);
|
||||
|
||||
useEffect(() => {
|
||||
setSchema((schema: any) => {
|
||||
const newSchema = JSON.parse(JSON.stringify(schema));
|
||||
let elementIdx = newSchema.pages[0].elements.findIndex((e: any) => e.name === props.name);
|
||||
if (elementIdx === -1) {
|
||||
newSchema.pages[0].elements.push({ ...props, type });
|
||||
elementIdx = newSchema.pages[0].elements.length - 1; // set elementIdx to newly added elem
|
||||
}
|
||||
if ("options" in props) {
|
||||
newSchema.pages[0].elements[elementIdx].options = getOptionsSchema(props.options);
|
||||
}
|
||||
return newSchema;
|
||||
});
|
||||
}, [props, setSchema]);
|
||||
};
|
||||
40
packages/engine-react/src/types.tsx
Normal file
40
packages/engine-react/src/types.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface FormOption {
|
||||
label: string;
|
||||
value: string;
|
||||
frontend?: any;
|
||||
}
|
||||
|
||||
export interface FormPage {
|
||||
id: string;
|
||||
endScreen?: boolean;
|
||||
elements: FormElement[];
|
||||
config?: {
|
||||
addFieldsToCustomer?: string[];
|
||||
autoSubmit?: boolean;
|
||||
allowSkip?: boolean;
|
||||
};
|
||||
branchingRules?: {
|
||||
type: "value";
|
||||
name: string;
|
||||
value: string;
|
||||
nextPageId: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface FormElement {
|
||||
id: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
type: "radio" | "text" | "checkbox" | "html";
|
||||
options?: FormOption[];
|
||||
component: React.FC<any>;
|
||||
frontend?: any;
|
||||
}
|
||||
|
||||
export interface Form {
|
||||
pages: FormPage[];
|
||||
config?: {
|
||||
progressBar?: boolean;
|
||||
};
|
||||
}
|
||||
5
packages/engine-react/tsconfig.json
Normal file
5
packages/engine-react/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "@formbricks/tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
12
packages/engine-react/tsup.config.js
Normal file
12
packages/engine-react/tsup.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
export default defineConfig({
|
||||
format: ["cjs", "esm"],
|
||||
entry: ["src/index.tsx"],
|
||||
clean: isProduction,
|
||||
splitting: true,
|
||||
dts: true,
|
||||
minify: isProduction,
|
||||
});
|
||||
@@ -20,7 +20,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsup --dts && tailwindcss -i ./src/styles.css -o ./dist/styles.css --minify",
|
||||
"dev": "concurrently \"tsup --dts --external react --watch && generate-tailwind\" \"tailwindcss -i ./src/styles.css -o ./dist/styles.css --watch\"",
|
||||
"dev": "concurrently \"tsup --dts --external react --watch\" \"tailwindcss -i ./src/styles.css -o ./dist/styles.css --watch\"",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
66
pnpm-lock.yaml
generated
66
pnpm-lock.yaml
generated
@@ -66,6 +66,7 @@ importers:
|
||||
apps/formbricks-com:
|
||||
specifiers:
|
||||
'@docsearch/react': ^3.3.2
|
||||
'@formbricks/engine-react': workspace:*
|
||||
'@formbricks/pmf': workspace:*
|
||||
'@formbricks/react': workspace:*
|
||||
'@formbricks/ui': workspace:*
|
||||
@@ -101,6 +102,7 @@ importers:
|
||||
typescript: 4.9.4
|
||||
dependencies:
|
||||
'@docsearch/react': 3.3.2_hqhntm65ctxq3v64bjwdr5bfcq
|
||||
'@formbricks/engine-react': link:../../packages/engine-react
|
||||
'@formbricks/pmf': link:../../packages/pmfWidget
|
||||
'@formbricks/react': link:../../packages/react
|
||||
'@formbricks/ui': link:../../packages/ui
|
||||
@@ -339,6 +341,33 @@ importers:
|
||||
react: 18.2.0
|
||||
typescript: 4.9.4
|
||||
|
||||
packages/engine-react:
|
||||
specifiers:
|
||||
'@formbricks/tsconfig': workspace:*
|
||||
'@types/react': ^18.0.25
|
||||
'@types/react-dom': ^18.0.8
|
||||
concurrently: ^7.5.0
|
||||
eslint: ^8.27.0
|
||||
eslint-config-formbricks: workspace:*
|
||||
react: ^18.2.0
|
||||
react-hook-form: ^7.39.1
|
||||
tailwindcss: ^3.2.2
|
||||
tsup: ^6.4.0
|
||||
typescript: ^4.8.4
|
||||
dependencies:
|
||||
react-hook-form: 7.43.0_react@18.2.0
|
||||
devDependencies:
|
||||
'@formbricks/tsconfig': link:../tsconfig
|
||||
'@types/react': 18.0.27
|
||||
'@types/react-dom': 18.0.10
|
||||
concurrently: 7.6.0
|
||||
eslint: 8.33.0
|
||||
eslint-config-formbricks: link:../eslint-config-formbricks
|
||||
react: 18.2.0
|
||||
tailwindcss: 3.2.4_postcss@8.4.21
|
||||
tsup: 6.5.0_uujdqti2krmttzhqvubwnsmcci
|
||||
typescript: 4.9.5
|
||||
|
||||
packages/eslint-config-formbricks:
|
||||
specifiers:
|
||||
eslint: ^8.26.0
|
||||
@@ -18846,6 +18875,43 @@ packages:
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/tsup/6.5.0_uujdqti2krmttzhqvubwnsmcci:
|
||||
resolution: {integrity: sha512-36u82r7rYqRHFkD15R20Cd4ercPkbYmuvRkz3Q1LCm5BsiFNUgpo36zbjVhCOgvjyxNBWNKHsaD5Rl8SykfzNA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@swc/core': ^1
|
||||
postcss: ^8.4.12
|
||||
typescript: ^4.1.0
|
||||
peerDependenciesMeta:
|
||||
'@swc/core':
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
bundle-require: 3.1.2_esbuild@0.15.16
|
||||
cac: 6.7.14
|
||||
chokidar: 3.5.3
|
||||
debug: 4.3.4
|
||||
esbuild: 0.15.16
|
||||
execa: 5.1.1
|
||||
globby: 11.1.0
|
||||
joycon: 3.1.1
|
||||
postcss: 8.4.21
|
||||
postcss-load-config: 3.1.4_postcss@8.4.21
|
||||
resolve-from: 5.0.0
|
||||
rollup: 3.5.1
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.29.0
|
||||
tree-kill: 1.2.2
|
||||
typescript: 4.9.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/tsutils/3.21.0_typescript@4.9.3:
|
||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
Reference in New Issue
Block a user