mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
hq - update forms overview, add Nps Type, update charts lib, update react lib
This commit is contained in:
6
.changeset/gorgeous-owls-shout.md
Normal file
6
.changeset/gorgeous-owls-shout.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@formbricks/charts": minor
|
||||
"@formbricks/react": minor
|
||||
---
|
||||
|
||||
Add Nps Type, Update missing styling classes, minor bug fixes
|
||||
@@ -4,50 +4,23 @@ import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useForm } from "@/lib/forms";
|
||||
import { useTeam } from "@/lib/teams";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { UserIcon } from "@heroicons/react/20/solid";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import Prism from "prismjs";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { FaReact, FaVuejs } from "react-icons/fa";
|
||||
import { AiFillHtml5 } from "react-icons/ai";
|
||||
import { toast } from "react-toastify";
|
||||
import { useRouter } from "next/router";
|
||||
import Prism from "prismjs";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AiFillApi } from "react-icons/ai";
|
||||
import { FaReact, FaVuejs } from "react-icons/fa";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
require("prismjs/components/prism-javascript");
|
||||
|
||||
const getLibs = (formId: string) => [
|
||||
{
|
||||
id: "react",
|
||||
name: "React",
|
||||
href: `https://formbricks.com/docs/react-form-library/introduction`,
|
||||
bgColor: "bg-brand-dark",
|
||||
target: "_blank",
|
||||
icon: FaReact,
|
||||
},
|
||||
{
|
||||
id: "html",
|
||||
name: "HTML",
|
||||
href: `https://formbricks.com/docs/react-form-library/link-formbricks-hq`,
|
||||
bgColor: "bg-brand-dark",
|
||||
target: "_blank",
|
||||
icon: AiFillHtml5,
|
||||
},
|
||||
{
|
||||
id: "reactNative",
|
||||
name: "React Native",
|
||||
comingSoon: true,
|
||||
href: "#",
|
||||
disabled: true,
|
||||
icon: FaReact,
|
||||
},
|
||||
{
|
||||
id: "vue",
|
||||
name: "Vue.js",
|
||||
comingSoon: true,
|
||||
href: "#",
|
||||
disabled: true,
|
||||
icon: FaVuejs,
|
||||
},
|
||||
const tabs = [
|
||||
{ id: "overview", name: "Overview", icon: UserIcon },
|
||||
{ id: "api", name: "API", icon: AiFillApi },
|
||||
{ id: "react", name: "React", icon: FaReact },
|
||||
{ id: "vue", name: "Vue", icon: FaVuejs },
|
||||
];
|
||||
|
||||
export default function FormOverviewPage() {
|
||||
@@ -57,11 +30,7 @@ export default function FormOverviewPage() {
|
||||
router.query.teamId?.toString()
|
||||
);
|
||||
const { team, isLoadingTeam, isErrorTeam } = useTeam(router.query.teamId?.toString());
|
||||
const libs = useMemo(() => {
|
||||
if (form) {
|
||||
return getLibs(form.id);
|
||||
}
|
||||
}, [form]);
|
||||
const [activeTab, setActiveTab] = useState(tabs[0]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingForm) {
|
||||
@@ -91,47 +60,144 @@ export default function FormOverviewPage() {
|
||||
</h1>
|
||||
</header>
|
||||
<div>
|
||||
<div className="mx-auto mt-8">
|
||||
<h1 className="text-xl font-bold leading-tight text-slate-900">Connect your form</h1>
|
||||
<div>
|
||||
<div className="sm:hidden">
|
||||
<label htmlFor="tabs" className="sr-only">
|
||||
Select a tab
|
||||
</label>
|
||||
{/* Use an "onChange" listener to redirect the user to the selected tab URL. */}
|
||||
<select
|
||||
id="tabs"
|
||||
name="tabs"
|
||||
className="block w-full rounded-md border-gray-300 focus:border-teal-500 focus:ring-teal-500"
|
||||
defaultValue={activeTab.name}>
|
||||
{tabs.map((tab) => (
|
||||
<option key={tab.name}>{tab.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={clsx(
|
||||
activeTab.name === tab.name
|
||||
? "border-teal-500 text-teal-600"
|
||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium"
|
||||
)}
|
||||
aria-current={activeTab.name === tab.name ? "page" : undefined}>
|
||||
<tab.icon
|
||||
className={clsx(
|
||||
activeTab.name === tab.name
|
||||
? "text-teal-500"
|
||||
: "text-gray-400 group-hover:text-gray-500",
|
||||
"-ml-0.5 mr-2 h-5 w-5"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{tab.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-10">
|
||||
{activeTab.id === "overview" ? (
|
||||
<div>
|
||||
<div className="mt-4 mb-12">
|
||||
<p className="text-slate-700">
|
||||
To get started post your submission to the Formbricks HQ capture endpoint. To enable the
|
||||
summary feature also set the schema of this form.
|
||||
<br />
|
||||
If you are using the Formbricks React Library it's even easier to get started. All you
|
||||
need is you formId and we take care of the rest (including the schema).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="formId" className="block text-base text-slate-800">
|
||||
Your form ID
|
||||
</label>
|
||||
<div className="mt-3 w-96">
|
||||
<input
|
||||
id="formId"
|
||||
type="text"
|
||||
className="focus:border-brand focus:ring-brand block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100 sm:text-sm"
|
||||
value={form.id}
|
||||
disabled
|
||||
/>
|
||||
<div className="mt-4 mb-12 text-sm text-gray-600">
|
||||
<p className="text-slate-700">
|
||||
To get started post your submission to the Formbricks HQ capture endpoint. All submissions
|
||||
are stored in Formbricks HQ and can be viewed here.
|
||||
<br /> <br />
|
||||
If you want to get notified when a submission is made you can also set up a webhook or email
|
||||
notifications in{" "}
|
||||
<Link
|
||||
href={`/app/teams/${router.query.teamId}/forms/${router.query.formId}/pipelines`}
|
||||
className="underline">
|
||||
Pipelines
|
||||
</Link>
|
||||
.<br />
|
||||
<br />
|
||||
Optionally you can set a schema for your form. This schema tells Formbricks HQ more about
|
||||
the structure of your form and enables better form evaluation, e.g. displays the correct
|
||||
labels for your form fields in the Formbricks HQ UI insted of the fieldName or filters data
|
||||
that doesn't match the schema. The easiest way to get started with a schema is to used
|
||||
our react library because it handles schema creation and sending to Formbricks HQ
|
||||
automatically.
|
||||
<br /> To learn more about the schema please check out our{" "}
|
||||
<Link href="https://formbricks.com/docs/formbricks-hq/schema">docs</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-2 w-full justify-center"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.id);
|
||||
toast("Copied form ID to clipboard");
|
||||
}}>
|
||||
copy
|
||||
</Button>
|
||||
<div>
|
||||
<label htmlFor="formId" className="block text-base text-slate-800">
|
||||
Your form ID
|
||||
</label>
|
||||
<div className="mt-3 w-96">
|
||||
<input
|
||||
id="formId"
|
||||
type="text"
|
||||
className="focus:border-brand focus:ring-brand block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100 sm:text-sm"
|
||||
value={form.id}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-2 w-full justify-center"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.id);
|
||||
toast("Copied form ID to clipboard");
|
||||
}}>
|
||||
copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-6" />
|
||||
<div className="max-w-2xl">
|
||||
<label htmlFor="formId" className="block text-base text-slate-800">
|
||||
Capture POST Url:
|
||||
</label>
|
||||
<div className="mt-3">
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-200 px-3 text-gray-500 sm:text-sm">
|
||||
POST
|
||||
</span>
|
||||
<input
|
||||
id="captureUrl"
|
||||
type="text"
|
||||
className="focus:border-brand focus:ring-brand block w-full rounded-r-md border-gray-300 bg-gray-100 shadow-sm sm:text-sm"
|
||||
value={`${window.location.protocol}//${window.location.host}/capture/forms/${form.id}/submissions`}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-2 w-full justify-center"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.id);
|
||||
toast("Copied form url to clipboard");
|
||||
}}>
|
||||
copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-6" />
|
||||
</div>
|
||||
) : activeTab.id === "api" ? (
|
||||
<div className="mt-5">
|
||||
<p className="my-3 text-sm text-gray-600">
|
||||
You can send submissions directly to Formbricks HQ via our API. The API doesn't need any
|
||||
authentication and can also called in the users browser.
|
||||
</p>
|
||||
<hr className="my-8" />
|
||||
<div className="max-w-2xl">
|
||||
<label htmlFor="formId" className="block text-base text-slate-800">
|
||||
Capture POST Url:
|
||||
@@ -161,38 +227,133 @@ export default function FormOverviewPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
<div className="rounded-md bg-black p-8 text-sm font-light text-gray-200">
|
||||
<pre>
|
||||
<code className="language-js whitespace-pre-wrap">
|
||||
{`{
|
||||
"customerId": "user@example.com", /* optional */
|
||||
"data": {
|
||||
"firstname": "John",
|
||||
"lastname": "Doe",
|
||||
"feedback": "I like the app very much"
|
||||
}
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-black p-8 font-light text-gray-200">
|
||||
<pre>
|
||||
<code className="language-js whitespace-pre-wrap">
|
||||
{`import { Form, Text, Email, Checkbox, Submit, sendToHQ } from "@formbricks/react";
|
||||
) : activeTab.id === "react" ? (
|
||||
<div className="mt-5">
|
||||
<p className="my-3 text-sm text-gray-600">
|
||||
The best way to send submissions to Formbricks HQ in React is our simple to use{" "}
|
||||
<Link target="_blank" href="https://www.npmjs.com/package/@formbricks/react">
|
||||
React Library
|
||||
</Link>
|
||||
because it also creates and sends a schema to Formbricks HQ.
|
||||
</p>
|
||||
<div className="rounded-md bg-black p-8 text-sm font-light text-gray-200">
|
||||
<pre>
|
||||
<code className="language-js whitespace-pre-wrap">
|
||||
{`import { Form, Text, Email, Checkbox, Submit, sendToHq } from "@formbricks/react";
|
||||
import "@formbricks/react/styles.css";
|
||||
|
||||
export default function WaitlistForm() {
|
||||
return (
|
||||
<Form formId="${form.id}" hqUrl="${window.location.protocol}//${window.location.host}" onSubmit={sendToHQ}>
|
||||
<Text name="name" label="What's your name?" validation="required" />
|
||||
<Email
|
||||
name="email"
|
||||
label="What's your email address?"
|
||||
placeholder="you@example.com"
|
||||
validation="required|email"
|
||||
/>
|
||||
<Checkbox
|
||||
name="terms"
|
||||
label="Terms & Conditions"
|
||||
help="To use our service, please accept."
|
||||
validation="accepted"
|
||||
/>
|
||||
<Submit label="Let's go!" />
|
||||
</Form>
|
||||
);
|
||||
return (
|
||||
<Form formId="${form.id}" hqUrl="${window.location.protocol}//${window.location.host}" onSubmit={sendToHq}>
|
||||
<Text name="name" label="What's your name?" validation="required" />
|
||||
<Email
|
||||
name="email"
|
||||
label="What's your email address?"
|
||||
placeholder="you@example.com"
|
||||
validation="required|email"
|
||||
/>
|
||||
<Checkbox
|
||||
name="terms"
|
||||
label="Terms & Conditions"
|
||||
help="To use our service, please accept."
|
||||
validation="accepted"
|
||||
/>
|
||||
<Submit label="Let's go!" />
|
||||
</Form>
|
||||
);
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<p className="my-3 text-sm text-gray-600">
|
||||
But you can also use the default React Form functionality (or another form library) to send the
|
||||
submissions to Formbricks HQ.
|
||||
</p>
|
||||
<div className="rounded-md bg-black p-8 text-sm font-light text-gray-200">
|
||||
<pre>
|
||||
<code className="language-js whitespace-pre-wrap">
|
||||
{`<form
|
||||
onSubmit={({ data }) => {
|
||||
fetch("${window.location.protocol}//${window.location.host}/capture/forms/${form.id}/submissions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({data}),
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
}}>
|
||||
{/* YOUR FORM */}
|
||||
</form>`}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-16">
|
||||
) : activeTab.id === "vue" ? (
|
||||
<div className="mt-5">
|
||||
{" "}
|
||||
<p className="my-3 text-sm text-gray-600">
|
||||
To send a submission in Vue.Js you can use the default form functionality.
|
||||
</p>
|
||||
<div className="rounded-md bg-black p-8 text-sm font-light text-gray-200">
|
||||
<pre>
|
||||
<code className="language-js whitespace-pre-wrap">
|
||||
{`<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<label>
|
||||
<span>Email</span>
|
||||
<input type="email" name="email" v-model="email" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Message</span>
|
||||
<textarea name="message" v-model="message"></textarea>
|
||||
</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
message: '',
|
||||
endpoint: '${window.location.protocol}//${window.location.host}/capture/forms/${form.id}/submissions',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submitForm() {
|
||||
const data = {
|
||||
email: this.email,
|
||||
message: this.message,
|
||||
}
|
||||
const response = await this.$axios.post(this.endpoint, {data})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>`}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* <div className="mt-16">
|
||||
<h2 className="text-xl font-bold text-slate-800">Code your form</h2>
|
||||
<div className="mt-4 mb-12">
|
||||
<p className="text-slate-800">
|
||||
@@ -251,7 +412,7 @@ export default function WaitlistForm() {
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import AnalyticsCard from "@/components/AnalyticsCard";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useForm } from "@/lib/forms";
|
||||
import { useTeam } from "@/lib/teams";
|
||||
import { ExclamationTriangleIcon, InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
import { Bar, Table } from "@formbricks/charts";
|
||||
import { useSubmissions } from "@/lib/submissions";
|
||||
import AnalyticsCard from "@/components/AnalyticsCard";
|
||||
import { useTeam } from "@/lib/teams";
|
||||
import { Bar, Nps, Table } from "@formbricks/charts";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function SummaryPage() {
|
||||
@@ -107,6 +107,16 @@ export default function SummaryPage() {
|
||||
</h2>
|
||||
<Bar submissions={submissions} schema={form.schema} fieldName={elem.name} />
|
||||
</div>
|
||||
) : ["nps"].includes(elem.type) ? (
|
||||
<div>
|
||||
<h2 className="mb-6 text-xl font-bold leading-tight tracking-tight text-gray-900">
|
||||
{elem.label}
|
||||
<span className="text-brand-dark ml-4 inline-flex items-center rounded-md border border-teal-100 bg-teal-50 px-2.5 py-0.5 text-sm font-medium">
|
||||
{elem.type}
|
||||
</span>
|
||||
</h2>
|
||||
<Nps submissions={submissions} schema={form.schema} fieldName={elem.name} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { hashString } from "./utils";
|
||||
and we cannot trace anything back to you or your customers. If you still want to
|
||||
disable telemetry, set the environment variable TELEMETRY_DISABLED=1 */
|
||||
|
||||
export const captureTelemetryEvent = async (eventName: string, properties = {}) => {
|
||||
export const captureTelemetry = async (eventName: string, properties = {}) => {
|
||||
if (
|
||||
process.env.TELEMETRY_DISABLED !== "1" &&
|
||||
process.env.NODE_ENV === "production" &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { runPipelines } from "@/lib/pipelinesHandler";
|
||||
import { capturePosthogEvent } from "@/lib/posthog";
|
||||
import { captureTelemetry } from "@/lib/telemetry";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import NextCors from "nextjs-cors";
|
||||
@@ -67,6 +68,3 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
|
||||
}
|
||||
}
|
||||
function captureTelemetry(arg0: string) {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
||||
@@ -18,12 +18,10 @@ export function FbBar({ color, submissions, schema, fieldName }: Props) {
|
||||
}
|
||||
// build data object by finding schema definition of field and scanning submissions for this key
|
||||
const dataDict: any = {};
|
||||
console.log(fieldName);
|
||||
const schemaElem = schema.children.find((e: any) => e.name === fieldName);
|
||||
if (typeof schemaElem === "undefined") {
|
||||
throw Error("key not found in schema");
|
||||
}
|
||||
console.log(JSON.stringify(schemaElem, null, 2));
|
||||
for (const option of schemaElem.options) {
|
||||
dataDict[option.value] = { name: option.label, value: 0 };
|
||||
}
|
||||
|
||||
59
packages/charts/src/charts/Nps.tsx
Normal file
59
packages/charts/src/charts/Nps.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useMemo } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, Legend, Tooltip, XAxis, YAxis } from "recharts";
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
submissions: any;
|
||||
schema: any;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
export function Nps({ color, submissions, schema, fieldName }: Props) {
|
||||
const data = useMemo(() => {
|
||||
if (!fieldName) {
|
||||
throw Error("no field name provided");
|
||||
}
|
||||
if (!submissions || !schema || Object.keys(schema).length === 0) {
|
||||
return [];
|
||||
}
|
||||
// build data object by finding schema definition of field and scanning submissions for this key
|
||||
const dataDict: any = {};
|
||||
const schemaElem = schema.children.find((e: any) => e.name === fieldName);
|
||||
if (typeof schemaElem === "undefined") {
|
||||
throw Error("key not found in schema");
|
||||
}
|
||||
for (const option of [...Array(11).keys()]) {
|
||||
dataDict[option] = { name: option, value: 0 };
|
||||
}
|
||||
console.log(dataDict);
|
||||
for (const submission of submissions) {
|
||||
if (fieldName in submission.data) {
|
||||
if (submission.data[fieldName] in dataDict) {
|
||||
dataDict[submission.data[fieldName]] = {
|
||||
...dataDict[submission.data[fieldName]],
|
||||
value: dataDict[submission.data[fieldName]].value + 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// transform dataDict to desired form
|
||||
const data = [];
|
||||
for (const entry of Object.entries(dataDict)) {
|
||||
data.push(entry[1]);
|
||||
}
|
||||
return data;
|
||||
}, [submissions, schema]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BarChart width={730} height={250} data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis label={{ value: "# answers", angle: -90, position: "insideLeft" }} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill={color || "#00C4B8"} />
|
||||
</BarChart>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./charts/Bar";
|
||||
export * from "./charts/Nps";
|
||||
export * from "./charts/Table";
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.4",
|
||||
"@hookform/error-message": "^2.0.1",
|
||||
"clsx": "^1.2.1",
|
||||
"react-hook-form": "^7.39.1"
|
||||
|
||||
92
packages/react/src/components/inputs/Nps.tsx
Normal file
92
packages/react/src/components/inputs/Nps.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { getElementId } from "../../lib/element";
|
||||
import { useEffectUpdateSchema } from "../../lib/schema";
|
||||
import { NameRequired, UniversalInputProps } from "../../types";
|
||||
import { Help } from "../shared/Help";
|
||||
import { Inner } from "../shared/Inner";
|
||||
import { Label } from "../shared/Label";
|
||||
import { Messages } from "../shared/Messages";
|
||||
import { Options } from "../shared/Options";
|
||||
import { Outer } from "../shared/Outer";
|
||||
import { Wrapper } from "../shared/Wrapper";
|
||||
|
||||
interface NpsInputUniqueProps {
|
||||
optionsClassName?: string;
|
||||
optionClassName?: string;
|
||||
}
|
||||
|
||||
type NpsProps = NpsInputUniqueProps & UniversalInputProps & NameRequired;
|
||||
|
||||
const inputType = "nps";
|
||||
|
||||
export function Nps(props: NpsProps) {
|
||||
const elemId = useMemo(() => getElementId(props.id, props.name), [props.id, props.name]);
|
||||
useEffectUpdateSchema(props, inputType);
|
||||
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<Outer inputType={inputType} outerClassName={props.outerClassName}>
|
||||
<Wrapper wrapperClassName={props.wrapperClassName}>
|
||||
<Label label={props.label} elemId={elemId} labelClassName={props.labelClassName} />
|
||||
<Inner innerClassName={props.innerClassName}>
|
||||
<Controller
|
||||
name={props.name}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }: any) => (
|
||||
<RadioGroup {...field} id="test">
|
||||
<RadioGroup.Label className="sr-only">Choose a number from 0 to 10</RadioGroup.Label>
|
||||
<Options optionsClassName={props.optionsClassName}>
|
||||
{[...Array(11).keys()].map((option) => (
|
||||
<RadioGroup.Option
|
||||
key={option}
|
||||
value={option}
|
||||
className={props.optionClassName || "formbricks-option"}>
|
||||
<Wrapper wrapperClassName={props.wrapperClassName}>
|
||||
<Inner innerClassName={props.innerClassName}>
|
||||
<RadioGroup.Label as="span" className={props.inputClassName || "formbricks-input"}>
|
||||
{option}
|
||||
</RadioGroup.Label>
|
||||
</Inner>
|
||||
</Wrapper>
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</Options>
|
||||
<div className="formbricks-input-addition">
|
||||
<p className="formbricks-input-addition-item">not likely</p>
|
||||
<p className="formbricks-input-addition-item">very likely</p>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <textarea
|
||||
className={props.inputClassName || "formbricks-input"}
|
||||
id={elemId}
|
||||
placeholder={props.placeholder || ""}
|
||||
cols={props.cols}
|
||||
rows={props.rows}
|
||||
aria-invalid={errors[props.name] ? "true" : "false"}
|
||||
{...register(props.name, {
|
||||
required: { value: "required" in validationRules, message: "This field is required" },
|
||||
minLength: {
|
||||
value: props.minLength || 0,
|
||||
message: `Your answer must be at least ${props.minLength} characters long`,
|
||||
},
|
||||
maxLength: {
|
||||
value: props.maxLength || 524288,
|
||||
message: `Your answer musn't be longer than ${props.maxLength} characters`,
|
||||
},
|
||||
validate: validate(validationRules),
|
||||
})}
|
||||
/> */}
|
||||
</Inner>
|
||||
</Wrapper>
|
||||
<Help help={props.help} elemId={elemId} helpClassName={props.helpClassName} />
|
||||
<Messages {...props} />
|
||||
</Outer>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export function Textarea(props: TextareaProps) {
|
||||
<Label label={props.label} elemId={elemId} labelClassName={props.labelClassName} />
|
||||
<Inner innerClassName={props.innerClassName}>
|
||||
<textarea
|
||||
className="formbricks-input"
|
||||
className={props.inputClassName || "formbricks-input"}
|
||||
id={elemId}
|
||||
placeholder={props.placeholder || ""}
|
||||
cols={props.cols}
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from "./components/FormbricksSchema";
|
||||
export * from "./components/inputs/Button";
|
||||
export * from "./components/inputs/Checkbox";
|
||||
export * from "./components/inputs/Email";
|
||||
export * from "./components/inputs/Nps";
|
||||
export * from "./components/inputs/Number";
|
||||
export * from "./components/inputs/Password";
|
||||
export * from "./components/inputs/Phone";
|
||||
|
||||
@@ -55,3 +55,27 @@ textarea.formbricks-input {
|
||||
.formbricks-fieldset {
|
||||
@apply border-gray-50 rounded-lg max-w-md;
|
||||
}
|
||||
|
||||
.formbricks-input-nps {
|
||||
@apply flex cursor-pointer items-center justify-center rounded-md border py-3 px-3 text-sm font-medium uppercase focus:outline-none sm:flex-1;
|
||||
}
|
||||
|
||||
[data-type="nps"] .formbricks-options {
|
||||
@apply grid grid-cols-11 gap-1 sm:grid-cols-11 mt-2;
|
||||
}
|
||||
|
||||
[data-type="nps"] .formbricks-option[aria-checked="true"] {
|
||||
@apply border-transparent bg-slate-600 text-white hover:bg-slate-700;
|
||||
}
|
||||
|
||||
[data-type="nps"] .formbricks-option {
|
||||
@apply flex cursor-pointer items-center justify-center rounded-md border py-3 px-3 text-sm font-medium uppercase focus:outline-none sm:flex-1;
|
||||
}
|
||||
|
||||
[data-type="nps"] .formbricks-input-addition {
|
||||
@apply flex items-center justify-between mt-1;
|
||||
}
|
||||
|
||||
[data-type="nps"] .formbricks-input-addition-item {
|
||||
@apply text-xs text-gray-500;
|
||||
}
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -10,7 +10,7 @@ importers:
|
||||
turbo: latest
|
||||
devDependencies:
|
||||
'@changesets/cli': 2.25.0
|
||||
prettier: 2.8.0
|
||||
prettier: 2.8.1
|
||||
tsx: 3.9.0
|
||||
turbo: 1.6.3
|
||||
|
||||
@@ -273,6 +273,7 @@ importers:
|
||||
packages/react:
|
||||
specifiers:
|
||||
'@formbricks/tsconfig': workspace:*
|
||||
'@headlessui/react': ^1.7.4
|
||||
'@hookform/error-message': ^2.0.1
|
||||
'@types/react': ^18.0.25
|
||||
'@types/react-dom': ^18.0.8
|
||||
@@ -286,6 +287,7 @@ importers:
|
||||
tsup: ^6.4.0
|
||||
typescript: ^4.8.4
|
||||
dependencies:
|
||||
'@headlessui/react': 1.7.4_biqbaboplfbrettd7655fr4n2y
|
||||
'@hookform/error-message': 2.0.1_nrnvpvixg5xdweezd67llqjwze
|
||||
clsx: 1.2.1
|
||||
react-hook-form: 7.40.0_react@18.2.0
|
||||
@@ -1938,7 +1940,7 @@ packages:
|
||||
fs-extra: 7.0.1
|
||||
lodash.startcase: 4.4.0
|
||||
outdent: 0.5.0
|
||||
prettier: 2.8.0
|
||||
prettier: 2.8.1
|
||||
resolve-from: 5.0.0
|
||||
semver: 5.7.1
|
||||
dev: true
|
||||
@@ -2105,7 +2107,7 @@ packages:
|
||||
'@changesets/types': 5.2.0
|
||||
fs-extra: 7.0.1
|
||||
human-id: 1.0.2
|
||||
prettier: 2.8.0
|
||||
prettier: 2.8.1
|
||||
dev: true
|
||||
|
||||
/@cnakazawa/watch/1.0.4:
|
||||
@@ -13295,6 +13297,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/prettier/2.8.1:
|
||||
resolution: {integrity: sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/pretty-error/2.1.2:
|
||||
resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==}
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user