add new view for empty form list, improve onboarding screen, add basic nocode editor as basis for further development, update prisma schema and api for nocode editor

This commit is contained in:
Matthias Nannt
2022-06-15 22:14:20 +09:00
parent fd8e1f4159
commit b622510dc4
18 changed files with 546 additions and 119 deletions
+127 -78
View File
@@ -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 (
<div>
{forms && (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<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>
{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>
</li>
</button>
{forms
.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">
<Link href={`/forms/${form.id}`}>
<a>
<div className="px-4 py-5 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-600 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 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>
) : (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<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>
</li>
))}
</ul>
)}
</button>
{forms
.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">
<Link href={`/forms/${form.id}`}>
<a>
<div className="px-4 py-5 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-600 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 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>
</li>
))}
</ul>
))}
</div>
);
}
+49
View File
@@ -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 <Loading />;
}
return (
<div className="w-full bg-gray-100">
<div className="flex justify-center w-full mt-10">
<div className="w-full max-w-5xl">
<div className="grid grid-cols-1 gap-6">
{noCodeForm.pages.map((page) => (
<Page key={page.id} />
))}
</div>
<button
onClick={() => addPage()}
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 text-sm font-medium text-gray-700 border border-gray-300 border-dashed rounded-md bg-gray-50 hover:bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500"
>
+ Add Page
</button>
</div>
</div>
</div>
);
}
+38
View File
@@ -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 (
<ReactEditorJS
onInitialize={handleInitialize}
tools={EDITOR_JS_TOOLS}
minHeight={0}
/>
);
};
// Return the CustomEditor to use by other components.
export default Editor;
+12
View File
@@ -0,0 +1,12 @@
import dynamic from "next/dynamic";
let Editor = dynamic(() => import("./Editor"), {
ssr: false,
});
export default function Page({}) {
return (
<div className="w-full p-10 bg-white rounded-lg">
{Editor && <Editor />}
</div>
);
}
+7 -3
View File
@@ -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) {
+8 -3
View File
@@ -30,8 +30,13 @@ export default function Layout({ children }) {
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<div className="flex items-center flex-shrink-0">
<img src="../../snoopForms_Logo_v4.svg" alt="snoopForms logo"/>
<div className="flex items-center flex-shrink-0 w-48">
<Image
src="/img/snoopforms-logo.svg"
alt="snoopForms logo"
width={300}
height={53}
/>
</div>
</div>
<div className="hidden sm:ml-6 sm:flex sm:items-center">
@@ -86,7 +91,7 @@ export default function Layout({ children }) {
</div>
<div className="flex items-center -mr-2 sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center p-2 text-darkgray-400 bg-white rounded-md hover:text-darkgray-500 hover:bg-lightgray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred">
<Disclosure.Button className="inline-flex items-center justify-center p-2 bg-white rounded-md text-darkgray-400 hover:text-darkgray-500 hover:bg-lightgray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred">
<span className="sr-only">Open main menu</span>
{open ? (
<XIcon className="block w-6 h-6" aria-hidden="true" />
+63
View File
@@ -0,0 +1,63 @@
import { signIn, useSession } from "next-auth/react";
import Head from "next/head";
import Loading from "../Loading";
import MenuBreadcrumbs from "./MenuBreadcrumbs";
import MenuProfile from "./MenuProfile";
import MenuSteps from "./MenuSteps";
export default function LayoutFormResults({
title,
formId,
currentStep,
children,
}) {
const { data: session, status } = useSession();
if (status === "loading") {
return <Loading />;
}
if (!session) {
signIn();
return <div>You need to be authenticated to view this page.</div>;
}
return (
<>
<Head>
<title>{title}</title>
</Head>
<div className="flex min-h-screen overflow-hidden bg-gray-100">
<div className="flex flex-col flex-1 overflow-hidden">
<header className="w-full">
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-lightgray-200">
<div className="flex flex-1 px-4 sm:px-6">
<MenuBreadcrumbs formId={formId} />
<MenuSteps formId={formId} currentStep={currentStep} />
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:ml-6 sm:space-x-4">
{/* Profile dropdown */}
<MenuProfile />
</div>
</div>
</div>
<div className="relative z-10 flex flex-shrink-0 h-16 border-b border-gray-200 shadow-inner bg-gray-50">
<div className="flex items-center justify-center flex-1 px-4">
<nav className="flex space-x-4" aria-label="resultModes">
<button
onClick={() => {}}
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
>
Save
</button>
</nav>
</div>
</div>
</header>
{/* Main content */}
{children}
</div>
</div>
</>
);
}