mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 02:55:04 -05:00
Add PMF dashboard to Formbricks App (#189)
* add PMF results dashboard * add PMF Segmentation View * add PMF setup instructions * Built new Filter Navigation that is also used in Feedback Co-authored-by: knugget <johannes@knugget.de>
This commit is contained in:
@@ -16,11 +16,15 @@ export default function PmfButton() {
|
||||
...window.formbricks,
|
||||
config: {
|
||||
formbricksUrl: "http://localhost:3000",
|
||||
formId: "clda6d0ot0000yzikvnnz07lm",
|
||||
formId: "cldbru2nu000s19t6mtc4bhk4",
|
||||
containerId: "formbricks",
|
||||
style: {
|
||||
brandColor: "#0891b2",
|
||||
},
|
||||
customer: {
|
||||
name: "John Doe",
|
||||
email: "john@doe.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
require("@formbricks/pmf");
|
||||
|
||||
@@ -9,7 +9,7 @@ declare global {
|
||||
|
||||
const formbricksConfig = {
|
||||
hqUrl: "http://localhost:3000",
|
||||
formId: "cld8pxn4j0000yznuo6qggxfu",
|
||||
formId: "cldbr2x45000i19t69ncbohnn",
|
||||
contact: {
|
||||
name: "Johannes",
|
||||
position: "Founder",
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
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 Textarea from "@/components/engine/Textarea";
|
||||
import ThankYouHeading from "@/components/engine/ThankYouHeading";
|
||||
import ThankYouPlans from "@/components/engine/ThankYouPlans";
|
||||
import LayoutWaitlist from "@/components/shared/LayoutWaitlist";
|
||||
import {
|
||||
OnboardingIcon,
|
||||
PMFIcon,
|
||||
DogChaserIcon,
|
||||
CancelSubscriptionIcon,
|
||||
InterviewPromptIcon,
|
||||
DoorIcon,
|
||||
FeedbackIcon,
|
||||
BugBlueIcon,
|
||||
AngryBirdRageIcon,
|
||||
FeatureRequestIcon,
|
||||
FounderIcon,
|
||||
EngineerIcon,
|
||||
LaptopWorkerIcon,
|
||||
UserCommentIcon,
|
||||
UserGroupIcon,
|
||||
BellIcon,
|
||||
SkyscraperIcon,
|
||||
CheckMarkIcon,
|
||||
CrossMarkIcon,
|
||||
UserCoupleIcon,
|
||||
} from "@formbricks/ui";
|
||||
|
||||
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>
|
||||
|
||||
<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" : "clda41dvz0004u08k3gbawcky"
|
||||
}
|
||||
survey={{
|
||||
config: {
|
||||
progressBar: false,
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
id: "pmfTypePage",
|
||||
config: {
|
||||
autoSubmit: true,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
id: "pmfType",
|
||||
component: IconRadio,
|
||||
type: "radio",
|
||||
name: "pmfType",
|
||||
label: "How disappointed would you be if you could no longer use our service?",
|
||||
options: [
|
||||
{ label: "Very disappointed", value: "veryDisappointed" },
|
||||
{ label: "Somewhat disappointed", value: "somewhatDisappointed" },
|
||||
{ label: "Not disappointed", value: "notDisappointed" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "mainBenefitPage",
|
||||
elements: [
|
||||
{
|
||||
id: "mainBenefit",
|
||||
component: Textarea,
|
||||
type: "text",
|
||||
name: "mainBenefit",
|
||||
label: "What is the main benefit you receive from our service?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "userSegmentPage",
|
||||
config: {
|
||||
autoSubmit: true,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
id: "userSegment",
|
||||
component: IconRadio,
|
||||
type: "radio",
|
||||
name: "userSegment",
|
||||
label: "What is your job title?",
|
||||
options: [
|
||||
{ label: "Founder", value: "founder" },
|
||||
{ label: "Executive", value: "executive" },
|
||||
{ label: "Product Manager", value: "product manager" },
|
||||
{ label: "Software Engineer", value: "engineer" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "improvementPage",
|
||||
elements: [
|
||||
{
|
||||
id: "improvement",
|
||||
component: Textarea,
|
||||
type: "text",
|
||||
name: "improvement",
|
||||
label: "How can we improve our service for you?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "selfSegmentationPage",
|
||||
elements: [
|
||||
{
|
||||
id: "selfSegmentation",
|
||||
component: Textarea,
|
||||
type: "text",
|
||||
name: "selfSegmentation",
|
||||
label: "What type of people would benefit most from using our service?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "thankYouPage",
|
||||
endScreen: true,
|
||||
elements: [
|
||||
{
|
||||
id: "thankYou",
|
||||
component: ThankYouHeading,
|
||||
type: "html",
|
||||
name: "thankYou",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutWaitlist>
|
||||
);
|
||||
|
||||
export default WaitlistPage;
|
||||
@@ -3,7 +3,7 @@
|
||||
import { createForm } from "@/lib/forms";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { Dialog, RadioGroup, Transition } from "@headlessui/react";
|
||||
import { UserIcon } from "@heroicons/react/24/outline";
|
||||
import { PMFIcon, FeedbackIcon, UserCommentIcon } from "@formbricks/ui";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -21,13 +21,19 @@ const formTypes = [
|
||||
id: "feedback",
|
||||
name: "Feedback Box",
|
||||
description: "A direct channel to feel the pulse of your users.",
|
||||
icon: UserIcon,
|
||||
icon: FeedbackIcon,
|
||||
},
|
||||
{
|
||||
id: "custom",
|
||||
name: "Custom Form",
|
||||
description: "Send and analyze your custom form.",
|
||||
icon: UserIcon,
|
||||
name: "Custom Survey",
|
||||
description: "Create and analyze your custom survey.",
|
||||
icon: UserCommentIcon,
|
||||
},
|
||||
{
|
||||
id: "pmf",
|
||||
name: "Product-Market Fit Survey",
|
||||
description: "Leverage the Superhuman PMF engine.",
|
||||
icon: PMFIcon,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -44,7 +50,7 @@ export default function NewFormModal({ open, setOpen, workspaceId }: FormOnboard
|
||||
label,
|
||||
type: "feedback",
|
||||
schema: {
|
||||
type: "form",
|
||||
schemaVersion: 1,
|
||||
config: {},
|
||||
pages: [
|
||||
{
|
||||
@@ -90,6 +96,101 @@ export default function NewFormModal({ open, setOpen, workspaceId }: FormOnboard
|
||||
label,
|
||||
type: "custom",
|
||||
};
|
||||
} else if (formType === "pmf") {
|
||||
formTemplate = {
|
||||
label,
|
||||
type: "pmf",
|
||||
schema: {
|
||||
schemaVersion: 1,
|
||||
config: {},
|
||||
pages: [
|
||||
{
|
||||
id: "disappointmentPage",
|
||||
config: {
|
||||
autoSubmit: true,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
id: "disappointment",
|
||||
type: "radio",
|
||||
name: "disappointment",
|
||||
label: "How disappointed would you be if you could no longer use our service?",
|
||||
options: [
|
||||
{ label: "Very disappointed", value: "veryDisappointed" },
|
||||
{ label: "Somewhat disappointed", value: "somewhatDisappointed" },
|
||||
{ label: "Not disappointed", value: "notDisappointed" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "mainBenefitPage",
|
||||
elements: [
|
||||
{
|
||||
id: "mainBenefit",
|
||||
type: "text",
|
||||
name: "mainBenefit",
|
||||
label: "What is the main benefit you receive from our service?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "userSegmentPage",
|
||||
config: {
|
||||
autoSubmit: true,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
id: "userSegment",
|
||||
type: "radio",
|
||||
name: "userSegment",
|
||||
label: "What is your job title?",
|
||||
options: [
|
||||
{ label: "Founder", value: "founder" },
|
||||
{ label: "Executive", value: "executive" },
|
||||
{ label: "Product Manager", value: "productManager" },
|
||||
{ label: "Product Owner", value: "productOwner" },
|
||||
{ label: "Software Engineer", value: "softwareEngineer" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "improvementPage",
|
||||
elements: [
|
||||
{
|
||||
id: "improvement",
|
||||
type: "text",
|
||||
name: "improvement",
|
||||
label: "How can we improve our service for you?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "selfSegmentationPage",
|
||||
elements: [
|
||||
{
|
||||
id: "selfSegmentation",
|
||||
type: "text",
|
||||
name: "selfSegmentation",
|
||||
label: "What type of people would benefit most from using our service?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "thankYouPage",
|
||||
endScreen: true,
|
||||
elements: [
|
||||
{
|
||||
id: "thankYou",
|
||||
type: "html",
|
||||
name: "thankYou",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new Error("Unknown form type");
|
||||
}
|
||||
@@ -176,7 +277,7 @@ export default function NewFormModal({ open, setOpen, workspaceId }: FormOnboard
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className="mt-2 mr-3 flex text-sm sm:mt-0 sm:flex-col sm:text-right">
|
||||
<formType.icon className="h-6 w-6" />
|
||||
<formType.icon className="h-8 w-8" />
|
||||
</RadioGroup.Description>
|
||||
<span className="flex items-center">
|
||||
<span className="flex flex-col text-sm">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { InboxIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/router";
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import FilterNavigation from "../shared/FilterNavigation";
|
||||
import FeedbackTimeline from "./FeedbackTimeline";
|
||||
|
||||
const subCategories = [
|
||||
@@ -181,35 +182,7 @@ export default function FeedbackResults() {
|
||||
<div>
|
||||
<section aria-labelledby="products-heading" className="pt-6 pb-24">
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-10 lg:grid-cols-4">
|
||||
{/* Filters */}
|
||||
<form className="hidden lg:block">
|
||||
<h3 className="sr-only">Categories</h3>
|
||||
{navigation.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.name}
|
||||
onClick={() => setCurrentFilter(item.id)}
|
||||
className={clsx(
|
||||
item.id === currentFilter
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
|
||||
"group flex w-full items-center rounded-md px-3 py-2 text-sm font-medium"
|
||||
)}
|
||||
aria-current={item.id === currentFilter ? "page" : undefined}>
|
||||
<div className={clsx(item.color, "-ml-1 mr-3 h-2 w-2 flex-shrink-0 rounded-full")} />
|
||||
<span className="truncate">{item.name}</span>
|
||||
{item.count ? (
|
||||
<span
|
||||
className={clsx(
|
||||
item.id === currentFilter ? "bg-white" : "bg-gray-100 group-hover:bg-white",
|
||||
"ml-auto inline-block rounded-full py-0.5 px-3 text-xs"
|
||||
)}>
|
||||
{item.count}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
</form>
|
||||
<FilterNavigation submissions={submissions} setFilteredSubmissions={setFilteredSubmissions} />
|
||||
|
||||
{/* Product grid */}
|
||||
<div className="lg:col-span-3">
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import { useForm } from "@/lib/forms";
|
||||
import {
|
||||
BoltIcon,
|
||||
ChartPieIcon,
|
||||
InformationCircleIcon,
|
||||
RectangleStackIcon,
|
||||
ShareIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import PipelinesOverview from "../pipelines/PipelinesOverview";
|
||||
import PMFResults from "./PMFResults";
|
||||
import SegmentResults from "./SegmentResults";
|
||||
import SetupInstructions from "./SetupInstructions";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Results", icon: RectangleStackIcon },
|
||||
{ name: "Segments", icon: ChartPieIcon },
|
||||
{ name: "Data Pipelines", icon: ShareIcon },
|
||||
{ name: "Setup Instructions", icon: InformationCircleIcon },
|
||||
];
|
||||
|
||||
export default function PMFPage() {
|
||||
const router = useRouter();
|
||||
const [currentTab, setCurrentTab] = useState("Results");
|
||||
const { form, isLoadingForm, isErrorForm } = useForm(
|
||||
router.query.formId?.toString(),
|
||||
router.query.workspaceId?.toString()
|
||||
);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isErrorForm) {
|
||||
return <div>Error loading ressources. Maybe you don‘t have enough access rights.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<main className="mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="border-b border-gray-200 pt-8">
|
||||
<h1 className="pb-6 text-4xl font-bold tracking-tight text-gray-900">{form.label}</h1>
|
||||
<TabNavigation tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} />
|
||||
</div>
|
||||
{currentTab === "Results" ? (
|
||||
<PMFResults />
|
||||
) : currentTab === "Segments" ? (
|
||||
<SegmentResults />
|
||||
) : currentTab === "Data Pipelines" ? (
|
||||
<PipelinesOverview />
|
||||
) : currentTab === "Setup Instructions" ? (
|
||||
<SetupInstructions />
|
||||
) : null}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import EmptyPageFiller from "@/components/EmptyPageFiller";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useSubmissions } from "@/lib/submissions";
|
||||
import { InboxIcon } from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import FilterNavigation from "../shared/FilterNavigation";
|
||||
import PMFTimeline from "./PMFTimeline";
|
||||
|
||||
export default function PMFResults() {
|
||||
const router = useRouter();
|
||||
const { submissions, isLoadingSubmissions, isErrorSubmissions, mutateSubmissions } = useSubmissions(
|
||||
router.query.workspaceId?.toString(),
|
||||
router.query.formId?.toString()
|
||||
);
|
||||
|
||||
const [filteredSubmissions, setFilteredSubmissions] = useState([]);
|
||||
|
||||
if (isLoadingSubmissions) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isErrorSubmissions) {
|
||||
return <div>Error loading ressources. Maybe you don‘t have enough access rights</div>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<section aria-labelledby="filters" className="pt-6 pb-24">
|
||||
<div className="grid grid-cols-1 gap-x-16 gap-y-10 lg:grid-cols-4">
|
||||
<FilterNavigation submissions={submissions} setFilteredSubmissions={setFilteredSubmissions} />
|
||||
|
||||
{/* Submission grid */}
|
||||
|
||||
<div className="max-w-3xl md:col-span-3">
|
||||
{submissions.length === 0 ? (
|
||||
<EmptyPageFiller
|
||||
alertText="You haven't received any submissions yet."
|
||||
hintText="Embed the PMF survey on your website to start gathering insights."
|
||||
borderStyles="border-4 border-dotted border-red">
|
||||
<InboxIcon className="stroke-thin mx-auto h-24 w-24 text-slate-300" />
|
||||
</EmptyPageFiller>
|
||||
) : (
|
||||
<PMFTimeline submissions={filteredSubmissions} setSubmissions={setFilteredSubmissions} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import EmptyPageFiller from "@/components/EmptyPageFiller";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useForm } from "@/lib/forms";
|
||||
import { MergeWithSchema, persistSubmission, useSubmissions } from "@/lib/submissions";
|
||||
import { convertDateTimeString, parseUserAgent } from "@/lib/utils";
|
||||
import { Button, NotDisappointedIcon, SomewhatDisappointedIcon, VeryDisappointedIcon } from "@formbricks/ui";
|
||||
import { InboxIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export default function PMFTimeline({ submissions, setSubmissions }) {
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
submissions: allSubmissions,
|
||||
mutateSubmissions,
|
||||
isLoadingSubmissions,
|
||||
isErrorSubmissions,
|
||||
} = useSubmissions(router.query.workspaceId?.toString(), router.query.formId?.toString());
|
||||
|
||||
const { form, isLoadingForm, isErrorForm } = useForm(
|
||||
router.query.formId?.toString(),
|
||||
router.query.workspaceId?.toString()
|
||||
);
|
||||
|
||||
const toggleArchiveSubmission = (submission) => {
|
||||
const updatedSubmission = JSON.parse(JSON.stringify(submission));
|
||||
updatedSubmission.archived = !updatedSubmission.archived;
|
||||
// save submission without customer
|
||||
const submissionWoCustomer = { ...updatedSubmission };
|
||||
delete submissionWoCustomer.customer;
|
||||
persistSubmission(submissionWoCustomer, router.query.workspaceId?.toString());
|
||||
// update all submissions
|
||||
const submissionIdx = allSubmissions.findIndex((s) => s.id === submission.id);
|
||||
const updatedSubmissions = JSON.parse(JSON.stringify(allSubmissions));
|
||||
updatedSubmissions[submissionIdx] = updatedSubmission;
|
||||
mutateSubmissions(updatedSubmissions, false);
|
||||
if (updatedSubmission.archived) {
|
||||
toast.success("Submission archived");
|
||||
} else {
|
||||
toast.success("Submission restored");
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoadingForm || isLoadingSubmissions) return <LoadingSpinner />;
|
||||
|
||||
if (isErrorForm || isErrorSubmissions) {
|
||||
return <div>Error loading ressources. Maybe you don‘t have enough access rights</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
<ul role="list" className="-mb-8">
|
||||
{submissions.length === 0 ? (
|
||||
<EmptyPageFiller
|
||||
alertText="There are no submission matching this filter."
|
||||
hintText="Try changing the filter or wait for new submissions."
|
||||
borderStyles="border-4 border-dotted border-red">
|
||||
<InboxIcon className="stroke-thin mx-auto h-24 w-24 text-slate-300" />
|
||||
</EmptyPageFiller>
|
||||
) : (
|
||||
<>
|
||||
{submissions.map((submission, submissionIdx) => (
|
||||
<li key={submission.id}>
|
||||
<div className="relative pb-8">
|
||||
{submissionIdx !== submissions.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex space-x-3">
|
||||
<div>
|
||||
<span
|
||||
className={clsx(
|
||||
"bg-white",
|
||||
"flex h-8 w-8 items-center justify-center rounded-full ring-8 ring-gray-50"
|
||||
)}>
|
||||
{submission.data.disappointment === "veryDisappointed" ? (
|
||||
<VeryDisappointedIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
) : submission.data.disappointment === "notDisappointed" ? (
|
||||
<NotDisappointedIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
) : submission.data.disappointment === "somewhatDisappointed" ? (
|
||||
<SomewhatDisappointedIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="flex w-full justify-between">
|
||||
{submission.data.disappointment === "veryDisappointed" ? (
|
||||
<span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">
|
||||
Very disappointed
|
||||
</span>
|
||||
) : submission.data.disappointment === "notDisappointed" ? (
|
||||
<span className="inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800">
|
||||
Not disappointed
|
||||
</span>
|
||||
) : submission.data.disappointment === "somewhatDisappointed" ? (
|
||||
<span className="inline-flex items-center rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-medium text-yellow-800">
|
||||
Somewhat disappointed
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<div className="text-sm text-gray-400">
|
||||
<time dateTime={convertDateTimeString(submission.createdAt)}>
|
||||
{convertDateTimeString(submission.createdAt)}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<ul className="whitespace-pre-wrap text-sm text-gray-500">
|
||||
{Object.entries(MergeWithSchema(submission.data, form.schema)).map(
|
||||
([key, value]) => (
|
||||
<li key={key} className="py-5">
|
||||
<p className="text-sm font-semibold text-gray-800">{key}</p>
|
||||
<p
|
||||
className={clsx(
|
||||
value ? "text-gray-600" : "text-gray-400",
|
||||
"whitespace-pre-line pt-1 text-sm text-gray-600"
|
||||
)}>
|
||||
{value.toString()}
|
||||
</p>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" bg-gray-50 p-4 sm:p-6">
|
||||
<div className="flex w-full justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-thin text-gray-500">User</p>
|
||||
{submission.customerEmail ? (
|
||||
<Link
|
||||
className="text-sm font-medium text-gray-700"
|
||||
href={`/workspaces/${router.query.workspaceId}/customers/${submission.customerEmail}`}>
|
||||
{submission.customerEmail}
|
||||
</Link>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Anonymous</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-thin text-gray-500">Device</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{parseUserAgent(submission.meta.userAgent)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-thin text-gray-500">Page</p>
|
||||
<p className="text-sm text-gray-500">{submission.data.pageUrl}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex w-full justify-end">
|
||||
{!submission.archived ? (
|
||||
<button
|
||||
className="text-base text-gray-500 underline"
|
||||
onClick={() => toggleArchiveSubmission(submission)}>
|
||||
Archive
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="text-base text-gray-500 underline"
|
||||
onClick={() => toggleArchiveSubmission(submission)}>
|
||||
Restore
|
||||
</button>
|
||||
)}
|
||||
{submission.customerEmail && (
|
||||
<Button
|
||||
variant="primary"
|
||||
href={`mailto:${submission.customerEmail}`}
|
||||
className="ml-4">
|
||||
Send Email
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useForm } from "@/lib/forms";
|
||||
import { getOptionLabelMap, useSubmissions } from "@/lib/submissions";
|
||||
import { Pie } from "@formbricks/charts";
|
||||
import { NotDisappointedIcon, SomewhatDisappointedIcon, VeryDisappointedIcon } from "@formbricks/ui";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo, useState } from "react";
|
||||
import FilterNavigation from "../shared/FilterNavigation";
|
||||
|
||||
export default function SegmentResults() {
|
||||
const router = useRouter();
|
||||
const { form, isLoadingForm, isErrorForm } = useForm(
|
||||
router.query.formId?.toString(),
|
||||
router.query.workspaceId?.toString()
|
||||
);
|
||||
const { submissions, isLoadingSubmissions, isErrorSubmissions, mutateSubmissions } = useSubmissions(
|
||||
router.query.workspaceId?.toString(),
|
||||
router.query.formId?.toString()
|
||||
);
|
||||
const [filteredSubmissions, setFilteredSubmissions] = useState([]);
|
||||
|
||||
const labelMap = useMemo(() => {
|
||||
if (form) {
|
||||
return getOptionLabelMap(form.schema);
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
if (isLoadingSubmissions || isLoadingForm) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isErrorSubmissions || isErrorForm) {
|
||||
return <div>Error loading ressources. Maybe you don‘t have enough access rights</div>;
|
||||
}
|
||||
|
||||
const questions = [
|
||||
{
|
||||
label: "What is the main benefit you receive from our service?",
|
||||
name: "mainBenefit",
|
||||
},
|
||||
{
|
||||
label: "How can we improve our service for you?",
|
||||
name: "improvement",
|
||||
},
|
||||
{
|
||||
label: "What type of people would benefit most from using our service?",
|
||||
name: "selfSegmentation",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<section aria-labelledby="filters" className="pt-6 pb-24">
|
||||
<div className="grid grid-cols-1 gap-x-16 gap-y-10 lg:grid-cols-4">
|
||||
<FilterNavigation submissions={submissions} setFilteredSubmissions={setFilteredSubmissions} />
|
||||
|
||||
{/* Submission grid */}
|
||||
|
||||
<div className="max-w-7xl lg:col-span-3">
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||
<div className="flex flex-col items-center justify-center rounded-lg bg-white p-2">
|
||||
<h3 className="text-sm font-medium text-slate-800">Overall</h3>
|
||||
<Pie submissions={submissions} schema={form.schema} fieldName={"disappointment"} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center rounded-lg bg-white p-2">
|
||||
<h3 className="text-sm font-medium text-slate-800">Selected Segment</h3>
|
||||
<Pie submissions={filteredSubmissions} schema={form.schema} fieldName={"disappointment"} />
|
||||
</div>
|
||||
</div>
|
||||
{questions.map((question) => (
|
||||
<div key={question.name} className="my-4 rounded-lg bg-white">
|
||||
<div className="rounded-t-lg bg-slate-100 p-4 text-lg font-bold text-slate-800">
|
||||
{question.label}
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2 border-t border-slate-200 bg-slate-100 px-4 py-2 text-sm font-semibold text-slate-500">
|
||||
<div className="col-span-3">Response</div>
|
||||
<div>Feeling</div>
|
||||
<div>Segment</div>
|
||||
</div>
|
||||
{filteredSubmissions
|
||||
.filter((s) => question.name in s.data)
|
||||
.map((submission) => (
|
||||
<div
|
||||
key={submission.id}
|
||||
className="grid grid-cols-5 gap-2 border-t border-slate-100 px-4 pt-2 pb-4 text-sm">
|
||||
<div className="col-span-3">{submission.data[question.name]}</div>
|
||||
<div>
|
||||
{submission.data.disappointment === "veryDisappointed" ? (
|
||||
<VeryDisappointedIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
) : submission.data.disappointment === "notDisappointed" ? (
|
||||
<NotDisappointedIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
) : submission.data.disappointment === "somewhatDisappointed" ? (
|
||||
<SomewhatDisappointedIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<div className="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-0.5 text-xs text-slate-600">
|
||||
{labelMap[submission.data.userSegment]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Button } from "@formbricks/ui";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import Prism from "prismjs";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export default function SetupInstructions({}) {
|
||||
const router = useRouter();
|
||||
const formId = router.query.formId.toString();
|
||||
|
||||
useEffect(() => {
|
||||
Prism.highlightAll();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-5 gap-8 py-4">
|
||||
<div className="col-span-3 text-sm text-gray-600">
|
||||
<h3 className="block text-lg font-semibold text-slate-800">How to get started</h3>
|
||||
<ol className="mt-2 list-decimal pl-4 leading-8 text-slate-700">
|
||||
<li>
|
||||
Copy the Javascript widget below into your application and customize the config according to
|
||||
your needs.
|
||||
</li>
|
||||
<li>
|
||||
Choose a HTML element which you want to replace with the PMF survey. Set a unique ID for this
|
||||
element and configure the script accordingly.
|
||||
</li>
|
||||
<li>You are ready to receive your first submission and view it in the Results tab.</li>
|
||||
<li>Get notified or pipe submission data to a different tool in the Data Pipelines tab.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div>
|
||||
<label htmlFor="formId" className="block text-lg font-semibold 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={formId}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-2 w-full justify-center"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(formId);
|
||||
toast("Copied form ID to clipboard");
|
||||
}}>
|
||||
copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-4 text-gray-800" />
|
||||
<div className="py-6">
|
||||
<label htmlFor="formId" className="block text-lg font-semibold text-slate-800">
|
||||
Javascript Snippet
|
||||
</label>
|
||||
<p>
|
||||
Place this Javascript script tags into the head of your HTML file and include the HTML element to
|
||||
hold the survey into the body to start using the Formbricks PMF survey.
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<div className="col-span-3 rounded-md bg-black p-4 text-sm font-light text-gray-200">
|
||||
<pre>
|
||||
<code className="language-html whitespace-pre-wrap">
|
||||
{`<!-- HTML header script -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@formbricks/pmf@0.1.0/dist/index.umd.js" defer></script>
|
||||
|
||||
<script>
|
||||
window.formbricks = {
|
||||
...window.formbricks,
|
||||
config: {
|
||||
formbricksUrl: "${window.location.protocol}//${window.location.host}",
|
||||
formId: "${formId}",
|
||||
containerId: "formbricks-container",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Element to hold the survey -->
|
||||
<div id="formbricks-container"></div>`}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
"use client";
|
||||
|
||||
import EmptyPageFiller from "@/components/EmptyPageFiller";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useSubmissions } from "@/lib/submissions";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ComplimentIcon,
|
||||
IdeaIcon,
|
||||
VeryDisappointedIcon,
|
||||
SomewhatDisappointedIcon,
|
||||
NotDisappointedIcon,
|
||||
} from "@formbricks/ui";
|
||||
import Image from "next/image";
|
||||
import PMFThumb from "@/images/pmfthumb.webp";
|
||||
import PMFThumb2 from "@/images/pmfthumb-2.webp";
|
||||
import Link from "next/link";
|
||||
import { InboxIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/router";
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import FeedbackTimeline from "./PMFTimeline";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import sq from "date-fns/esm/locale/sq/index.js";
|
||||
|
||||
const subCategories = [
|
||||
{ name: "Somewhat disappointed", href: "#" },
|
||||
{ name: "Very disappointed", href: "#" },
|
||||
{ name: "Not disappointed", href: "#" },
|
||||
];
|
||||
|
||||
export default function SegmentResults() {
|
||||
const router = useRouter();
|
||||
const { submissions, isLoadingSubmissions, isErrorSubmissions, mutateSubmissions } = useSubmissions(
|
||||
router.query.workspaceId?.toString(),
|
||||
router.query.formId?.toString()
|
||||
);
|
||||
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
const [filteredSubmissions, setFilteredSubmissions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!submissions) return;
|
||||
let newSubmissions = [];
|
||||
if (currentFilter === "all") {
|
||||
newSubmissions = submissions.filter((submission) => !submission.archived);
|
||||
} else if (currentFilter === "archive") {
|
||||
newSubmissions = submissions.filter((submission) => submission.archived);
|
||||
} else {
|
||||
newSubmissions = submissions.filter((submission) => submission.data.feedbackType === currentFilter);
|
||||
}
|
||||
setFilteredSubmissions(newSubmissions);
|
||||
}, [currentFilter, submissions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingSubmissions) {
|
||||
setFilteredSubmissions(submissions.filter((submission) => !submission.archived));
|
||||
}
|
||||
}, [isLoadingSubmissions, submissions]);
|
||||
|
||||
const completed = [
|
||||
{
|
||||
segment: "Founder",
|
||||
},
|
||||
{
|
||||
segment: "Entrepreneur",
|
||||
},
|
||||
{
|
||||
segment: "Product Manager",
|
||||
},
|
||||
{
|
||||
segment: "Engineer",
|
||||
},
|
||||
];
|
||||
|
||||
const submissionz = [
|
||||
{
|
||||
question: "What is the main benefit you receive from our service?",
|
||||
},
|
||||
{
|
||||
question: "How can we improve our service for you?",
|
||||
},
|
||||
{
|
||||
question: "What type of people would benefit most from using our service?",
|
||||
},
|
||||
];
|
||||
|
||||
const q1responses = [
|
||||
{
|
||||
response:
|
||||
"A think it would be awesome if your app could do this because I keep having this problem! I would use it everyday and tell all my friends.",
|
||||
feeling: "very disapp.",
|
||||
segment: "Founder",
|
||||
},
|
||||
{
|
||||
response:
|
||||
"B think it would be awesome if your app could do this because I keep having this problem! I would use it everyday and tell all my friends.",
|
||||
feeling: "very disapp.",
|
||||
segment: "Entrepreneur",
|
||||
},
|
||||
{
|
||||
response:
|
||||
"C think it would be awesome if your app could do this because I keep having this problem! I would use it everyday and tell all my friends.",
|
||||
feeling: "very disapp.",
|
||||
segment: "Product Manager",
|
||||
},
|
||||
];
|
||||
|
||||
const q2responses = [
|
||||
{
|
||||
response:
|
||||
"A think it would be awesome if your app could do this because I keep having this problem! I would use it everyday and tell all my friends.",
|
||||
feeling: "somewhat disapp.",
|
||||
segment: "Founder",
|
||||
},
|
||||
{
|
||||
response:
|
||||
"B think it would be awesome if your app could do this because I keep having this problem! I would use it everyday and tell all my friends.",
|
||||
feeling: "somewhat disapp.",
|
||||
segment: "Entrepreneur",
|
||||
},
|
||||
{
|
||||
response:
|
||||
"C think it would be awesome if your app could do this because I keep having this problem! I would use it everyday and tell all my friends.",
|
||||
feeling: "somewhat disapp.",
|
||||
segment: "Product Manager",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Mobile filter dialog */}
|
||||
{/* <Transition.Root show={mobileFiltersOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-40 lg:hidden" onClose={setMobileFiltersOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-40 flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full">
|
||||
<Dialog.Panel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-auto bg-white py-4 pb-12 shadow-xl">
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<h2 className="text-lg font-medium text-gray-900">Filters</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="-mr-2 flex h-10 w-10 items-center justify-center rounded-md bg-white p-2 text-gray-400"
|
||||
onClick={() => setMobileFiltersOpen(false)}>
|
||||
<span className="sr-only">Close menu</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters
|
||||
<form className="mt-4 border-t border-gray-200">
|
||||
<h3 className="sr-only">Categories</h3>
|
||||
<ul role="list" className="px-2 py-3 font-medium text-gray-900">
|
||||
{subCategories.map((category) => (
|
||||
<li key={category.name}>
|
||||
<a href={category.href} className="block px-2 py-3">
|
||||
{category.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
\
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root> */}
|
||||
<div>
|
||||
<section aria-labelledby="filters" className="pt-6 pb-24">
|
||||
<div className="grid grid-cols-1 gap-x-16 gap-y-10 lg:grid-cols-4">
|
||||
<div>
|
||||
{/* Segments */}
|
||||
|
||||
<form className="mb-4 hidden lg:block">
|
||||
<h3 className="sr-only">Segment</h3>
|
||||
<div className="flex py-2 text-sm font-bold">
|
||||
<h4 className="text-slate-600">Segment</h4>
|
||||
</div>
|
||||
{completed.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.segment}
|
||||
className={clsx(
|
||||
item.segment === currentFilter
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
|
||||
"group flex w-full items-center rounded-md px-3 py-2 text-sm font-medium"
|
||||
)}>
|
||||
<div className="-ml-1 mr-3 h-2 w-2 flex-shrink-0 rounded-full" />
|
||||
<span className="truncate">{item.segment}</span>
|
||||
</button>
|
||||
))}
|
||||
</form>
|
||||
<div className="mb-2 flex py-2 text-sm font-bold">
|
||||
<h4 className="text-slate-600">Tutorials</h4>
|
||||
</div>
|
||||
<Link
|
||||
href="https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit"
|
||||
target={"_blank"}>
|
||||
<div className="mb-4 rounded-md bg-white shadow-sm transition-all duration-100 ease-in-out hover:scale-105">
|
||||
<Image src={PMFThumb} className="rounded-t-md" alt={"PMF Article Thumb 1"} />
|
||||
|
||||
<div className="p-4">
|
||||
<p className="font-bold text-slate-600">
|
||||
Superhuman built an engine to find Product-Market Fit
|
||||
</p>
|
||||
<p className="text-brand-dark text-sm">firstround.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="https://coda.io/@rahulvohra/superhuman-product-market-fit-engine" target={"_blank"}>
|
||||
<div className="mb-4 rounded-md bg-white shadow-sm transition-all duration-100 ease-in-out hover:scale-105">
|
||||
<Image src={PMFThumb2} className="rounded-t-md" alt={"PMF Article Thumb 2"} />
|
||||
<div className="p-4">
|
||||
<p className="font-bold text-slate-600">The Superhuman Product/Market Fit Engine</p>
|
||||
<p className="text-brand-dark text-sm">coda.io</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Double down on what they love*/}
|
||||
|
||||
<div className="max-w-3xl lg:col-span-3">
|
||||
<div className="flex w-full space-x-3">
|
||||
<div className="flex h-12 w-1/2 items-center justify-center rounded-lg bg-white">
|
||||
overall results
|
||||
</div>
|
||||
<div className="flex h-12 w-1/2 items-center justify-center rounded-lg bg-white">
|
||||
max. very d. segment
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mt-10 mb-4 text-2xl font-bold text-slate-500">Double down on what they love</h2>
|
||||
<div className="my-4 rounded-lg bg-white">
|
||||
<div className="rounded-t-lg bg-slate-100 p-4 text-lg font-bold text-slate-800">
|
||||
What is the main benefit you receive from our service?
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2 bg-slate-100 px-4 pb-2 text-sm font-semibold text-slate-500">
|
||||
<div className="col-span-3">Response</div>
|
||||
<div>Feeling</div>
|
||||
<div>Segment</div>
|
||||
</div>
|
||||
{q1responses.map((r) => (
|
||||
<div className="grid grid-cols-5 gap-2 px-4 pt-2 pb-4">
|
||||
<div className="col-span-3">{r.response}</div>
|
||||
<div>
|
||||
<div
|
||||
className={clsx(
|
||||
// base styles independent what type of button it is
|
||||
"inline-grid rounded-full px-2 text-xs",
|
||||
// different styles depending on size
|
||||
r.feeling === "very disapp." && "bg-green-100 text-green-700 ",
|
||||
r.feeling === "somewhat disapp." && "bg-orange-100 text-orange-500 ",
|
||||
r.feeling === "not disapp." && "bg-red-100 text-red-500"
|
||||
)}>
|
||||
{r.feeling}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="inline-grid rounded-full bg-slate-100 px-2 text-xs text-slate-600">
|
||||
{r.segment}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="mt-10 mb-4 text-2xl font-bold text-slate-500">Fix what’s holding them back</h2>
|
||||
<div className="my-4 rounded-lg bg-white">
|
||||
<div className="rounded-t-lg bg-slate-100 p-4 text-lg font-bold text-slate-800">
|
||||
How can we improve our service for you?
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2 bg-slate-100 px-4 pb-2 text-sm font-semibold text-slate-500">
|
||||
<div className="col-span-3">Response</div>
|
||||
<div>Feeling</div>
|
||||
<div>Segment</div>
|
||||
</div>
|
||||
{q2responses.map((r) => (
|
||||
<div className="grid grid-cols-5 gap-2 px-4 pt-2 pb-4">
|
||||
<div className="col-span-3">{r.response}</div>
|
||||
<div>
|
||||
<div
|
||||
className={clsx(
|
||||
// base styles independent what type of button it is
|
||||
"inline-grid rounded-full px-2 text-xs",
|
||||
// different styles depending on size
|
||||
r.feeling === "very disapp." && "bg-green-100 text-green-700 ",
|
||||
r.feeling === "somewhat disapp." && "bg-orange-100 text-orange-500 ",
|
||||
r.feeling === "not disapp." && "bg-red-100 text-red-500"
|
||||
)}>
|
||||
{r.feeling}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="inline-grid rounded-full bg-slate-100 px-2 text-xs text-slate-600">
|
||||
{r.segment}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useForm } from "@/lib/forms";
|
||||
import { camelToTitle } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Filter {
|
||||
name: string;
|
||||
label: string;
|
||||
type: string;
|
||||
options: {
|
||||
value: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function FilterNavigation({ submissions, setFilteredSubmissions }) {
|
||||
const router = useRouter();
|
||||
const { formId, workspaceId } = router.query;
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
|
||||
const { form, isLoadingForm, isErrorForm } = useForm(formId?.toString(), workspaceId?.toString());
|
||||
|
||||
// filter submissions based on selected filters
|
||||
useEffect(() => {
|
||||
if (form) {
|
||||
let newFilteredSubmissions = JSON.parse(JSON.stringify(submissions));
|
||||
for (const filter of filters) {
|
||||
const isAllActive = filter.options.find((option) => option.value === "all")?.active;
|
||||
// no filter is all is selected, if not keep on filtering
|
||||
if (!isAllActive) {
|
||||
// special routine for archive
|
||||
if (filter.type === "archive") {
|
||||
newFilteredSubmissions = newFilteredSubmissions.filter((submission) => submission.archived);
|
||||
continue;
|
||||
}
|
||||
// filter for all other types
|
||||
for (const option of filter.options) {
|
||||
if (option.active) {
|
||||
newFilteredSubmissions = newFilteredSubmissions.filter((submission) => {
|
||||
return submission.data[filter.name] === option.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setFilteredSubmissions(newFilteredSubmissions);
|
||||
}
|
||||
}, [filters, form, submissions, setFilteredSubmissions]);
|
||||
|
||||
const chosseOptionFilter = (filterName, optionValue) => {
|
||||
const newFilters = [...filters];
|
||||
const filter = newFilters.find((filter) => filter.name === filterName);
|
||||
|
||||
if (filter) {
|
||||
// reset all previous filter options
|
||||
for (const option of filter.options) {
|
||||
if (option.value === optionValue) {
|
||||
option.active = true;
|
||||
} else {
|
||||
option.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
setFilters(newFilters);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (form && form.schema) {
|
||||
const filters = [];
|
||||
for (const page of form.schema.pages) {
|
||||
for (const element of page.elements) {
|
||||
if (element.type === "radio") {
|
||||
filters.push({
|
||||
name: element.name,
|
||||
label: element.label,
|
||||
type: element.type,
|
||||
options: [{ value: "all", label: "All", active: true }].concat([
|
||||
...element.options.map((option) => ({ ...option, active: false })),
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// add archived filter at the end
|
||||
filters.push({
|
||||
name: "archived",
|
||||
label: "Archived",
|
||||
type: "archive",
|
||||
options: [
|
||||
{ value: "all", label: "All", active: true },
|
||||
{ value: "archived", label: "Archived", active: false },
|
||||
],
|
||||
});
|
||||
setFilters(filters);
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorForm) {
|
||||
return <div>Error loading ressources. Maybe you don‘t have enough access rights</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{filters.map((filter) => (
|
||||
<div key={filter.name}>
|
||||
<div className="flex py-2 text-sm font-bold">
|
||||
<h4 className="text-slate-600">{camelToTitle(filter.name)}</h4>
|
||||
</div>
|
||||
{filter.options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
chosseOptionFilter(filter.name, option.value);
|
||||
}}
|
||||
className={clsx(
|
||||
option.active
|
||||
? "bg-gray-200 text-gray-900"
|
||||
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
|
||||
"group flex w-full items-center rounded-md px-3 py-2 text-sm font-medium"
|
||||
)}
|
||||
aria-current={option.active ? "page" : undefined}>
|
||||
<div className={clsx("-ml-1 mr-3 h-2 w-2 flex-shrink-0 rounded-full")} />
|
||||
<span className="truncate">{option.label}</span>
|
||||
{/* {item.count ? (
|
||||
<span
|
||||
className={clsx(
|
||||
item.id === currentFilter ? "bg-white" : "bg-gray-100 group-hover:bg-white",
|
||||
"ml-auto inline-block rounded-full py-0.5 px-3 text-xs"
|
||||
)}>
|
||||
{item.count}
|
||||
</span>
|
||||
) : null} */}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -37,9 +37,6 @@ export default function SummaryPage() {
|
||||
return <div>Error loading ressources. Maybe you don‘t have enough access rights</div>;
|
||||
}
|
||||
|
||||
{
|
||||
console.log(JSON.stringify(submissions, null, 2));
|
||||
}
|
||||
return (
|
||||
<div className="mx-auto py-8 sm:px-6 lg:px-8">
|
||||
<header className="mb-8">
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function LayoutWrapperWorkspace({ children }) {
|
||||
<>
|
||||
<div className="flex h-full">
|
||||
{/* Narrow sidebar */}
|
||||
<div className="hidden overflow-y-auto border-r border-gray-200 bg-white bg-gradient-to-r md:block md:w-64">
|
||||
<div className="hidden overflow-y-auto border-r border-gray-200 bg-white bg-gradient-to-r sm:w-40 md:block xl:w-64">
|
||||
<div className="flex w-full flex-col items-center py-6">
|
||||
<div className="w-full flex-1 space-y-2 px-2">
|
||||
{sidebarNavigation.map((item) => (
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -85,7 +85,7 @@ export const MergeWithSchema = (submissionData, schema) => {
|
||||
return mergedData;
|
||||
};
|
||||
|
||||
const getOptionLabelMap = (schema) => {
|
||||
export const getOptionLabelMap = (schema) => {
|
||||
const optionLabelMap = {};
|
||||
for (const page of schema.pages) {
|
||||
for (const elem of page.elements) {
|
||||
|
||||
@@ -96,3 +96,13 @@ export const parseUserAgent = (userAgent: string) => {
|
||||
export const capitalizeFirstLetter = (string) => {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
};
|
||||
|
||||
// camel case to title case
|
||||
export const camelToTitle = (string) => {
|
||||
return string
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.replace(/^./, function (str) {
|
||||
return str.toUpperCase();
|
||||
})
|
||||
.trim();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import PMFPage from "@/components/forms/pmf/PMFPage";
|
||||
import LayoutApp from "@/components/layout/LayoutApp";
|
||||
import LayoutWrapperWorkspace from "@/components/layout/LayoutWrapperWorkspace";
|
||||
|
||||
export default function WorkspaceFormsPage({}) {
|
||||
return (
|
||||
<LayoutApp>
|
||||
<LayoutWrapperWorkspace>
|
||||
<PMFPage />
|
||||
</LayoutWrapperWorkspace>
|
||||
</LayoutApp>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useMemo } from "react";
|
||||
import { Pie, PieChart } from "recharts";
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
submissions: any;
|
||||
schema: any;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
export function FbPie({ 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 = {};
|
||||
let schemaElem;
|
||||
for (const pages of schema.pages) {
|
||||
for (const elem of pages.elements) {
|
||||
if (elem.name === fieldName) {
|
||||
schemaElem = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof schemaElem === "undefined") {
|
||||
throw Error("key not found in schema");
|
||||
}
|
||||
if (!("options" in schemaElem)) {
|
||||
throw Error(`No options found for element "${schemaElem.name}"`);
|
||||
}
|
||||
for (const option of schemaElem.options) {
|
||||
dataDict[option.value] = { name: option.label, value: 0 };
|
||||
}
|
||||
for (const submission of submissions) {
|
||||
if (fieldName in submission.data) {
|
||||
// if submission value is array (checkboxes)
|
||||
if (Array.isArray(submission.data[fieldName])) {
|
||||
for (const value of submission.data[fieldName]) {
|
||||
if (value in dataDict) {
|
||||
dataDict[value] = {
|
||||
...dataDict[value],
|
||||
value: dataDict[value].value + 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// if submission value is string (radio buttons)
|
||||
else if (typeof submission.data[fieldName] == "string") {
|
||||
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 (
|
||||
<>
|
||||
<PieChart width={400} height={250}>
|
||||
{/* <Pie dataKey="value" fill={color || "#00C4B8"} /> */}
|
||||
<Pie
|
||||
data={data}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
outerRadius={50}
|
||||
fill={color || "#00C4B8"}
|
||||
label={({ cx, cy, midAngle, innerRadius, outerRadius, value, index }) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
const radius = 25 + innerRadius + (outerRadius - innerRadius);
|
||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fill={color || "#00C4B8"}
|
||||
fontSize={12}
|
||||
textAnchor={x > cx ? "start" : "end"}
|
||||
dominantBaseline="central">
|
||||
{/*
|
||||
// @ts-ignore */}
|
||||
{data[index].name} ({value})
|
||||
</text>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { FbPie as Pie };
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./charts/Bar";
|
||||
export * from "./charts/Nps";
|
||||
export * from "./charts/Pie";
|
||||
export * from "./charts/Table";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "FormType" ADD VALUE 'pmf';
|
||||
@@ -51,6 +51,7 @@ model Customer {
|
||||
enum FormType {
|
||||
custom
|
||||
feedback
|
||||
pmf
|
||||
}
|
||||
|
||||
model Form {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const formHtml = `<div id="formbricks-content"><div id="formbricks-form"><div class="formbricks-element" id="formbricks-question-0"><p class="formbricks-element-label">How disappointed would you be if you could no longer use our service?</p><fieldset class="formbricks-radio-fieldset"><legend class="formbricks-sr-only">Disappointment options</legend><div class="formbricks-radio-options"><div class="formbricks-radio-option" data-element-name="disappointment" data-element-value="veryDisappointed"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">very disappointed</label></div><div class="formbricks-radio-option" data-element-name="disappointment" data-element-value="somehowDisappointed"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">somewhat disappointed</label></div><div class="formbricks-radio-option" data-element-name="disappointment" data-element-value="notDisappointed"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">not disappointed</label></div></div></fieldset></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-1"><p class="formbricks-element-label">What is the main benefit you receive from our service?</p><form class="formbricks-form" data-element-name="benefit"><textarea rows="4" name="benefit" class="formbricks-textarea" required></textarea><div class="formbricks-next-button-wrapper"><button type="submit" class="formbricks-next-button">Next</button></div></form></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-2"><p class="formbricks-element-label">What is your job title?</p><fieldset class="formbricks-radio-fieldset"><legend class="formbricks-sr-only">Job title options</legend><div class="formbricks-radio-options"><div class="formbricks-radio-option" data-element-name="jobTitle" data-element-value="founder"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Founder</label></div><div class="formbricks-radio-option" data-element-name="jobTitle" data-element-value="executive"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Executive</label></div><div class="formbricks-radio-option" data-element-name="jobTitle" data-element-value="productManager"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Product Manager</label></div><div class="formbricks-radio-option" data-element-name="jobTitle" data-element-value="productOwner"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Product Owner</label></div><div class="formbricks-radio-option" data-element-name="jobTitle" data-element-value="softwareEngineer"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Software Engineer</label></div></div></fieldset></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-3"><p class="formbricks-element-label">How can we improve our service for you?</p><form class="formbricks-form" data-element-name="howImprove"><textarea rows="4" name="howImprove" class="formbricks-textarea" required></textarea><div class="formbricks-next-button-wrapper"><button type="submit" class="formbricks-next-button">Next</button></div></form></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-4"><p class="formbricks-element-label">What type of people would benefit most from using our service?</p><form class="formbricks-form" data-element-name="idealCustomer"><textarea rows="4" name="idealCustomer" class="formbricks-textarea" required></textarea><div class="formbricks-next-button-wrapper"><button type="submit" class="formbricks-next-button">Submit</button></div></form></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-5"><p class="formbricks-thankyou-icon">❤️</p><p class="formbricks-thankyou-title">Thank you!</p><p class="formbricks-thankyou-subtitle">Your feedback helps us improve our service for you.</p></div></div></div><div id="formbricks-progressbar-wrapper"><div class="formbricks-progressbar"><div class="formbricks-progressbar-item formbricks-progressbar-item-current"></div><div class="formbricks-progressbar-item"></div><div class="formbricks-progressbar-item"></div><div class="formbricks-progressbar-item"></div><div class="formbricks-progressbar-item"></div></div></div>`;
|
||||
export const formHtml = `<div id="formbricks-content"><div id="formbricks-form"><div class="formbricks-element" id="formbricks-question-0"><p class="formbricks-element-label">How disappointed would you be if you could no longer use our service?</p><fieldset class="formbricks-radio-fieldset"><legend class="formbricks-sr-only">Disappointment options</legend><div class="formbricks-radio-options"><div class="formbricks-radio-option" data-element-name="disappointment" data-element-value="veryDisappointed"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">very disappointed</label></div><div class="formbricks-radio-option" data-element-name="disappointment" data-element-value="somewhatDisappointed"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">somewhat disappointed</label></div><div class="formbricks-radio-option" data-element-name="disappointment" data-element-value="notDisappointed"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">not disappointed</label></div></div></fieldset></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-1"><p class="formbricks-element-label">What is the main benefit you receive from our service?</p><form class="formbricks-form" data-element-name="mainBenefit"><textarea rows="4" name="mainBenefit" class="formbricks-textarea" required></textarea><div class="formbricks-next-button-wrapper"><button type="submit" class="formbricks-next-button">Next</button></div></form></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-2"><p class="formbricks-element-label">What is your job title?</p><fieldset class="formbricks-radio-fieldset"><legend class="formbricks-sr-only">Job title options</legend><div class="formbricks-radio-options"><div class="formbricks-radio-option" data-element-name="userSegment" data-element-value="founder"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Founder</label></div><div class="formbricks-radio-option" data-element-name="userSegment" data-element-value="executive"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Executive</label></div><div class="formbricks-radio-option" data-element-name="userSegment" data-element-value="productManager"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Product Manager</label></div><div class="formbricks-radio-option" data-element-name="userSegment" data-element-value="productOwner"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Product Owner</label></div><div class="formbricks-radio-option" data-element-name="userSegment" data-element-value="softwareEngineer"><input name="notification-method" type="radio" class="formbricks-radio-input"><label class="formbricks-radio-label">Software Engineer</label></div></div></fieldset></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-3"><p class="formbricks-element-label">How can we improve our service for you?</p><form class="formbricks-form" data-element-name="improvement"><textarea rows="4" name="improvement" class="formbricks-textarea" required></textarea><div class="formbricks-next-button-wrapper"><button type="submit" class="formbricks-next-button">Next</button></div></form></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-4"><p class="formbricks-element-label">What type of people would benefit most from using our service?</p><form class="formbricks-form" data-element-name="selfSegmentation"><textarea rows="4" name="selfSegmentation" class="formbricks-textarea" required></textarea><div class="formbricks-next-button-wrapper"><button type="submit" class="formbricks-next-button">Submit</button></div></form></div><div class="formbricks-element formbricks-hidden" id="formbricks-question-5"><p class="formbricks-thankyou-icon">❤️</p><p class="formbricks-thankyou-title">Thank you!</p><p class="formbricks-thankyou-subtitle">Your feedback helps us improve our service for you.</p></div></div></div><div id="formbricks-progressbar-wrapper"><div class="formbricks-progressbar"><div class="formbricks-progressbar-item formbricks-progressbar-item-current"></div><div class="formbricks-progressbar-item"></div><div class="formbricks-progressbar-item"></div><div class="formbricks-progressbar-item"></div><div class="formbricks-progressbar-item"></div></div></div>`;
|
||||
@@ -17,7 +17,7 @@
|
||||
<div
|
||||
class="formbricks-radio-option"
|
||||
data-element-name="disappointment"
|
||||
data-element-value="somehowDisappointed">
|
||||
data-element-value="somewhatDisappointed">
|
||||
<input name="notification-method" type="radio" class="formbricks-radio-input" />
|
||||
<label class="formbricks-radio-label">somewhat disappointed </label>
|
||||
</div>
|
||||
@@ -34,8 +34,8 @@
|
||||
|
||||
<div class="formbricks-element formbricks-hidden" id="formbricks-question-1">
|
||||
<p class="formbricks-element-label">What is the main benefit you receive from our service?</p>
|
||||
<form class="formbricks-form" data-element-name="benefit">
|
||||
<textarea rows="4" name="benefit" class="formbricks-textarea" required></textarea>
|
||||
<form class="formbricks-form" data-element-name="mainBenefit">
|
||||
<textarea rows="4" name="mainBenefit" class="formbricks-textarea" required></textarea>
|
||||
<div class="formbricks-next-button-wrapper">
|
||||
<button type="submit" class="formbricks-next-button">Next</button>
|
||||
</div>
|
||||
@@ -47,28 +47,31 @@
|
||||
<fieldset class="formbricks-radio-fieldset">
|
||||
<legend class="formbricks-sr-only">Job title options</legend>
|
||||
<div class="formbricks-radio-options">
|
||||
<div class="formbricks-radio-option" data-element-name="jobTitle" data-element-value="founder">
|
||||
<div class="formbricks-radio-option" data-element-name="userSegment" data-element-value="founder">
|
||||
<input name="notification-method" type="radio" class="formbricks-radio-input" />
|
||||
<label class="formbricks-radio-label">Founder</label>
|
||||
</div>
|
||||
<div class="formbricks-radio-option" data-element-name="jobTitle" data-element-value="executive">
|
||||
<div class="formbricks-radio-option" data-element-name="userSegment" data-element-value="executive">
|
||||
<input name="notification-method" type="radio" class="formbricks-radio-input" />
|
||||
<label class="formbricks-radio-label">Executive</label>
|
||||
</div>
|
||||
<div
|
||||
class="formbricks-radio-option"
|
||||
data-element-name="jobTitle"
|
||||
data-element-name="userSegment"
|
||||
data-element-value="productManager">
|
||||
<input name="notification-method" type="radio" class="formbricks-radio-input" />
|
||||
<label class="formbricks-radio-label">Product Manager</label>
|
||||
</div>
|
||||
<div class="formbricks-radio-option" data-element-name="jobTitle" data-element-value="productOwner">
|
||||
<div
|
||||
class="formbricks-radio-option"
|
||||
data-element-name="userSegment"
|
||||
data-element-value="productOwner">
|
||||
<input name="notification-method" type="radio" class="formbricks-radio-input" />
|
||||
<label class="formbricks-radio-label">Product Owner</label>
|
||||
</div>
|
||||
<div
|
||||
class="formbricks-radio-option"
|
||||
data-element-name="jobTitle"
|
||||
data-element-name="userSegment"
|
||||
data-element-value="softwareEngineer">
|
||||
<input name="notification-method" type="radio" class="formbricks-radio-input" />
|
||||
<label class="formbricks-radio-label">Software Engineer</label>
|
||||
@@ -79,8 +82,8 @@
|
||||
|
||||
<div class="formbricks-element formbricks-hidden" id="formbricks-question-3">
|
||||
<p class="formbricks-element-label">How can we improve our service for you?</p>
|
||||
<form class="formbricks-form" data-element-name="howImprove">
|
||||
<textarea rows="4" name="howImprove" class="formbricks-textarea" required></textarea>
|
||||
<form class="formbricks-form" data-element-name="improvement">
|
||||
<textarea rows="4" name="improvement" class="formbricks-textarea" required></textarea>
|
||||
<div class="formbricks-next-button-wrapper">
|
||||
<button type="submit" class="formbricks-next-button">Next</button>
|
||||
</div>
|
||||
@@ -89,8 +92,8 @@
|
||||
|
||||
<div class="formbricks-element formbricks-hidden" id="formbricks-question-4">
|
||||
<p class="formbricks-element-label">What type of people would benefit most from using our service?</p>
|
||||
<form class="formbricks-form" data-element-name="idealCustomer">
|
||||
<textarea rows="4" name="idealCustomer" class="formbricks-textarea" required></textarea>
|
||||
<form class="formbricks-form" data-element-name="selfSegmentation">
|
||||
<textarea rows="4" name="selfSegmentation" class="formbricks-textarea" required></textarea>
|
||||
<div class="formbricks-next-button-wrapper">
|
||||
<button type="submit" class="formbricks-next-button">Submit</button>
|
||||
</div>
|
||||
|
||||
@@ -120,7 +120,7 @@ async function submitElement(name?: string, value?: string) {
|
||||
const response = await createSubmission(submission);
|
||||
submissionId = response.id;
|
||||
} else {
|
||||
await updateSubmission(submissionId, submission, !!("idealCustomer" in submission));
|
||||
await updateSubmission(submissionId, submission, !!("selfSegmentation" in submission));
|
||||
}
|
||||
|
||||
// loading indication end
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
export function ArchiveIcon(props: any) {
|
||||
return (
|
||||
<svg viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M23.0625 3.9375H3.9375C3.31618 3.9375 2.8125 4.44118 2.8125 5.0625V16.0695C2.8125 16.6908 3.31618 17.1945 3.9375 17.1945H23.0625C23.6838 17.1945 24.1875 16.6908 24.1875 16.0695V5.0625C24.1875 4.44118 23.6838 3.9375 23.0625 3.9375Z"
|
||||
fill="#C4F0EB"
|
||||
/>
|
||||
<path
|
||||
d="M19.6875 14.0625C19.3891 14.0625 19.103 14.181 18.892 14.392C18.681 14.603 18.5625 14.8891 18.5625 15.1875C18.5625 15.4859 18.444 15.772 18.233 15.983C18.022 16.194 17.7359 16.3125 17.4375 16.3125H9.5625C9.26413 16.3125 8.97798 16.194 8.767 15.983C8.55603 15.772 8.4375 15.4859 8.4375 15.1875C8.4375 14.8891 8.31897 14.603 8.108 14.392C7.89702 14.181 7.61087 14.0625 7.3125 14.0625H1.6875C1.38913 14.0625 1.10298 14.181 0.892005 14.392C0.681026 14.603 0.5625 14.8891 0.5625 15.1875V21.9375C0.5625 22.2359 0.681026 22.522 0.892005 22.733C1.10298 22.944 1.38913 23.0625 1.6875 23.0625H25.3125C25.6109 23.0625 25.897 22.944 26.108 22.733C26.319 22.522 26.4375 22.2359 26.4375 21.9375V15.1875C26.4375 14.8891 26.319 14.603 26.108 14.392C25.897 14.181 25.6109 14.0625 25.3125 14.0625H19.6875Z"
|
||||
fill="#C4F0EB"
|
||||
/>
|
||||
<path
|
||||
d="M25.3125 20.0869H1.6875C1.38913 20.0869 1.10298 19.9684 0.892005 19.7574C0.681026 19.5464 0.5625 19.2603 0.5625 18.9619V21.9375C0.5625 22.2359 0.681026 22.5221 0.892005 22.733C1.10298 22.944 1.38913 23.0625 1.6875 23.0625H25.3125C25.6109 23.0625 25.897 22.944 26.108 22.733C26.319 22.5221 26.4375 22.2359 26.4375 21.9375V18.9619C26.4375 19.2603 26.319 19.5464 26.108 19.7574C25.897 19.9684 25.6109 20.0869 25.3125 20.0869Z"
|
||||
fill="#00C4B8"
|
||||
/>
|
||||
<path
|
||||
d="M10.6875 14.0625H16.3125"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.125"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.8125 11.8125C2.8125 11.5141 2.93103 11.228 3.142 11.017C3.35298 10.806 3.63913 10.6875 3.9375 10.6875H23.0625C23.3609 10.6875 23.647 10.806 23.858 11.017C24.069 11.228 24.1875 11.5141 24.1875 11.8125"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.125"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.8125 8.4375C2.8125 8.13913 2.93103 7.85298 3.142 7.64201C3.35298 7.43103 3.63913 7.3125 3.9375 7.3125H23.0625C23.3609 7.3125 23.647 7.43103 23.858 7.64201C24.069 7.85298 24.1875 8.13913 24.1875 8.4375"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.125"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.8125 5.0625C2.8125 4.76413 2.93103 4.47798 3.142 4.267C3.35298 4.05603 3.63913 3.9375 3.9375 3.9375H23.0625C23.3609 3.9375 23.647 4.05603 23.858 4.267C24.069 4.47798 24.1875 4.76413 24.1875 5.0625"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.125"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M19.6875 14.0625C19.3891 14.0625 19.103 14.181 18.892 14.392C18.681 14.603 18.5625 14.8891 18.5625 15.1875C18.5625 15.4859 18.444 15.772 18.233 15.983C18.022 16.194 17.7359 16.3125 17.4375 16.3125H9.5625C9.26413 16.3125 8.97798 16.194 8.76701 15.983C8.55603 15.772 8.4375 15.4859 8.4375 15.1875C8.4375 14.8891 8.31897 14.603 8.10799 14.392C7.89702 14.181 7.61087 14.0625 7.3125 14.0625H1.6875C1.38913 14.0625 1.10298 14.181 0.892005 14.392C0.681026 14.603 0.5625 14.8891 0.5625 15.1875V21.9375C0.5625 22.2359 0.681026 22.522 0.892005 22.733C1.10298 22.944 1.38913 23.0625 1.6875 23.0625H25.3125C25.6109 23.0625 25.897 22.944 26.108 22.733C26.319 22.522 26.4375 22.2359 26.4375 21.9375V15.1875C26.4375 14.8891 26.319 14.603 26.108 14.392C25.897 14.181 25.6109 14.0625 25.3125 14.0625H19.6875Z"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.125"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export function NotDisappointedIcon(props: any) {
|
||||
return (
|
||||
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g clipPath="url(#clip0_1_431)">
|
||||
<path
|
||||
d="M19.9999 39.1668C30.5854 39.1668 39.1666 30.5856 39.1666 20.0002C39.1666 9.41471 30.5854 0.833496 19.9999 0.833496C9.41446 0.833496 0.833252 9.41471 0.833252 20.0002C0.833252 30.5856 9.41446 39.1668 19.9999 39.1668Z"
|
||||
fill="#EF4444"
|
||||
/>
|
||||
<path
|
||||
d="M19.9999 7.50016C24.1407 7.50047 28.1848 8.75106 31.6029 11.0882C35.021 13.4253 37.6538 16.7401 39.1566 20.5985C39.1566 20.3985 39.1666 20.2002 39.1666 20.0002C39.1666 14.9168 37.1472 10.0417 33.5528 6.44728C29.9583 2.85284 25.0832 0.833496 19.9999 0.833496C14.9166 0.833496 10.0415 2.85284 6.44704 6.44728C2.85259 10.0417 0.833252 14.9168 0.833252 20.0002C0.833252 20.2002 0.833252 20.3985 0.843252 20.5985C2.34601 16.7401 4.97879 13.4253 8.39691 11.0882C11.815 8.75106 15.8592 7.50047 19.9999 7.50016Z"
|
||||
fill="#FEE2E2"
|
||||
/>
|
||||
<path
|
||||
d="M24.1667 15.6533C24.5202 16.2185 25.0116 16.6845 25.5947 17.0075C26.1778 17.3306 26.8335 17.5001 27.5001 17.5001C28.1667 17.5001 28.8223 17.3306 29.4054 17.0075C29.9885 16.6845 30.4799 16.2185 30.8334 15.6533"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.66667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.8334 15.6533C15.4799 16.2185 14.9885 16.6845 14.4054 17.0075C13.8223 17.3306 13.1667 17.5001 12.5001 17.5001C11.8335 17.5001 11.1778 17.3306 10.5947 17.0075C10.0116 16.6845 9.52024 16.2185 9.16675 15.6533"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.66667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M19.9999 39.1668C30.5854 39.1668 39.1666 30.5856 39.1666 20.0002C39.1666 9.41471 30.5854 0.833496 19.9999 0.833496C9.41446 0.833496 0.833252 9.41471 0.833252 20.0002C0.833252 30.5856 9.41446 39.1668 19.9999 39.1668Z"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.66667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M22.5 30.8332H25.8333C25.8333 31.2752 26.0089 31.6991 26.3215 32.0117C26.634 32.3242 27.058 32.4998 27.5 32.4998C27.942 32.4998 28.366 32.3242 28.6785 32.0117C28.9911 31.6991 29.1667 31.2752 29.1667 30.8332V25.8332C29.1667 25.3911 28.9911 24.9672 28.6785 24.6547C28.366 24.3421 27.942 24.1665 27.5 24.1665C27.058 24.1665 26.634 24.3421 26.3215 24.6547C26.0089 24.9672 25.8333 25.3911 25.8333 25.8332H22.5"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.66667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_431">
|
||||
<rect width="40" height="40" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export function SomewhatDisappointedIcon(props: any) {
|
||||
return (
|
||||
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g clipPath="url(#clip0_1_440)">
|
||||
<path
|
||||
d="M19.9999 39.1666C30.5854 39.1666 39.1666 30.5854 39.1666 19.9999C39.1666 9.41446 30.5854 0.833252 19.9999 0.833252C9.41446 0.833252 0.833252 9.41446 0.833252 19.9999C0.833252 30.5854 9.41446 39.1666 19.9999 39.1666Z"
|
||||
fill="#F59E0B"
|
||||
/>
|
||||
<path
|
||||
d="M19.9999 7.49992C24.1407 7.50023 28.1848 8.75081 31.6029 11.0879C35.021 13.4251 37.6538 16.7398 39.1566 20.5983C39.1566 20.3983 39.1666 20.1999 39.1666 19.9999C39.1666 14.9166 37.1472 10.0415 33.5528 6.44704C29.9583 2.85259 25.0832 0.833252 19.9999 0.833252C14.9166 0.833252 10.0415 2.85259 6.44704 6.44704C2.85259 10.0415 0.833252 14.9166 0.833252 19.9999C0.833252 20.1999 0.833252 20.3983 0.843252 20.5983C2.34601 16.7398 4.97879 13.4251 8.39691 11.0879C11.815 8.75081 15.8592 7.50023 19.9999 7.49992Z"
|
||||
fill="#FFFBEB"
|
||||
/>
|
||||
<path
|
||||
d="M19.9999 39.1666C30.5854 39.1666 39.1666 30.5854 39.1666 19.9999C39.1666 9.41446 30.5854 0.833252 19.9999 0.833252C9.41446 0.833252 0.833252 9.41446 0.833252 19.9999C0.833252 30.5854 9.41446 39.1666 19.9999 39.1666Z"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.66667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.8334 16.25C10.9158 16.25 10.9964 16.2744 11.0649 16.3202C11.1334 16.366 11.1868 16.4311 11.2184 16.5072C11.2499 16.5834 11.2582 16.6671 11.2421 16.748C11.226 16.8288 11.1863 16.903 11.128 16.9613C11.0698 17.0196 10.9955 17.0593 10.9147 17.0753C10.8339 17.0914 10.7501 17.0832 10.674 17.0516C10.5978 17.0201 10.5328 16.9667 10.487 16.8982C10.4412 16.8296 10.4167 16.7491 10.4167 16.6667C10.4167 16.5562 10.4606 16.4502 10.5388 16.372C10.6169 16.2939 10.7229 16.25 10.8334 16.25Z"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.66667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M29.1667 16.25C29.0843 16.25 29.0037 16.2744 28.9352 16.3202C28.8667 16.366 28.8133 16.4311 28.7817 16.5072C28.7502 16.5834 28.7419 16.6671 28.758 16.748C28.7741 16.8288 28.8138 16.903 28.872 16.9613C28.9303 17.0196 29.0046 17.0593 29.0854 17.0753C29.1662 17.0914 29.25 17.0832 29.3261 17.0516C29.4023 17.0201 29.4673 16.9667 29.5131 16.8982C29.5589 16.8296 29.5833 16.7491 29.5833 16.6667C29.5833 16.5562 29.5394 16.4502 29.4613 16.372C29.3832 16.2939 29.2772 16.25 29.1667 16.25Z"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.66667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M17.5 32.685C18.7236 30.7825 20.4885 29.2896 22.5677 28.3985C24.6469 27.5075 26.9451 27.259 29.1667 27.685"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.66667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_440">
|
||||
<rect width="40" height="40" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export function VeryDisappointedIcon(props: any) {
|
||||
return (
|
||||
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g clipPath="url(#clip0_1_422)">
|
||||
<path
|
||||
d="M19.9999 39.1666C30.5854 39.1666 39.1666 30.5854 39.1666 19.9999C39.1666 9.41446 30.5854 0.833252 19.9999 0.833252C9.41446 0.833252 0.833252 9.41446 0.833252 19.9999C0.833252 30.5854 9.41446 39.1666 19.9999 39.1666Z"
|
||||
fill="#10B981"
|
||||
/>
|
||||
<path
|
||||
d="M19.9999 7.49992C24.1407 7.50023 28.1848 8.75081 31.6029 11.0879C35.021 13.4251 37.6538 16.7398 39.1566 20.5983C39.1566 20.3983 39.1666 20.1999 39.1666 19.9999C39.1666 14.9166 37.1472 10.0415 33.5528 6.44704C29.9584 2.85259 25.0832 0.833252 19.9999 0.833252C14.9166 0.833252 10.0415 2.85259 6.44704 6.44704C2.85259 10.0415 0.833252 14.9166 0.833252 19.9999C0.833252 20.1999 0.833252 20.3983 0.843252 20.5983C2.34601 16.7398 4.97879 13.4251 8.39691 11.0879C11.815 8.75081 15.8592 7.50023 19.9999 7.49992Z"
|
||||
fill="#ECFDF5"
|
||||
/>
|
||||
<path
|
||||
d="M19.9999 39.1666C30.5854 39.1666 39.1666 30.5854 39.1666 19.9999C39.1666 9.41446 30.5854 0.833252 19.9999 0.833252C9.41446 0.833252 0.833252 9.41446 0.833252 19.9999C0.833252 30.5854 9.41446 39.1666 19.9999 39.1666Z"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.70833"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.8334 16.25C10.9158 16.25 10.9964 16.2744 11.0649 16.3202C11.1334 16.366 11.1868 16.4311 11.2184 16.5072C11.2499 16.5834 11.2582 16.6671 11.2421 16.748C11.226 16.8288 11.1863 16.903 11.128 16.9613C11.0698 17.0196 10.9955 17.0593 10.9147 17.0753C10.8339 17.0914 10.7501 17.0832 10.674 17.0516C10.5978 17.0201 10.5328 16.9667 10.487 16.8982C10.4412 16.8296 10.4167 16.7491 10.4167 16.6667C10.4167 16.5562 10.4606 16.4502 10.5388 16.372C10.6169 16.2939 10.7229 16.25 10.8334 16.25Z"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.70833"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M29.1667 16.25C29.0843 16.25 29.0037 16.2744 28.9352 16.3202C28.8667 16.366 28.8133 16.4311 28.7817 16.5072C28.7502 16.5834 28.7419 16.6671 28.758 16.748C28.7741 16.8288 28.8138 16.903 28.872 16.9613C28.9303 17.0196 29.0046 17.0593 29.0854 17.0753C29.1662 17.0914 29.25 17.0832 29.3261 17.0516C29.4023 17.0201 29.4673 16.9667 29.5131 16.8982C29.5589 16.8296 29.5833 16.7491 29.5833 16.6667C29.5833 16.5562 29.5394 16.4502 29.4613 16.372C29.3832 16.2939 29.2772 16.25 29.1667 16.25Z"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.70833"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.5 31.6667C12.5 29.6776 13.2902 27.77 14.6967 26.3634C16.1032 24.9569 18.0109 24.1667 20 24.1667C21.9891 24.1667 23.8968 24.9569 25.3033 26.3634C26.7098 27.77 27.5 29.6776 27.5 31.6667"
|
||||
stroke="#00303E"
|
||||
strokeWidth="1.70833"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_422">
|
||||
<rect width="40" height="40" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -32,3 +32,7 @@ export * from "./icons/CheckMarkIcon";
|
||||
export * from "./icons/BellIcon";
|
||||
export * from "./icons/SkyscraperIcon";
|
||||
export * from "./icons/UserDeveloperIcon";
|
||||
export * from "./icons/VeryDisappointedIcon";
|
||||
export * from "./icons/SomewhatDisappointedIcon";
|
||||
export * from "./icons/NotDisappointedIcon";
|
||||
export * from "./icons/ArchiveIcon";
|
||||
|
||||
Reference in New Issue
Block a user