diff --git a/components/FormList.tsx b/components/FormList.tsx index f9b7c9f8f5..78fa153112 100644 --- a/components/FormList.tsx +++ b/components/FormList.tsx @@ -2,17 +2,23 @@ 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, TrashIcon } from "@heroicons/react/solid"; +import { + DotsHorizontalIcon, + PlusIcon, + TrashIcon, +} from "@heroicons/react/solid"; import { classNames } from "../lib/utils"; import { createForm, useForms } from "../lib/forms"; +import Image from "next/image"; export default function FormList() { const { forms, mutateForms } = useForms(); const newForm = async () => { const form = await createForm(); - await Router.push(`/forms/${form.id}/form`); + await Router.push(`/forms/${form.id}/welcome`); }; const deleteForm = async (form, formIdx) => { @@ -30,85 +36,128 @@ export default function FormList() { }; return (
- {forms && ( -
- - - {forms - .sort((a, b) => b.updatedAt - a.updatedAt) - .map((form, formIdx) => ( -
  • -
    - - -
    {form.name}
    -
    - -
    - - {({ open }) => ( - <> -
    - - Open options - -
    - - - -
    - - {({ active }) => ( - - )} - -
    -
    -
    - - )} -
    -
    +
    + + ) : ( + - )} + + {forms + .sort((a, b) => b.updatedAt - a.updatedAt) + .map((form, formIdx) => ( +
  • +
    + + +
    {form.name}
    +
    + +
    + + {({ open }) => ( + <> +
    + + Open options + +
    + + + +
    + + {({ active }) => ( + + )} + +
    +
    +
    + + )} +
    +
    +
    +
  • + ))} + + ))} ); } diff --git a/components/builder/Builder.tsx b/components/builder/Builder.tsx new file mode 100644 index 0000000000..452ac2036f --- /dev/null +++ b/components/builder/Builder.tsx @@ -0,0 +1,49 @@ +import { useCallback, useEffect } from "react"; +import { v4 as uuidv4 } from "uuid"; +import { useNoCodeForm } from "../../lib/noCodeForm"; +import Loading from "../Loading"; +import Page from "./Page"; + +export default function Builder({ formId }) { + const { noCodeForm, mutateNoCodeForm, isLoadingNoCodeForm } = + useNoCodeForm(formId); + + const addPage = useCallback(() => { + if (noCodeForm) { + const updatedNCF = JSON.parse(JSON.stringify(noCodeForm)); + updatedNCF.pages.push({ + id: uuidv4(), + elements: [], + }); + mutateNoCodeForm(updatedNCF, false); + } + }, [mutateNoCodeForm, noCodeForm]); + + useEffect(() => { + if (noCodeForm && noCodeForm.pages.length === 0) addPage(); + }, [noCodeForm]); + + if (isLoadingNoCodeForm) { + return ; + } + + return ( +
    +
    +
    +
    + {noCodeForm.pages.map((page) => ( + + ))} +
    + +
    +
    +
    + ); +} diff --git a/components/builder/Editor.tsx b/components/builder/Editor.tsx new file mode 100644 index 0000000000..3f5b408b13 --- /dev/null +++ b/components/builder/Editor.tsx @@ -0,0 +1,38 @@ +import { useCallback, useRef } from "react"; +import { createReactEditorJS } from "react-editor-js"; + +const ReactEditorJS = createReactEditorJS(); + +const Editor = ({}) => { + const editorCore = useRef(null); + + const handleInitialize = useCallback((instance) => { + editorCore.current = instance; + }, []); + + /* const handleSave = useCallback(async () => { + const savedData = await editorCore.current.save(); + console.log(savedData); + }, []); + + setTimeout(() => { + // save every ten seconds + handleSave(); + }, 10000); */ + + const EDITOR_JS_TOOLS = {}; + + // Editor.js This will show block editor in component + // pass EDITOR_JS_TOOLS in tools props to configure tools with editor.js + return ( + + ); +}; + +// Return the CustomEditor to use by other components. + +export default Editor; diff --git a/components/builder/Page.tsx b/components/builder/Page.tsx new file mode 100644 index 0000000000..45dcdbbc1f --- /dev/null +++ b/components/builder/Page.tsx @@ -0,0 +1,12 @@ +import dynamic from "next/dynamic"; +let Editor = dynamic(() => import("./Editor"), { + ssr: false, +}); + +export default function Page({}) { + return ( +
    + {Editor && } +
    + ); +} diff --git a/components/form/FormOnboardingModal.tsx b/components/form/FormOnboardingModal.tsx index ccea7a0012..041c0678e6 100644 --- a/components/form/FormOnboardingModal.tsx +++ b/components/form/FormOnboardingModal.tsx @@ -1,8 +1,10 @@ /* This example requires Tailwind CSS v2.0+ */ import { Dialog, RadioGroup, Transition } from "@headlessui/react"; import { CheckCircleIcon, LightBulbIcon } 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"; @@ -23,15 +25,14 @@ const formTypes = [ type FormOnboardingModalProps = { open: boolean; - setOpen: (o: boolean) => void; formId: string; }; export default function FormOnboardingModal({ open, - setOpen, formId, }: FormOnboardingModalProps) { + const router = useRouter(); const { form, mutateForm, isLoadingForm } = useForm(formId); const [name, setName] = useState(form.name); const [formType, setFormType] = useState(formTypes[0]); @@ -46,7 +47,10 @@ export default function FormOnboardingModal({ }; await persistForm(updatedForm); mutateForm(updatedForm); - setOpen(false); + if (updatedForm.formType === "NOCODE") { + await createNoCodeForm(formId); + } + router.push(`/forms/${formId}/form`); }; if (isLoadingForm) { diff --git a/components/layout/LayoutBasic.tsx b/components/layout/LayoutBasic.tsx index 73f8f5f05c..b7530b2b08 100644 --- a/components/layout/LayoutBasic.tsx +++ b/components/layout/LayoutBasic.tsx @@ -30,8 +30,13 @@ export default function Layout({ children }) {
    -
    - snoopForms logo +
    + snoopForms logo
    @@ -86,7 +91,7 @@ export default function Layout({ children }) {
    {/* Mobile menu button */} - + Open main menu {open ? (
    )} -
    +
    - snoopForms logo + snoopForms logo
    @@ -65,7 +71,7 @@ export default function SignIn({ csrfToken }: props) { type="email" autoComplete="email" required - className="block w-full px-3 py-2 placeholder-lightgray-400 border border-lightgray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm" + className="block w-full px-3 py-2 border rounded-md shadow-sm appearance-none placeholder-lightgray-400 border-lightgray-300 focus:outline-none focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm" />
    @@ -83,7 +89,7 @@ export default function SignIn({ csrfToken }: props) { type="password" autoComplete="current-password" required - className="block w-full px-3 py-2 placeholder-lightgray-400 border border-lightgray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm" + className="block w-full px-3 py-2 border rounded-md shadow-sm appearance-none placeholder-lightgray-400 border-lightgray-300 focus:outline-none focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm" />
    @@ -91,12 +97,18 @@ export default function SignIn({ csrfToken }: props) {
    + + Create an account + +
    diff --git a/pages/forms/[id]/form.tsx b/pages/forms/[id]/form.tsx index 64b9afba57..b966ed9071 100644 --- a/pages/forms/[id]/form.tsx +++ b/pages/forms/[id]/form.tsx @@ -1,10 +1,10 @@ import { GetServerSideProps } from "next"; import { getSession } from "next-auth/react"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import Builder from "../../../components/builder/Builder"; import FormCode from "../../../components/form/FormCode"; -import FormOnboardingModal from "../../../components/form/FormOnboardingModal"; import LayoutFormBasics from "../../../components/layout/LayoutFormBasic"; +import LayoutFormBuilder from "../../../components/layout/LayoutFormBuilder"; import Loading from "../../../components/Loading"; import { useForm } from "../../../lib/forms"; @@ -12,28 +12,26 @@ export default function FormPage() { const router = useRouter(); const formId = router.query.id.toString(); const { form, isLoadingForm } = useForm(router.query.id); - const [openOnboardingModal, setOpenOnboardingModal] = useState(false); - - useEffect(() => { - if (form && !form.finishedOnboarding) { - setOpenOnboardingModal(true); - } - }, [isLoadingForm]); if (isLoadingForm) { return ; } + if (!form.finishedOnboarding) { + router.push(`/forms/${formId}/welcome`); + return
    ; + } + if (form.formType === "NOCODE") { return ( <> - - - + + + ); } else { @@ -41,11 +39,6 @@ export default function FormPage() { <> - ); diff --git a/pages/forms/[id]/welcome.tsx b/pages/forms/[id]/welcome.tsx new file mode 100644 index 0000000000..1ec31b658b --- /dev/null +++ b/pages/forms/[id]/welcome.tsx @@ -0,0 +1,43 @@ +import { GetServerSideProps } from "next"; +import { getSession } from "next-auth/react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import FormOnboardingModal from "../../../components/form/FormOnboardingModal"; +import LayoutFormBasics from "../../../components/layout/LayoutFormBasic"; +import Loading from "../../../components/Loading"; +import { useForm } from "../../../lib/forms"; + +export default function WelcomePage() { + const router = useRouter(); + const formId = router.query.id.toString(); + const { form, isLoadingForm } = useForm(router.query.id); + const [openOnboardingModal, setOpenOnboardingModal] = useState(false); + + useEffect(() => { + if (form && !form.finishedOnboarding) { + setOpenOnboardingModal(true); + } + }, [isLoadingForm]); + + if (isLoadingForm) { + return ; + } + + if (!form.finishedOnboarding) { + return ( + + + + ); + } else { + router.push(`/forms/${formId}`); + } +} + +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await getSession({ req }); + if (!session) { + res.statusCode = 403; + } + return { props: {} }; +}; diff --git a/prisma/migrations/20220614082103_init/migration.sql b/prisma/migrations/20220615062843_init/migration.sql similarity index 85% rename from prisma/migrations/20220614082103_init/migration.sql rename to prisma/migrations/20220615062843_init/migration.sql index 4881278485..ebe1fadfab 100644 --- a/prisma/migrations/20220614082103_init/migration.sql +++ b/prisma/migrations/20220615062843_init/migration.sql @@ -19,6 +19,17 @@ CREATE TABLE "Form" ( 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 '[]', + + CONSTRAINT "NoCodeForm_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "Pipeline" ( "id" TEXT NOT NULL, @@ -79,6 +90,9 @@ CREATE TABLE "verification_requests" ( 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"); @@ -88,6 +102,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 "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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ccb23c17c4..bfaedb1cb8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,6 +29,16 @@ model Form { schema Json submissionSessions SubmissionSession[] pipelines Pipeline[] + noCodeForm NoCodeForm? +} + +model NoCodeForm { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + form Form @relation(fields: [formId], references: [id], onDelete: Cascade) + formId String @unique + pages Json @default("[]") } model Pipeline { diff --git a/public/img/mascot-face-small.png b/public/img/mascot-face-small.png new file mode 100644 index 0000000000..650acb06a5 Binary files /dev/null and b/public/img/mascot-face-small.png differ diff --git a/public/snoopForms_Logo_v4.svg b/public/img/snoopforms-logo.svg similarity index 100% rename from public/snoopForms_Logo_v4.svg rename to public/img/snoopforms-logo.svg diff --git a/yarn.lock b/yarn.lock index 94139d6898..e795e6f70f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,6 +44,20 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@editorjs/editorjs@^2.24.3": + version "2.24.3" + resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.24.3.tgz#60ee6dd37d57b870ef29754355d77f9c61f30e79" + integrity sha512-VzrWaQ7mggNUAPTDGcqXJNIlBZH3S2IqsIUGA43UM2Q9VFaeS5KuVFVOTrFJvAzF7G+vZTO52ocm+hrDhTwvyw== + dependencies: + codex-notifier "^1.1.2" + codex-tooltip "^1.0.5" + nanoid "^3.1.22" + +"@editorjs/paragraph@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@editorjs/paragraph/-/paragraph-2.8.0.tgz#11cc381fcafaf8b9160517ce65d59eee93fc4af9" + integrity sha512-z6w5ZR0ru3p/IjxJW/tb7OcSnVttkZukQMIsnBMX1FIKc1BNdr7NwM1YoCyTl4OnC90YfL0xgES6/20/W267pw== + "@eslint/eslintrc@^1.2.3": version "1.3.0" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" @@ -216,6 +230,25 @@ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#f691893df506b93e3cb1ccc15ec6e5ac64e8e570" integrity sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg== +"@react-editor-js/client@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@react-editor-js/client/-/client-2.0.6.tgz#be9a2704b58322bc37dc6d2acb014f0ff28fe43c" + integrity sha512-LMMJLAXAwk1kVMy7fxTRFK6OdouvoseqJbmVUygJb2EcfuT84nC9OAtvGEL4vsVLUcnzEV400+F9t5OKa77FGQ== + dependencies: + "@react-editor-js/core" "2.0.6" + +"@react-editor-js/core@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@react-editor-js/core/-/core-2.0.6.tgz#3f20c0668d1f8502489ed7e354ff26461b270dce" + integrity sha512-mvHM2I+gT3AnvFpFhTZI0EFLKD9pRpgXDf286uwv6n6tngwLfnCCmtCbgiGI9ICph2GJvRZfaQubE+MHQ6YV8g== + +"@react-editor-js/server@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@react-editor-js/server/-/server-2.0.6.tgz#237f11002b4db9fe754fd9a89ff76f131f8a21fb" + integrity sha512-soW/bV5auciYr8gEYISWK4fuIblAcc4bcwPuCKnDBj9W9r/nAxMmNgCG+z9rs9Gnroa0Ko3Hzwzs9d5MdOShzg== + dependencies: + "@react-editor-js/core" "2.0.6" + "@rushstack/eslint-patch@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz#6801033be7ff87a6b7cadaf5b337c9f366a3c4b0" @@ -581,6 +614,16 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +codex-notifier@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895" + integrity sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg== + +codex-tooltip@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.5.tgz#ba25fd5b3a58ba2f73fd667c2b46987ffd1edef2" + integrity sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1567,7 +1610,7 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.1.30, nanoid@^3.3.4: +nanoid@^3.1.22, nanoid@^3.1.30, nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== @@ -1908,7 +1951,7 @@ prisma@^3.15.1: dependencies: "@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -1940,12 +1983,13 @@ react-dom@18.1.0: loose-envify "^1.1.0" scheduler "^0.22.0" -react-feather@^2.0.9: - version "2.0.10" - resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.10.tgz#0e9abf05a66754f7b7bb71757ac4da7fb6be3b68" - integrity sha512-BLhukwJ+Z92Nmdcs+EMw6dy1Z/VLiJTzEQACDUEnWMClhYnFykJCGWQx+NmwP/qQHGX/5CzQ+TGi8ofg2+HzVQ== +react-editor-js@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/react-editor-js/-/react-editor-js-2.0.6.tgz#34771596986d79513e12e5f4990da46b9e0f2430" + integrity sha512-8u47IbhExiFB2kGNdJYlsX5iVlSzac38A3oJ7bmnTz3Lp7Slys1xreoYdG71+KiOcfX0dEgOIavV4e6TJeB5eg== dependencies: - prop-types "^15.7.2" + "@react-editor-js/client" "2.0.6" + "@react-editor-js/server" "2.0.6" react-icons@^4.4.0: version "4.4.0"