feat: add basic code form page and pipelines page

This commit is contained in:
Matthias Nannt
2022-06-14 17:21:47 +09:00
parent 0eef1bed32
commit 8f0c2ce642
14 changed files with 530 additions and 35 deletions

View File

@@ -44,7 +44,7 @@ export default function FormList() {
.map((form, formIdx) => (
<li key={form.id} className="col-span-1 ">
<div className="bg-white divide-y divide-gray-200 rounded-lg shadow">
<Link href={`/forms/${form.id}/form`}>
<Link href={`/forms/${form.id}`}>
<a>
<div className="px-4 py-5 sm:p-6">{form.name}</div>
</a>

View File

@@ -0,0 +1,233 @@
import { RadioGroup } from "@headlessui/react";
import Link from "next/link";
import { useState } from "react";
import { FaReact, FaVuejs } from "react-icons/fa";
import { classNames } from "../../lib/utils";
const libs = [
{
id: "react",
name: "React",
href: "#",
bgColor: "bg-cyan-500",
ringColor: "ring-cyan-500",
icon: FaReact,
},
{
id: "reactNative",
name: "React Native",
comingSoon: true,
href: "#",
members: 12,
bgColor: "bg-cyan-600",
ringColor: "ring-cyan-600",
icon: FaReact,
},
{
id: "vue",
name: "Vue.js",
comingSoon: true,
href: "#",
members: 16,
bgColor: "bg-emerald-400",
ringColor: "ring-emerald-400",
icon: FaVuejs,
},
];
export default function FormCode() {
const [selectedLib, setSelectedLib] = useState(null);
return (
<>
<header>
<div className="mx-auto mt-8 max-w-7xl">
<h1 className="text-3xl font-bold leading-tight text-gray-900">
Get started
</h1>
</div>
</header>
<div className="my-4">
<p className="text-gray-700">
Welcome to your new form! To start using snoopHub with your
application you need to build a form using our libs for your preferred
programming language or framework.
</p>
</div>
<div className="mt-5">
<RadioGroup value={selectedLib} onChange={setSelectedLib}>
<RadioGroup.Label className="text-xs font-medium tracking-wide text-gray-500 uppercase">
Choose your framework
</RadioGroup.Label>
<ul
role="list"
className="grid grid-cols-1 gap-5 mt-3 sm:gap-6 sm:grid-cols-3 lg:grid-cols-3"
>
{libs.map((lib) => (
<RadioGroup.Option
key={lib.id}
value={lib}
className={({ checked }) =>
classNames(
checked ? `ring-2 ${lib.ringColor}` : "",
lib.comingSoon ? "opacity-50" : "",
"flex col-span-1 rounded-md shadow-sm"
)
}
disabled={lib.comingSoon}
>
{({}) => (
<li
className={classNames(
lib.comingSoon ? "opacity-50" : "",
"flex col-span-1 rounded-md shadow-sm w-full"
)}
>
<div
className={classNames(
lib.bgColor,
"flex-shrink-0 flex items-center justify-center w-16 text-white text-sm font-medium rounded-l-md"
)}
>
<lib.icon className="w-5 h-5" />
</div>
<div
className={classNames(
lib.comingSoon ? "border-dashed" : "",
"flex items-center justify-between flex-1 truncate bg-white border-t border-b border-r border-gray-200 rounded-r-md"
)}
>
<div className="flex-1 px-4 py-5 text-sm truncate">
<RadioGroup.Label className="font-medium text-gray-900 hover:text-gray-600">
{lib.name}
</RadioGroup.Label>
{lib.comingSoon && (
<p className="inline-block ml-1 text-gray-500">
(coming soon)
</p>
)}
</div>
</div>
</li>
)}
</RadioGroup.Option>
))}
</ul>
</RadioGroup>
</div>
<div className="mt-10">
{selectedLib?.id === "react" ? (
<div className="relative px-5 py-16 overflow-hidden border border-gray-200 rounded-lg shadow-inner">
<div className="relative px-4 sm:px-6 lg:px-8">
<div className="mx-auto text-lg max-w-prose">
<h1>
<span className="block text-base font-semibold tracking-wide text-center text-red-600">
snoopReact
</span>
<span className="block mt-2 text-3xl font-extrabold leading-8 tracking-tight text-center text-gray-900 sm:text-4xl">
How to build your form
</span>
</h1>
</div>
<div className="mx-auto mt-6 text-base prose prose-lg text-gray-500 prose-red">
<p>
Getting the snoopForms React Library up and running with Node
Package Manager:
</p>
<pre>
<code className="language-js">
npm install --save @snoopforms/react
</code>
</pre>
<p>Then build your form using our built-in components</p>
<pre>
<code className="language-js">
{`import React from "react";
import { SnoopForm, SnoopElement, SnoopPage } from "@snoopforms/react";
export default function Example({}) {
return (
<SnoopForm
domain="localhost:3000"
protocol="http"
className="w-full space-y-6"
onSubmit={({ submission, schema }) => {
// do something with the data additional to sending to snoopForms
}}
>
<SnoopPage name="first">
<SnoopElement
type="text"
name={"name"}
label="Your name"
classNames={{
label: "your-label-class",
element: "your-input-class",
}}
required
/>
</SnoopPage>
<SnoopPage name="second">
<SnoopElement
type="radio"
name={"importance"}
label="What's your favorite food?"
classNames={{
label: "your-label-class",
radioGroup: "your-radio-group-class",
radioOption: "your-radio-option-class",
}}
options={["Pizza", "Pasta", "Sushi"]}
/>
<SnoopElement
type="submit"
label="Submit"
classNames={{
button: "your-submit-button-class",
}}
/>
</SnoopPage>
<SnoopPage thankyou>
<h1>Thank you!</h1>
</SnoopPage>
</SnoopForm>
);
}`}
</code>
</pre>
<p>
To read more about building your form with snoopReact, check
out our{" "}
<Link href="https://docs.snoopforms.com/">
<a target="_blank">docs</a>
</Link>
.
</p>
<div className="p-4 border border-gray-200 rounded-md bg-gray-50">
<div className="flex">
<div className="flex-1 ml-3 md:flex md:justify-between">
<p className="text-sm text-gray-700">
Are you ready to go live and receive submissions? Go to{" "}
<Link href="pipelines">
<a>Pipelines</a>
</Link>{" "}
to pipe your submissions to other systems or go straight
to the{" "}
<Link href="results">
<a>Results</a>
</Link>{" "}
to see how your form is used and keep track of your
submissions.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
) : null}
</div>
</>
);
}

View File

@@ -38,8 +38,12 @@ export default function LayoutShare({ title, formId, currentStep, children }) {
</header>
{/* Main content */}
<main className="max-w-lg px-4 pt-10 pb-12 mx-auto lg:pb-16">
{children}
<main>
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
{/* Replace with your content */}
{children}
{/* /End replace */}
</div>
</main>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import useSWR from "swr";
import { fetcher } from "./utils";
export const useAnswerSessions = (formId: string) => {
export const useSubmissionSessions = (formId: string) => {
const { data, error, mutate } = useSWR(
() => `/api/forms/${formId}/submissionSessions`,
fetcher
@@ -9,8 +9,8 @@ export const useAnswerSessions = (formId: string) => {
return {
submissionSessions: data,
isLoadingAnswerSessions: !error && !data,
isErrorAnswerSessions: error,
mutateAnswerSessions: mutate,
isLoadingSubmissionSessions: !error && !data,
isErrorSubmissionSessions: error,
mutateSubmissionSessions: mutate,
};
};

View File

@@ -25,11 +25,13 @@
"react-contenteditable": "^3.3.6",
"react-dom": "18.1.0",
"react-feather": "^2.0.9",
"react-icons": "^4.4.0",
"superjson": "^1.9.1",
"swr": "^1.3.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.1",
"@tailwindcss/typography": "^0.5.2",
"@types/bcryptjs": "^2.4.2",
"@types/node": "17.0.32",
"@types/react": "18.0.9",

View File

@@ -2,8 +2,9 @@ import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import FormOnboardingModal from "../../../components/build/FormOnboardingModal";
import LayoutResults from "../../../components/layout/LayoutResults";
import FormCode from "../../../components/form/FormCode";
import FormOnboardingModal from "../../../components/form/FormOnboardingModal";
import LayoutFormBasics from "../../../components/layout/LayoutFormBasic";
import Loading from "../../../components/Loading";
import { useForm } from "../../../lib/forms";
@@ -23,18 +24,32 @@ export default function FormPage() {
return <Loading />;
}
return (
<>
<LayoutResults title={form.title} formId={formId} currentStep="form">
<div>Form</div>
<FormOnboardingModal
open={openOnboardingModal}
setOpen={setOpenOnboardingModal}
formId={formId}
/>
</LayoutResults>
</>
);
if (form.formType === "NOCODE") {
return (
<>
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
<FormOnboardingModal
open={openOnboardingModal}
setOpen={setOpenOnboardingModal}
formId={formId}
/>
</LayoutFormBasics>
</>
);
} else {
return (
<>
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
<FormCode />
<FormOnboardingModal
open={openOnboardingModal}
setOpen={setOpenOnboardingModal}
formId={formId}
/>
</LayoutFormBasics>
</>
);
}
}
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {

View File

@@ -0,0 +1,26 @@
import { useRouter } from "next/router";
import { useEffect } from "react";
import Loading from "../../../components/Loading";
import { useSubmissionSessions } from "../../../lib/submissionSessions";
export default function FormIndex() {
const router = useRouter();
console.log(router.query);
const formId = router.query.id;
const { submissionSessions, isLoadingSubmissionSessions } =
useSubmissionSessions(formId?.toString());
useEffect(() => {
if (!isLoadingSubmissionSessions) {
// redirect to /results if there is at least one submissionSession
if (submissionSessions.length > 0) {
router.push(`/forms/${formId}/results`);
} else {
// redirect to /form if there isn't one submissionSession
router.push(`/forms/${formId}/form`);
}
}
}, [isLoadingSubmissionSessions]);
return <Loading />;
}

View File

@@ -1,9 +1,55 @@
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import LayoutResults from "../../../components/layout/LayoutResults";
import LayoutFormBasics from "../../../components/layout/LayoutFormBasic";
import Loading from "../../../components/Loading";
import { useForm } from "../../../lib/forms";
import { BiPlug } from "react-icons/bi";
import { SiZapier, SiAirtable, SiSlack } from "react-icons/si";
import { FaCode, FaGoogle } from "react-icons/fa";
import { classNames } from "../../../lib/utils";
const libs = [
{
id: "webhook",
name: "Webhook",
href: "#",
bgColor: "bg-red-500",
icon: FaCode,
},
{
id: "googleSheets",
name: "Google Sheets",
comingSoon: true,
href: "#",
bgColor: "bg-green-700",
icon: FaGoogle,
},
{
id: "zapier",
name: "Zapier",
comingSoon: true,
href: "#",
bgColor: "bg-orange-500",
icon: SiZapier,
},
{
id: "airtable",
name: "Airtable",
comingSoon: true,
href: "#",
bgColor: "bg-sky-400",
icon: SiAirtable,
},
{
id: "slack",
name: "Slack",
comingSoon: true,
href: "#",
bgColor: "bg-purple-800",
icon: SiSlack,
},
];
export default function PipelinesPage() {
const router = useRouter();
@@ -16,9 +62,96 @@ export default function PipelinesPage() {
return (
<>
<LayoutResults title={form.title} formId={formId} currentStep="pipelines">
<div>Pipelines</div>
</LayoutResults>
<LayoutFormBasics
title={form.title}
formId={formId}
currentStep="pipelines"
>
<header>
<div className="mx-auto mt-8 max-w-7xl">
<h1 className="text-3xl font-bold leading-tight text-gray-900">
Pipe your data
</h1>
</div>
</header>
<div className="my-4">
<p className="text-gray-700">
snoopHub automatically stores your data and gives you and overview
of your submissions and form analytics. If you want to use your
submissions or form events in other systems you can set up pipelines
to let snoopHub sent the data to these applications as soon as it
arrives.
</p>
</div>
<div>
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase">
Active integrations
</div>
<div className="p-3 my-5 text-center border rounded-lg shadow-inner sm:p-6">
<BiPlug className="w-12 h-12 mx-auto text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
You don&apos;t have any active pipelines
</h3>
<p className="mt-1 text-sm text-gray-500">
Choose a method from the available integrations and set up your
pipeline
</p>
</div>
</div>
<div>
<div className="mt-8">
<div>
<p className="text-xs font-medium tracking-wide text-gray-500 uppercase">
Available integrations
</p>
<ul
role="list"
className="grid grid-cols-1 gap-5 mt-3 sm:gap-6 sm:grid-cols-3 lg:grid-cols-3"
>
{libs.map((lib) => (
<a
className="flex col-span-1 rounded-md shadow-sm"
key={lib.id}
>
<li
className={classNames(
lib.comingSoon ? "opacity-50" : "",
"flex col-span-1 rounded-md shadow-sm w-full"
)}
>
<div
className={classNames(
lib.bgColor,
"flex-shrink-0 flex items-center justify-center w-16 text-white text-sm font-medium rounded-l-md"
)}
>
<lib.icon className="w-5 h-5" />
</div>
<div
className={classNames(
lib.comingSoon ? "border-dashed" : "",
"flex items-center justify-between flex-1 truncate bg-white border-t border-b border-r border-gray-200 rounded-r-md"
)}
>
<div className="inline-flex px-4 py-8 text-sm truncate">
<p className="font-medium text-gray-900 hover:text-gray-600">
{lib.name}
</p>
{lib.comingSoon && (
<p className="ml-1 text-gray-500">(coming soon)</p>
)}
</div>
</div>
</li>
</a>
))}
</ul>
</div>
</div>
</div>
</LayoutFormBasics>
</>
);
}

View File

@@ -1,27 +1,30 @@
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import LayoutResults from "../../../components/layout/LayoutResults";
import LayoutFormBasics from "../../../components/layout/LayoutFormBasic";
import Loading from "../../../components/Loading";
import Submission from "../../../components/results/Submission";
import { useForm } from "../../../lib/forms";
import { useAnswerSessions } from "../../../lib/submissionSessions";
import { useSubmissionSessions } from "../../../lib/submissionSessions";
export default function Share() {
const router = useRouter();
const formId = router.query.id.toString();
const { form, isLoadingForm } = useForm(router.query.id);
const { submissionSessions, isLoadingAnswerSessions } = useAnswerSessions(
form?.id
);
const { submissionSessions, isLoadingSubmissionSessions } =
useSubmissionSessions(form?.id);
if (isLoadingForm || isLoadingAnswerSessions) {
if (isLoadingForm || isLoadingSubmissionSessions) {
return <Loading />;
}
return (
<>
<LayoutResults title={form.title} formId={formId} currentStep="results">
<LayoutFormBasics
title={form.title}
formId={formId}
currentStep="results"
>
<div className="bg-white shadow sm:rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium leading-6 text-gray-900">
@@ -39,7 +42,7 @@ export default function Share() {
/>
))}
</div>
</LayoutResults>
</LayoutFormBasics>
</>
);
}

View File

@@ -1,6 +1,9 @@
-- CreateEnum
CREATE TYPE "FormType" AS ENUM ('CODE', 'NOCODE');
-- CreateEnum
CREATE TYPE "PipelineType" AS ENUM ('WEBHOOK');
-- CreateTable
CREATE TABLE "Form" (
"id" TEXT NOT NULL,
@@ -16,6 +19,18 @@ CREATE TABLE "Form" (
CONSTRAINT "Form_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Pipeline" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"type" "PipelineType" NOT NULL,
"formId" TEXT NOT NULL,
"data" JSONB NOT NULL,
CONSTRAINT "Pipeline_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SubmissionSession" (
"id" TEXT NOT NULL,
@@ -73,6 +88,9 @@ CREATE UNIQUE INDEX "verification_requests_token_key" ON "verification_requests"
-- AddForeignKey
ALTER TABLE "Form" ADD CONSTRAINT "Form_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Pipeline" ADD CONSTRAINT "Pipeline_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SubmissionSession" ADD CONSTRAINT "SubmissionSession_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -12,6 +12,10 @@ enum FormType {
NOCODE
}
enum PipelineType {
WEBHOOK
}
model Form {
id String @id
createdAt DateTime @default(now())
@@ -24,6 +28,17 @@ model Form {
finishedOnboarding Boolean @default(false)
schema Json
submissionSessions SubmissionSession[]
pipelines Pipeline[]
}
model Pipeline {
id String @id @default(uuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
type PipelineType
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String
data Json
}
model SubmissionSession {
@@ -42,7 +57,7 @@ model SessionEvent {
updatedAt DateTime @updatedAt @map(name: "updated_at")
submissionSession SubmissionSession @relation(fields: [submissionSessionId], references: [id], onDelete: Cascade)
submissionSessionId String
type String
type String
data Json
}

View File

@@ -6,5 +6,5 @@ module.exports = {
theme: {
extend: {},
},
plugins: [require("@tailwindcss/forms")],
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
};

View File

@@ -220,6 +220,15 @@
dependencies:
mini-svg-data-uri "^1.2.3"
"@tailwindcss/typography@^0.5.2":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.2.tgz#24b069dab24d7a2467d01aca0dd432cb4b29f0ee"
integrity sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==
dependencies:
lodash.castarray "^4.4.0"
lodash.isplainobject "^4.0.6"
lodash.merge "^4.6.2"
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
@@ -1234,6 +1243,16 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
highlight.js@^10.5.0:
version "10.7.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
highlight.js@^11.5.1:
version "11.5.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.5.1.tgz#027c24e4509e2f4dcd00b4a6dda542ce0a1f7aea"
integrity sha512-LKzHqnxr4CrD2YsNoIf/o5nJ09j4yi/GcH5BnYz9UnVpZdS4ucMgvP61TDty5xJcFGRjnH4DpujkS9bHT3hq0Q==
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -1477,6 +1496,16 @@ lodash-es@^4.17.15:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.castarray@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -1888,6 +1917,11 @@ prisma@^3.15.1:
dependencies:
"@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
prismjs@^1.28.0:
version "1.28.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.28.0.tgz#0d8f561fa0f7cf6ebca901747828b149147044b6"
integrity sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==
prop-types@^15.5.10, prop-types@^15.7.1, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@@ -1966,6 +2000,18 @@ react-feather@^2.0.9:
dependencies:
prop-types "^15.7.2"
react-highlight@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/react-highlight/-/react-highlight-0.14.0.tgz#5aefa5518baa580f96b68d48129d7a5d2dc0c9ef"
integrity sha512-kWE+KXOXidS7SABhVopOgMnowbI3RAfeGZbnrduLNlWrYAED8sycL9l/Fvw3w0PFpIIawB7mRDnyhDcM/cIIGA==
dependencies:
highlight.js "^10.5.0"
react-icons@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.4.0.tgz#a13a8a20c254854e1ec9aecef28a95cdf24ef703"
integrity sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"