mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-25 18:48:58 -06:00
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:
@@ -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't have any forms yet.
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
It'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
components/builder/Builder.tsx
Normal file
49
components/builder/Builder.tsx
Normal 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
components/builder/Editor.tsx
Normal file
38
components/builder/Editor.tsx
Normal 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
components/builder/Page.tsx
Normal file
12
components/builder/Page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
components/layout/LayoutFormBuilder.tsx
Normal file
63
components/layout/LayoutFormBuilder.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
lib/noCodeForm.ts
Normal file
32
lib/noCodeForm.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "./utils";
|
||||
|
||||
export const useNoCodeForm = (formId) => {
|
||||
const { data, error, mutate } = useSWR(
|
||||
`/api/forms/${formId}/nocodeform`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
noCodeForm: data,
|
||||
isLoadingNoCodeForm: !error && !data,
|
||||
isErrorNoCodeForm: error,
|
||||
mutateNoCodeForm: mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const createNoCodeForm = async (formId) => {
|
||||
try {
|
||||
const res = await fetch(`/api/forms/${formId}/nocodeform`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
return await res.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw Error(
|
||||
`createNoCodeForm: unable to create noCodeForm: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,8 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@editorjs/editorjs": "^2.24.3",
|
||||
"@editorjs/paragraph": "^2.8.0",
|
||||
"@headlessui/react": "^1.6.1",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@prisma/client": "^3.15.1",
|
||||
@@ -22,7 +24,7 @@
|
||||
"nodemailer": "^6.7.5",
|
||||
"react": "18.1.0",
|
||||
"react-dom": "18.1.0",
|
||||
"react-feather": "^2.0.9",
|
||||
"react-editor-js": "^2.0.6",
|
||||
"react-icons": "^4.4.0",
|
||||
"superjson": "^1.9.1",
|
||||
"swr": "^1.3.0"
|
||||
|
||||
54
pages/api/forms/[id]/nocodeform/index.ts
Normal file
54
pages/api/forms/[id]/nocodeform/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { NextApiResponse, NextApiRequest } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { formHasOwnership } from "../../../../../lib/api";
|
||||
import { prisma } from "../../../../../lib/prisma";
|
||||
|
||||
export default async function handle(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
// Check Authentication
|
||||
const session = await getSession({ req: req });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
const formId = req.query.id.toString();
|
||||
|
||||
// GET /api/forms/:id/nocodeform
|
||||
// Get noCodeForm for a form with specific id
|
||||
if (req.method === "GET") {
|
||||
const data = await prisma.noCodeForm.findUnique({
|
||||
where: {
|
||||
formId: formId,
|
||||
},
|
||||
});
|
||||
return res.json(data);
|
||||
}
|
||||
// POST /api/forms/:id/nocodeform
|
||||
// Updates an existing nocodeform
|
||||
// Required fields in body: -
|
||||
// Optional fields in body: title, published, finishedOnboarding, elements, elementsDraft
|
||||
else if (req.method === "POST") {
|
||||
const ownership = await formHasOwnership(session, formId);
|
||||
if (!ownership) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ message: "You are not authorized to change this noCodeForm" });
|
||||
}
|
||||
const data = { ...req.body, updatedAt: new Date() };
|
||||
// create or update record
|
||||
const prismaRes = await prisma.noCodeForm.upsert({
|
||||
where: { formId },
|
||||
update: data,
|
||||
create: { form: { connect: { id: formId } } },
|
||||
});
|
||||
return res.json(prismaRes);
|
||||
}
|
||||
// Unknown HTTP Method
|
||||
else {
|
||||
throw new Error(
|
||||
`The HTTP ${req.method} method is not supported by this route.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { getCsrfToken } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { XCircleIcon } from "@heroicons/react/solid";
|
||||
import { GetServerSideProps } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
interface props {
|
||||
csrfToken: string;
|
||||
@@ -34,9 +35,14 @@ export default function SignIn({ csrfToken }: props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full bg-white rounded-xl p-8 shadow-cont max-w-sm mx-auto lg:w-96">
|
||||
<div className="w-full max-w-sm p-8 mx-auto bg-white rounded-xl shadow-cont lg:w-96">
|
||||
<div>
|
||||
<img src="../../snoopForms_Logo_v4.svg" alt="snoopForms logo"/>
|
||||
<Image
|
||||
src="/img/snoopforms-logo.svg"
|
||||
alt="snoopForms logo"
|
||||
width={500}
|
||||
height={89}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,12 +97,18 @@ export default function SignIn({ csrfToken }: props) {
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-snoopred border border-transparent rounded-md shadow-sm hover:bg-snoopred-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred-500"
|
||||
className="flex justify-center w-full px-4 py-2 text-sm 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-500"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<a href="" className="text-snoopred hover:text-snoopred-600 text-xs">Create an account</a></div>
|
||||
<a
|
||||
href=""
|
||||
className="text-xs text-snoopred hover:text-snoopred-600"
|
||||
>
|
||||
Create an account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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 <Loading />;
|
||||
}
|
||||
|
||||
if (!form.finishedOnboarding) {
|
||||
router.push(`/forms/${formId}/welcome`);
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
if (form.formType === "NOCODE") {
|
||||
return (
|
||||
<>
|
||||
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
|
||||
<FormOnboardingModal
|
||||
open={openOnboardingModal}
|
||||
setOpen={setOpenOnboardingModal}
|
||||
formId={formId}
|
||||
/>
|
||||
</LayoutFormBasics>
|
||||
<LayoutFormBuilder
|
||||
title={form.title}
|
||||
formId={formId}
|
||||
currentStep="form"
|
||||
>
|
||||
<Builder formId={formId} />
|
||||
</LayoutFormBuilder>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
@@ -41,11 +39,6 @@ export default function FormPage() {
|
||||
<>
|
||||
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
|
||||
<FormCode />
|
||||
<FormOnboardingModal
|
||||
open={openOnboardingModal}
|
||||
setOpen={setOpenOnboardingModal}
|
||||
formId={formId}
|
||||
/>
|
||||
</LayoutFormBasics>
|
||||
</>
|
||||
);
|
||||
|
||||
43
pages/forms/[id]/welcome.tsx
Normal file
43
pages/forms/[id]/welcome.tsx
Normal file
@@ -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 <Loading />;
|
||||
}
|
||||
|
||||
if (!form.finishedOnboarding) {
|
||||
return (
|
||||
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
|
||||
<FormOnboardingModal open={openOnboardingModal} formId={formId} />
|
||||
</LayoutFormBasics>
|
||||
);
|
||||
} else {
|
||||
router.push(`/forms/${formId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
BIN
public/img/mascot-face-small.png
Normal file
BIN
public/img/mascot-face-small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
58
yarn.lock
58
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"
|
||||
|
||||
Reference in New Issue
Block a user