Merge branch 'jojo' into main

This commit is contained in:
knugget
2022-06-22 14:52:53 -05:00
11 changed files with 7647 additions and 140 deletions

2
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -2,13 +2,14 @@ import Link from "next/link";
import Router from "next/router";
import { Fragment } from "react";
import { Menu, Transition } from "@headlessui/react";
import { BsFilesAlt } from "react-icons/bs";
import {
DotsHorizontalIcon,
DocumentAddIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/solid";
TerminalIcon,
ViewGridAddIcon,
} from "@heroicons/react/outline";
import EmptyPageFiller from "./layout/EmptyPageFiller";
import { DotsHorizontalIcon, TrashIcon } from "@heroicons/react/solid";
import { classNames } from "../lib/utils";
import { createForm, useForms } from "../lib/forms";
import Image from "next/image";
@@ -34,52 +35,32 @@ export default function FormList() {
console.error(error);
}
};
return (
<div>
{forms &&
(forms.length === 0 ? (
<div className="mt-5 text-center">
<Image
src="/img/mascot-face-small.png"
height={200}
width={200}
alt="snoopForms Mascot"
/>
<hr className="mb-8 -mt-2" />
<h1 className="text-xl font-extrabold tracking-tight text-gray-900 sm:text-2xl md:text-3xl">
<span className="block xl:inline">Welcome to snoopForms</span>{" "}
</h1>
<p className="max-w-md mx-auto mt-3 text-base text-gray-500 sm:text-lg md:mt-5 md:text-lg md:max-w-3xl">
Spin up forms in minutes. Pipe your data exactly where you need
it. Maximize your results with juicy analytics.
</p>
{/* <hr className="my-8" /> */}
<div className="max-w-md p-8 mx-auto mt-8 rounded-lg shadow-inner bg-lightgray-200">
<BsFilesAlt className="w-12 h-12 mx-auto text-gray-400" />
<h3 className="mt-3 text-sm font-medium text-gray-900">
You don&apos;t have any forms yet.
</h3>
<p className="mt-1 text-sm text-gray-500">
It&apos;s time to create your first!
</p>
<div className="mt-6">
<button
type="button"
onClick={() => newForm()}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white border border-transparent rounded-md shadow-sm bg-snoopred-600 hover:bg-snoopred-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred-500"
>
<PlusIcon className="w-5 h-5 mr-2 -ml-1" aria-hidden="true" />
New Form
</button>
</div>
</div>
<EmptyPageFiller
onClick={() => newForm()}
alertText="You don't have any forms yet."
hintText="Start by creating a form."
buttonText="create form"
borderStyles="border-4 border-dotted border-snoopred"
icon="BsFilePlus"
>
<DocumentAddIcon className="w-24 h-24 mx-auto text-lightgray-700 stroke-thin" />
</EmptyPageFiller>
</div>
) : (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 place-content-stretch">
<button onClick={() => newForm()}>
<li className="col-span-1">
<div className="overflow-hidden text-white rounded-lg shadow bg-snoopred">
<div className="px-4 py-8 sm:p-10">+ New Form</div>
<div className="overflow-hidden font-light text-white rounded-md shadow bg-snoopfade">
<div className="px-4 py-8 sm:p-14">
<PlusIcon className="mx-auto w-14 h-14 stroke-thin" />
create form
</div>
</div>
</li>
</button>
@@ -87,71 +68,92 @@ export default function FormList() {
.sort((a, b) => b.updatedAt - a.updatedAt)
.map((form, formIdx) => (
<li key={form.id} className="col-span-1 ">
<div className="bg-white divide-y rounded-lg shadow divide-lightgray-200">
<div className="flex flex-col justify-between h-full bg-white rounded-md shadow">
<Link href={`/forms/${form.id}`}>
<a>
<div className="px-4 py-5 sm:p-6">{form.name}</div>
<div className="px-4 py-5 text-lg sm:p-6">
{form.name}
</div>
</a>
</Link>
<div className="px-4 py-1 text-right sm:px-6">
<Menu
as="div"
className="relative inline-block text-left"
>
{({ open }) => (
<>
<div>
<Menu.Button className="flex items-center p-2 -m-2 rounded-full text-darkgray-400 hover:text-darkgray-500 focus:outline-none">
<span className="sr-only">Open options</span>
<DotsHorizontalIcon
className="w-5 h-5"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className="absolute left-0 w-56 mt-2 origin-top-right bg-white rounded-md shadow-lg"
>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<button
onClick={() =>
deleteForm(form, formIdx)
}
className={classNames(
active
? "bg-lightgray-100 text-darkgray-700"
: "text-darkgray-500",
"flex px-4 py-2 text-sm w-full"
)}
>
<TrashIcon
className="w-5 h-5 mr-3 text-darkgray-400"
aria-hidden="true"
/>
<span>Delete Form</span>
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
<div className="divide-y divide-lightgray-200 ">
<div className="inline-flex px-2 py-1 mb-2 ml-4 text-sm rounded-sm bg-lightgray-400 text-darkgray-700">
{form.formType == "NOCODE" ? (
<div className="flex">
<ViewGridAddIcon className="w-4 h-4 my-auto mr-1" />
No-Code
</div>
) : (
<div className="flex">
<TerminalIcon className="w-4 h-4 my-auto mr-1" />
Code
</div>
)}
</Menu>
</div>
<div className="flex justify-between px-4 py-2 text-right sm:px-6">
<p className="text-xs text-lightgray-900 ">
{form.submissionSessions?.length} responses
</p>
<Menu
as="div"
className="relative inline-block text-left"
>
{({ open }) => (
<>
<div>
<Menu.Button className="flex items-center p-2 -m-2 rounded-full text-snoopred">
<span className="sr-only">Open options</span>
<DotsHorizontalIcon
className="w-5 h-5"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className="absolute left-0 w-56 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<button
onClick={() =>
deleteForm(form, formIdx)
}
className={classNames(
active
? "bg-lightgray-100 text-darkgray-700"
: "text-darkgray-500",
"flex px-4 py-2 text-sm w-full"
)}
>
<TrashIcon
className="w-5 h-5 mr-3 text-darkgray-400"
aria-hidden="true"
/>
<span>Delete Form</span>
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
</div>
</div>
</li>

View File

@@ -0,0 +1,34 @@
import React from "react";
interface Props {
children: React.ReactNode;
onClick: () => void;
disabled?: boolean;
fullwidth?: boolean;
}
// button component, consuming props
const Button: React.FC<Props> = ({
children,
onClick,
disabled,
fullwidth,
...rest
}) => {
return (
<button
className={
`inline-flex items-center px-4 py-2 text-sm text-white border border-transparent rounded shadow-sm bg-snoopfade focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred-500 ` +
(disabled ? " disabled" : "") +
(fullwidth ? " w-full justify-center " : "")
}
onClick={onClick}
disabled={disabled}
{...rest}
>
{children}
</button>
);
};
export default Button;

View File

@@ -1,24 +1,27 @@
/* This example requires Tailwind CSS v2.0+ */
import { Dialog, RadioGroup, Transition } from "@headlessui/react";
import { CheckCircleIcon, LightBulbIcon } from "@heroicons/react/solid";
import { CheckCircleIcon, XIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import { Fragment, useState } from "react";
import { persistForm, useForm } from "../../lib/forms";
import { createNoCodeForm } from "../../lib/noCodeForm";
import { classNames } from "../../lib/utils";
import Loading from "../Loading";
import Button from "../StandardButton.tsx";
import { BsPlus } from "react-icons/bs";
const formTypes = [
{
id: "NOCODE",
title: "No-Code Builder",
description:
"Use our Notion-like form builder to build your form without a single line of code.",
"Use the Notion-like builder to build your form without a single line of code.",
},
{
id: "CODE",
title: "Code",
description: "Use our snoopReact library to code the form yourself.",
description:
"Use the snoopReact library to code the form yourself and manage the data here.",
additionalDescription: "",
},
];
@@ -76,7 +79,7 @@ export default function FormOnboardingModal({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 transition-opacity bg-darkgray-500 bg-opacity-10 backdrop-blur" />
<Dialog.Overlay className="fixed inset-0 transition-opacity bg-darkgray-300 bg-opacity-10 backdrop-blur" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
@@ -97,34 +100,26 @@ export default function FormOnboardingModal({
>
<form
onSubmit={(e) => submitForm(e)}
className="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-xl sm:w-full sm:p-6"
className="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-md shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"
>
<div>
<div className="flex items-center justify-center w-12 h-12 mx-auto rounded-full bg-snoopred-100">
<LightBulbIcon
className="w-6 h-6 text-snoopred"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-5">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-darkgray-700"
>
Welcome to your new form
</Dialog.Title>
</div>
<div className="flex flex-row justify-between">
<h2 className="flex-none text-darkgray-700 text-xl font-bold pb-4">
Create new form
</h2>
<XIcon className="flex-initial w-6 h-6 text-gray-200 stroke-1" />
</div>
<hr className="my-4" />
<div>
<label htmlFor="email" className="text-sm text-darkgray-500">
<label
htmlFor="email"
className="text-sm font-light text-darkgray-700"
>
Name your form
</label>
<div className="mt-2">
<input
type="text"
name="name"
className="block w-full p-2 mb-8 rounded-lg focus:ring-2 focus:ring-snoopred sm:text-sm"
className="block w-full p-2 mb-8 rounded border-none focus:ring-2 focus:ring-snoopred sm:text-sm bg-gray-100 placeholder:font-extralight placeholder:text-darkgray-200"
placeholder="e.g. Customer Research Survey"
value={name}
onChange={(e) => setName(e.target.value)}
@@ -132,10 +127,10 @@ export default function FormOnboardingModal({
/>
</div>
</div>
{/* <hr className="my-5" /> */}
<RadioGroup value={formType} onChange={setFormType}>
<RadioGroup.Label className="text-sm text-darkgray-500">
How would you like to build your form?
<RadioGroup.Label className="text-sm font-light text-darkgray-700">
How do you build your form?
</RadioGroup.Label>
<div className="grid grid-cols-1 mt-4 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
@@ -149,7 +144,7 @@ export default function FormOnboardingModal({
? "border-transparent"
: "border-lightgray-300",
active ? "border-snoopred ring-2 ring-snoopred" : "",
"relative bg-white border rounded-lg shadow-sm p-4 flex cursor-pointer focus:outline-none"
"relative bg-white border rounded shadow-sm p-4 flex cursor-pointer focus:outline-none"
)
}
>
@@ -159,13 +154,13 @@ export default function FormOnboardingModal({
<span className="flex flex-col">
<RadioGroup.Label
as="span"
className="block font-medium text-center text-md text-darkgray-900"
className="block font-bold text-md text-darkgray-700"
>
{formType.title}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className="flex items-center mt-1 text-xs text-center whitespace-pre-wrap text-darkgray-500"
className="flex items-center mt-1 text-xs whitespace-pre-wrap text-darkgray-500"
>
{formType.description}
</RadioGroup.Description>
@@ -173,18 +168,25 @@ export default function FormOnboardingModal({
</span>
<CheckCircleIcon
className={classNames(
!checked ? "invisible" : "",
!checked ? "hidden" : "",
"h-5 w-5 text-snoopred"
)}
aria-hidden="true"
/>
<div
className={classNames(
checked ? "hidden" : "",
"h-4 w-4 rounded-full border-2"
)}
aria-hidden="true"
/>
<span
className={classNames(
active ? "border" : "border-2",
checked
? "border-snoopred"
: "border-transparent",
"absolute -inset-px rounded-lg pointer-events-none"
"absolute -inset-px rounded pointer-events-none"
)}
aria-hidden="true"
/>
@@ -195,12 +197,10 @@ export default function FormOnboardingModal({
</div>
</RadioGroup>
<div className="mt-5 sm:mt-6">
<button
type="submit"
className="inline-flex justify-center w-full px-4 py-2 text-base font-medium text-white border border-transparent rounded-md shadow-sm bg-snoopred hover:bg-snoopred-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred sm:text-sm"
>
Create form
</button>
<Button type="submit" buttonText="create form" fullwidth>
create form
<BsPlus className="w-6 h-6 ml-1"></BsPlus>
</Button>
</div>
</form>
</Transition.Child>

View File

@@ -0,0 +1,40 @@
import React from "react";
import Button from "../StandardButton.tsx";
interface Props {
children: React.ReactNode;
onClick: () => void;
alertText: string;
hintText: string;
buttonText: string;
borderStyles: string;
}
const EmptyPageFiller: React.FC<Props> = ({
children,
onClick,
alertText,
hintText,
buttonText,
borderStyles,
...rest
}) => {
return (
<div
className={
`bg-white max-w-md p-8 mx-auto mt-8 rounded-lg ` + borderStyles
}
>
{children}
<h3 className="mt-5 text-base font-bold text-lightgray-700">
{alertText}
</h3>
<p className="mt-1 text-xs font-light text-lightgray-700">{hintText}</p>
<div className="mt-6">
<Button onClick={onClick}>{buttonText}</Button>
</div>
</div>
);
};
export default EmptyPageFiller;

View File

@@ -23,7 +23,7 @@ export default function Layout({ children }) {
}
return (
<div className="min-h-screen bg-lightgray-100">
<div className="min-h-screen bg-bggray">
<Disclosure as="nav" className="bg-white shadow-sm">
{({ open }) => (
<>

7279
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,9 @@ export default async function handle(
owner: {
select: { name: true },
},
submissionSessions: {
select: { id: true },
}
},
});
res.json(formData);

View File

@@ -0,0 +1,116 @@
-- CreateEnum
CREATE TYPE "FormType" AS ENUM ('CODE', 'NOCODE');
-- CreateEnum
CREATE TYPE "PipelineType" AS ENUM ('WEBHOOK');
-- CreateTable
CREATE TABLE "Form" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"ownerId" INTEGER NOT NULL,
"formType" "FormType" NOT NULL DEFAULT E'NOCODE',
"name" TEXT NOT NULL,
"published" BOOLEAN NOT NULL DEFAULT false,
"finishedOnboarding" BOOLEAN NOT NULL DEFAULT false,
"schema" JSONB NOT NULL,
CONSTRAINT "Form_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NoCodeForm" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"formId" TEXT NOT NULL,
"pages" JSONB NOT NULL DEFAULT '[]',
"pagesDraft" JSONB NOT NULL DEFAULT '[]',
CONSTRAINT "NoCodeForm_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,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"formId" TEXT NOT NULL,
"userFingerprint" TEXT NOT NULL,
CONSTRAINT "SubmissionSession_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SessionEvent" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"submissionSessionId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"data" JSONB NOT NULL,
CONSTRAINT "SessionEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" SERIAL NOT NULL,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"password" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verification_requests" (
"id" SERIAL NOT NULL,
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "verification_requests_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "NoCodeForm_formId_key" ON "NoCodeForm"("formId");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "verification_requests_token_key" ON "verification_requests"("token");
-- AddForeignKey
ALTER TABLE "Form" ADD CONSTRAINT "Form_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NoCodeForm" ADD CONSTRAINT "NoCodeForm_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE 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;
-- AddForeignKey
ALTER TABLE "SessionEvent" ADD CONSTRAINT "SessionEvent_submissionSessionId_fkey" FOREIGN KEY ("submissionSessionId") REFERENCES "SubmissionSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -90,3 +90,14 @@
.cdx-block {
max-width: 100% !important;
}
/* Recurring Tailwind Classes */
@layer components {
.bg-snoopfade {
@apply bg-gradient-to-br from-snoopred to-pink-500 hover:to-snoopred;
}
.text-snoopfade {
@apply text-transparent bg-clip-text bg-gradient-to-br from-snoopred to-pink-500 hover:to-snoopred;
}
}

View File

@@ -4,7 +4,24 @@ module.exports = {
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
borderRadius: {
none: "0",
sm: "0.25rem", // 4px
DEFAULT: "0.5rem", //8px
DEFAULT: "8px",
md: "1rem", // 16px
lg: "1.5rem", // 24px
xl: "2rem", // 32px
"2xl": "2.5rem", // 40px
"3xl": "3rem",
"4xl": "3.5rem",
full: "9999px",
},
extend: {
strokeWidth: {
thin: "0.5px",
thinner: "0.25px",
},
colors: {
snoopred: {
DEFAULT: "#f53b57",
@@ -175,6 +192,9 @@ module.exports = {
800: "#e1b50c",
900: "#d7ab02",
},
bggray: {
DEFAULT: "#FAFAFB",
},
lightgray: {
50: "#ffffff",
100: "#faffff",