from pull

This commit is contained in:
knugget
2022-06-21 11:25:13 -05:00
parent f9fd2ba263
commit 52f1fd5fa2
29 changed files with 8563 additions and 464 deletions
+52
View File
@@ -0,0 +1,52 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { TailSpin } from "react-loader-spinner";
export default function LoadingModal({ isLoading }) {
return (
<Transition.Root show={isLoading} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 z-10 overflow-y-auto"
open={isLoading}
onClose={() => {}}
>
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-20" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-flex items-center justify-center px-4 py-20 pb-4 overflow-hidden text-left align-bottom transition-all transform rounded-lg sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
<TailSpin color="#000" height={50} width={50} />
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
}
+97 -29
View File
@@ -1,49 +1,117 @@
import { useCallback, useEffect } from "react";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { useNoCodeForm } from "../../lib/noCodeForm";
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
import Loading from "../Loading";
import Page from "./Page";
import ShareModal from "./ShareModal";
import UsageIntro from "./UsageIntro";
export default function Builder({ formId }) {
const { noCodeForm, mutateNoCodeForm, isLoadingNoCodeForm } =
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
useNoCodeForm(formId);
const [isInitialized, setIsInitialized] = useState(false);
const [openShareModal, setOpenShareModal] = useState(false);
const addPage = useCallback(() => {
if (noCodeForm) {
const updatedNCF = JSON.parse(JSON.stringify(noCodeForm));
updatedNCF.pages.push({
id: uuidv4(),
elements: [],
});
mutateNoCodeForm(updatedNCF, false);
const addPage = useCallback(async () => {
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.pagesDraft.push({
id: uuidv4(),
blocks: [],
});
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
}, [noCodeForm, mutateNoCodeForm]);
const deletePage = async (pageIdx) => {
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.pagesDraft.splice(pageIdx, 1);
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
};
const initPages = useCallback(async () => {
if (!isLoadingNoCodeForm && !isInitialized) {
if (noCodeForm.pagesDraft.length === 0) {
await addPage();
}
setIsInitialized(true);
}
}, [mutateNoCodeForm, noCodeForm]);
}, [isLoadingNoCodeForm, noCodeForm, addPage, isInitialized]);
const publishChanges = async () => {
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.pages = newNoCodeForm.pagesDraft;
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
setOpenShareModal(true);
};
useEffect(() => {
if (noCodeForm && noCodeForm.pages.length === 0) addPage();
}, [noCodeForm]);
initPages();
}, [isLoadingNoCodeForm, initPages]);
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 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={() => addPage()}
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
>
Add Page
</button>
<Link href={`/forms/${formId}/preview`}>
<a className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600">
Preview Form
</a>
</Link>
<button
onClick={() => publishChanges()}
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
>
Publish
</button>
<button
onClick={() => setOpenShareModal(true)}
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
>
Share
</button>
</nav>
</div>
</div>
</div>
<div className="w-full bg-gray-100">
<div className="flex justify-center w-full mt-10">
<div className="w-full px-4 max-w-7xl">
<div className="grid grid-cols-1 gap-6">
<div className="px-10">
<UsageIntro />
</div>
{noCodeForm.pagesDraft.map((page, pageIdx) => (
<Page
key={page.id}
formId={formId}
page={page}
pageIdx={pageIdx}
deletePageAction={deletePage}
/>
))}
</div>
</div>
</div>
</div>
<ShareModal
open={openShareModal}
setOpen={setOpenShareModal}
formId={formId}
/>
</>
);
}
-38
View File
@@ -1,38 +0,0 @@
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;
+52 -4
View File
@@ -1,12 +1,60 @@
import { TrashIcon } from "@heroicons/react/outline";
import dynamic from "next/dynamic";
let Editor = dynamic(() => import("./Editor"), {
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
import Loading from "../Loading";
let Editor = dynamic(() => import("../editorjs/Editor"), {
ssr: false,
});
export default function Page({}) {
export default function Page({ formId, page, pageIdx, deletePageAction }) {
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
useNoCodeForm(formId);
const updatePage = async (blocks) => {
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
if (pageIdx < newNoCodeForm.pagesDraft.length) {
newNoCodeForm.pagesDraft[pageIdx].blocks = blocks;
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
} else {
throw Error(
`updatePage error: Page at position ${pageIdx} not found in pagesDraft`
);
}
};
if (isLoadingNoCodeForm) {
return <Loading />;
}
return (
<div className="w-full p-10 bg-white rounded-lg">
{Editor && <Editor />}
<div className="flex w-full">
<div className="flex w-8">
{pageIdx !== 0 && (
<button
className="flex items-center h-full text-gray-400"
onClick={() => {
if (confirm("Do you really want to delete this page?")) {
deletePageAction(pageIdx);
}
}}
>
<TrashIcon className="w-5 h-5" />
</button>
)}
</div>
<div className="relative w-full p-10 bg-white rounded-lg">
<div className="relative">
{Editor && (
<Editor
id={`${page.id}-editor`}
autofocus={pageIdx === 0}
onChange={(blocks) => updatePage(blocks)}
value={noCodeForm.pagesDraft[pageIdx]}
/>
)}
</div>
</div>
</div>
);
}
+90
View File
@@ -0,0 +1,90 @@
/* This example requires Tailwind CSS v2.0+ */
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/outline";
import { Fragment } from "react";
export default function ShareModal({ open, setOpen, formId }) {
const getPublicFormUrl = () => {
if (process.browser) {
return `${window.location.protocol}//${window.location.host}/f/${formId}/`;
}
};
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative px-4 pt-5 pb-4 overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:max-w-4xl sm:w-full sm:p-6">
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-snoopred-500"
onClick={() => setOpen(false)}
>
<span className="sr-only">Close</span>
<XIcon className="w-6 h-6" aria-hidden="true" />
</button>
</div>
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium leading-6 text-gray-900">
Share your form
</h3>
<div className="max-w-xl mt-2 text-sm text-gray-500">
<p>
Let your participants fill out your form by accessing it
via the public link.
</p>
</div>
<div className="mt-5 sm:flex sm:items-center">
<div className="w-full sm:max-w-xs">
<label htmlFor="surveyLink" className="sr-only">
Public link
</label>
<input
id="surveyLink"
type="text"
placeholder="Enter your email"
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm"
value={getPublicFormUrl()}
disabled
/>
</div>
<button
onClick={() => {
navigator.clipboard.writeText(getPublicFormUrl());
}}
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 font-medium text-white bg-gray-800 border border-transparent rounded-md shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Copy
</button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
+36
View File
@@ -0,0 +1,36 @@
/* This example requires Tailwind CSS v2.0+ */
import { InformationCircleIcon } from "@heroicons/react/solid";
import { useState } from "react";
export default function UsageIntro() {
const [dismissed, setDismissed] = useState(false);
return (
!dismissed && (
<div className="p-4 bg-gray-100 border border-gray-700 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="w-5 h-5 text-blue-400"
aria-hidden="true"
/>
</div>
<div className="flex-1 ml-3 md:flex md:justify-between">
<p className="text-sm text-gray-700">
Welcome to the snoopForms No-Code Editor. Use &apos;tab&apos; to
add new blocks or change their options. You can also drag &apos;n
drop blocks to reorder them.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<a
onClick={() => setDismissed(true)}
className="font-medium text-gray-700 whitespace-nowrap hover:text-gray-600"
>
Dismiss
</a>
</p>
</div>
</div>
</div>
)
);
}
+58
View File
@@ -0,0 +1,58 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Fragment, useEffect, useRef, useState } from "react";
import EditorJS from "@editorjs/editorjs";
import DragDrop from "editorjs-drag-drop";
import Undo from "editorjs-undo";
import TextQuestion from "./tools/TextQuestion";
import SubmitButton from "./tools/SubmitButton";
const Editor = ({ id, autofocus = false, onChange, value }) => {
const [blocks, setBlocks] = useState([]);
const ejInstance = useRef<EditorJS | null>();
useEffect(() => {
onChange(blocks);
}, [blocks]);
// This will run only once
useEffect(() => {
if (!ejInstance.current) {
initEditor();
}
return () => {
destroyEditor();
};
async function destroyEditor() {
await ejInstance.current.isReady;
ejInstance.current.destroy();
ejInstance.current = null;
}
}, []);
const initEditor = () => {
const editor = new EditorJS({
minHeight: 0,
holder: id,
data: value,
onReady: () => {
ejInstance.current = editor;
new DragDrop(editor);
new Undo({ editor });
},
onChange: async () => {
let content = await editor.saver.save();
setBlocks(content.blocks);
},
autofocus: autofocus,
tools: { textQuestion: TextQuestion, submitButton: SubmitButton },
});
};
return (
<Fragment>
<div id={id} />
</Fragment>
);
};
export default Editor;
@@ -0,0 +1,58 @@
import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs";
import { ArrowRightIcon } from "@heroicons/react/solid";
import ReactDOM from "react-dom";
//styles imports in angular.json
interface TextQuestionData extends BlockToolData {
latexString: string;
}
export default class TextQuestion implements BlockTool {
label: string;
placeholder: string;
api: API;
static get toolbox(): { icon: string; title?: string } {
return {
icon: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M18.1 15.3C18 15.4 17.8 15.5 17.7 15.6L15.3 16L17 19.6C17.2 20 17 20.4 16.6 20.6L13.8 21.9C13.7 22 13.6 22 13.5 22C13.2 22 12.9 21.8 12.8 21.6L11.2 18L9.3 19.5C9.2 19.6 9 19.7 8.8 19.7C8.4 19.7 8 19.4 8 18.9V7.5C8 7 8.3 6.7 8.8 6.7C9 6.7 9.2 6.8 9.3 6.9L18 14.3C18.3 14.5 18.4 15 18.1 15.3M6 12H4V4H20V12H18.4L20.6 13.9C21.4 13.6 21.9 12.9 21.9 12V4C21.9 2.9 21 2 19.9 2H4C2.9 2 2 2.9 2 4V12C2 13.1 2.9 14 4 14H6V12Z" />`,
title: "Submit Button",
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: TextQuestionData;
}) {
this.label = data.label || "Submit";
this.placeholder = data.placeholder;
}
save(block: HTMLDivElement) {
return {
label: (block.firstElementChild.firstElementChild as HTMLInputElement)
.innerHTML,
};
}
render(): HTMLElement {
const container = document.createElement("div");
const toolView = (
<div className="inline-flex items-center px-4 py-2 pb-3 text-sm font-medium text-white bg-gray-700 border border-transparent rounded-md shadow-sm hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<div
contentEditable
id="label"
defaultValue={this.label}
className="p-0 bg-transparent border-transparent ring-0 active:ring-0 focus:border-transparent focus:ring-0 focus:outline-none"
>
{this.label}
</div>
<ArrowRightIcon className="w-5 h-5 ml-2 -mr-1" aria-hidden="true" />
</div>
);
ReactDOM.render(toolView, container);
return container;
}
}
@@ -0,0 +1,68 @@
import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs";
import ReactDOM from "react-dom";
//styles imports in angular.json
interface TextQuestionData extends BlockToolData {
latexString: string;
}
export default class TextQuestion implements BlockTool {
label: string;
placeholder: string;
api: API;
static get toolbox(): { icon: string; title?: string } {
return {
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-justify"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>`,
title: "Text Question",
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: TextQuestionData;
}) {
this.label = data.label;
this.placeholder = data.placeholder;
}
save(block: HTMLDivElement) {
return {
label: (block.firstElementChild.firstElementChild as HTMLInputElement)
.value,
placeholder: (
block.firstElementChild.lastElementChild as HTMLInputElement
).value,
};
}
renderSettings(): HTMLElement {
return document.createElement("div");
}
render(): HTMLElement {
const container = document.createElement("div");
const toolView = (
<div className="pb-3">
<input
type="text"
id="label"
defaultValue={this.label}
className="block w-full p-0 text-base font-medium text-gray-700 border-0 border-transparent ring-0 focus:ring-0"
placeholder="Your Question"
/>
<input
type="text"
className="block w-full mt-1 text-gray-400 border-gray-300 rounded-md shadow-sm sm:text-base placeholder:text-gray-300"
placeholder="optional placeholder"
defaultValue={this.placeholder}
/>
</div>
);
ReactDOM.render(toolView, container);
return container;
}
}
+27 -1
View File
@@ -35,7 +35,7 @@ const libs = [
},
];
export default function FormCode() {
export default function FormCode({ formId }) {
const [selectedLib, setSelectedLib] = useState(null);
return (
@@ -54,6 +54,32 @@ export default function FormCode() {
programming language or framework.
</p>
</div>
<hr className="my-5 text-gray-600" />
<div>
<label htmlFor="formId" className="block text-base text-gray-800">
Your form ID
</label>
<div className="mt-5 sm:flex sm:items-center">
<div className="w-full sm:max-w-xs">
<input
id="formId"
type="text"
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm disabled:bg-gray-100"
value={formId}
disabled
/>
</div>
<button
onClick={() => {
navigator.clipboard.writeText(formId);
}}
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 font-medium text-white bg-gray-800 border border-transparent rounded-md shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Copy
</button>
</div>
</div>
<hr className="my-5 text-gray-600 " />
<div className="mt-5">
<RadioGroup value={selectedLib} onChange={setSelectedLib}>
<RadioGroup.Label className="text-xs font-medium tracking-wide text-gray-500 uppercase">
+62
View File
@@ -0,0 +1,62 @@
import { SnoopElement, SnoopForm, SnoopPage } from "@snoopforms/react";
import { useMemo } from "react";
import { useNoCodeForm } from "../../lib/noCodeForm";
import Loading from "../Loading";
export default function App({ id = "", formId, draft = false }) {
const { noCodeForm, isLoadingNoCodeForm } = useNoCodeForm(formId);
const pages = useMemo(() => {
if (!isLoadingNoCodeForm) {
return noCodeForm[draft ? "pagesDraft" : "pages"];
}
}, [draft, isLoadingNoCodeForm, noCodeForm]);
if (!pages) {
return <Loading />;
}
return (
<div className="w-full px-5 py-5">
<SnoopForm
key={id} // used to reset form
domain={window.location.host}
protocol={window.location.protocol === "http:" ? "http" : "https"}
formId={formId}
localOnly={draft}
className="w-full max-w-3xl mx-auto space-y-6"
>
{pages.map((page) => (
<SnoopPage key={page.id} name={page.id}>
{page.blocks.map((block) =>
block.type === "paragraph" ? (
<p>{block.data.text}</p>
) : block.type === "textQuestion" ? (
<SnoopElement
type="text"
name={"name"}
label={block.data.label}
classNames={{
label: "mt-4 block text-sm font-medium text-gray-800",
element:
"flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300",
}}
required
/>
) : block.type === "submitButton" ? (
<SnoopElement
name="submit"
type="submit"
label={block.data.label}
classNames={{
button:
"flex justify-center px-4 py-2 mt-5 text-sm font-medium text-white border border-transparent rounded-md shadow-sm bg-gray-700 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-700",
}}
/>
) : null
)}
</SnoopPage>
))}
</SnoopForm>
</div>
);
}
-12
View File
@@ -40,18 +40,6 @@ export default function LayoutFormResults({
</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 */}
+63
View File
@@ -0,0 +1,63 @@
import Head from "next/head";
import Link from "next/link";
import { ArrowLeftIcon, RefreshIcon } from "@heroicons/react/outline";
import { useSession, signIn } from "next-auth/react";
import Loading from "../Loading";
export default function LayoutShare({ formId, resetApp, 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>Form Preview</title>
</Head>
<div className="flex min-h-screen overflow-hidden bg-gray-50">
<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 border-gray-200 shadow-sm">
<div className="flex flex-1 px-4 sm:px-6">
<div className="flex items-center flex-1">
<Link href={`/forms/${formId}/form`}>
<a>
<ArrowLeftIcon className="w-6 h-6" aria-hidden="true" />
</a>
</Link>
</div>
<p className="flex items-center justify-center flex-1 text-gray-600">
Preview
</p>
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:ml-6 sm:space-x-4">
<button
type="button"
onClick={() => resetApp()}
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"
>
Restart
<RefreshIcon
className="w-5 h-5 ml-2 -mr-1"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
</header>
{/* Main content */}
{children}
</div>
</div>
</>
);
}
+12
View File
@@ -30,3 +30,15 @@ export const createNoCodeForm = async (formId) => {
);
}
};
export const persistNoCodeForm = async (noCodeForm) => {
try {
await fetch(`/api/forms/${noCodeForm.formId}/nocodeform`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(noCodeForm),
});
} catch (error) {
console.error(error);
}
};
+16
View File
@@ -0,0 +1,16 @@
import useSWR from "swr";
import { fetcher } from "./utils";
export const usePipelines = (formId: string) => {
const { data, error, mutate } = useSWR(
() => `/api/forms/${formId}/pipelines`,
fetcher
);
return {
pipelines: data,
isLoadingPipelines: !error && !data,
isErrorPipelines: error,
mutatePipeliness: mutate,
};
};
+7
View File
@@ -1,4 +1,6 @@
/** @type {import('next').NextConfig} */
var path = require("path");
const nextConfig = {
reactStrictMode: false,
async redirects() {
@@ -10,6 +12,11 @@ const nextConfig = {
},
];
},
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
config.resolve.alias["react"] = path.resolve("./node_modules/react");
// Important: return the modified config
return config;
},
};
module.exports = nextConfig;
+7279
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -10,22 +10,26 @@
},
"dependencies": {
"@editorjs/editorjs": "^2.24.3",
"@editorjs/header": "^2.6.2",
"@editorjs/paragraph": "^2.8.0",
"@headlessui/react": "^1.6.1",
"@heroicons/react": "^1.0.6",
"@prisma/client": "^3.15.1",
"@snoopforms/react": "^0.0.2",
"babel-plugin-superjson-next": "^0.4.3",
"bcryptjs": "^2.4.3",
"date-fns": "^2.28.0",
"editorjs-drag-drop": "^1.1.2",
"editorjs-undo": "^2.0.3",
"json2csv": "^5.0.7",
"next": "12.1.6",
"next-auth": "^4.3.4",
"nextjs-cors": "^2.1.1",
"nodemailer": "^6.7.5",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-editor-js": "^2.0.6",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-icons": "^4.4.0",
"react-loader-spinner": "^5.1.5",
"superjson": "^1.9.1",
"swr": "^1.3.0"
},
+69
View File
@@ -0,0 +1,69 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import NextCors from "nextjs-cors";
import { formHasOwnership } from "../../../../../lib/api";
import { prisma } from "../../../../../lib/prisma";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
const formId = req.query.id.toString();
await NextCors(req, res, {
// Options
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
origin: "*",
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
});
// check if session exist
const session = await getSession({ req: req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
// check if user is form owner
const ownership = await formHasOwnership(session, formId);
if (!ownership) {
return res
.status(401)
.json({ message: "You are not authorized to change this noCodeForm" });
}
// GET /api/forms/[formId]/pipelines
// Get all pipelines for a specific form
if (req.method === "GET") {
const pipelinesData = await prisma.pipeline.findMany({
where: {
form: { id: formId },
},
orderBy: [
{
createdAt: "desc",
},
],
});
return res.json(pipelinesData);
}
// POST /api/forms/:id/pipelines
// Creates a new submission session
// Required fields in body: -
// Optional fields in body: -
if (req.method === "POST") {
const { type, data } = req.body;
if (!["WEBHOOK"].includes(type)) {
return res.status(400).json({ message: "Unknown pipeline type" });
}
const prismaRes = await prisma.pipeline.create({
data: { type, data, 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.`
);
}
}
@@ -1,5 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import NextCors from "nextjs-cors";
import { formHasOwnership } from "../../../../../lib/api";
import { prisma } from "../../../../../lib/prisma";
export default async function handle(
@@ -17,6 +19,19 @@ export default async function handle(
// GET /api/forms
// Gets all forms of a user
if (req.method === "GET") {
// check if session exist
const session = await getSession({ req: req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
// check if user is form owner
const ownership = await formHasOwnership(session, formId);
if (!ownership) {
return res
.status(401)
.json({ message: "You are not authorized to change this noCodeForm" });
}
const submissionSessionsData = await prisma.submissionSession.findMany({
where: {
form: { id: formId },
@@ -33,6 +48,7 @@ export default async function handle(
return res.json(submissionSessionsData);
}
// PUBLIC
// POST /api/forms/:id/submissionSessions
// Creates a new submission session
// Required fields in body: -
+42
View File
@@ -0,0 +1,42 @@
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
import App from "../../components/frontend/App";
import Loading from "../../components/Loading";
import { useForm } from "../../lib/forms";
export default function Share({}) {
const router = useRouter();
const formId = router.query.id.toString();
const { form, isLoadingForm } = useForm(formId);
if (isLoadingForm) {
return <Loading />;
}
if (form.formType !== "NOCODE") {
return (
<div>
Form Frontend is only avaiblable for Forms built with No-Code-Editor
</div>
);
}
return (
<>
<Head>
<title>SnoopForms</title>
</Head>
<App formId={formId} />
</>
);
}
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const session = await getSession({ req });
if (!session) {
res.statusCode = 403;
}
return { props: {} };
};
+1 -1
View File
@@ -38,7 +38,7 @@ export default function FormPage() {
return (
<>
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
<FormCode />
<FormCode formId={formId} />
</LayoutFormBasics>
</>
);
+3 -3
View File
@@ -76,11 +76,11 @@ export default function PipelinesPage() {
</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
snoopHub automatically stores your data and gives you an 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.
arrives and keep everything in sync.
</p>
</div>
<div>
+44
View File
@@ -0,0 +1,44 @@
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useState } from "react";
import App from "../../../components/frontend/App";
import LayoutPreview from "../../../components/layout/LayoutPreview";
import Loading from "../../../components/Loading";
import { useForm } from "../../../lib/forms";
import { v4 as uuidv4 } from "uuid";
export default function Share({}) {
const router = useRouter();
const formId = router.query.id.toString();
const { form, isLoadingForm } = useForm(formId);
const [appId, setAppId] = useState(uuidv4());
const resetApp = () => {
setAppId(uuidv4());
};
if (isLoadingForm) {
return <Loading />;
}
if (form.formType !== "NOCODE") {
return (
<div>Preview is only avaiblable for Forms built with No-Code-Editor</div>
);
}
return (
<LayoutPreview formId={formId} resetApp={resetApp}>
<App id={appId} formId={formId} draft={true} />
</LayoutPreview>
);
}
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const session = await getSession({ req });
if (!session) {
res.statusCode = 403;
}
return { props: {} };
};
+2 -9
View File
@@ -1,7 +1,6 @@
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";
@@ -11,13 +10,6 @@ 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 />;
@@ -26,11 +18,12 @@ export default function WelcomePage() {
if (!form.finishedOnboarding) {
return (
<LayoutFormBasics title={form.title} formId={formId} currentStep="form">
<FormOnboardingModal open={openOnboardingModal} formId={formId} />
<FormOnboardingModal open={true} formId={formId} />
</LayoutFormBasics>
);
} else {
router.push(`/forms/${formId}`);
return <Loading />;
}
}
@@ -26,6 +26,7 @@ CREATE TABLE "NoCodeForm" (
"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")
);
+1
View File
@@ -39,6 +39,7 @@ model NoCodeForm {
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String @unique
pages Json @default("[]")
pagesDraft Json @default("[]")
}
model Pipeline {
+8
View File
@@ -82,3 +82,11 @@
font-style: normal;
font-display: swap;
}
.ce-block__content,
.ce-toolbar__content {
max-width: calc(100% - 80px) !important;
}
.cdx-block {
max-width: 100% !important;
}
+392 -364
View File
File diff suppressed because it is too large Load Diff