Updated Editor-Approach for easier multi-page management (#4)

* use new editor approach where we use a single editorJS instance and a pageTransition block to indicate the start of a new page
* improve toast messages in editor
* add keyboard listener and toast to showcase autosaving functionality
* bugfix first and secondary menu positioning
* add required functionality to textQuestion
This commit is contained in:
Matthias Nannt
2022-06-29 15:24:11 +09:00
committed by GitHub
parent 82dfbadc27
commit 4192461f5f
20 changed files with 393 additions and 338 deletions
+64 -111
View File
@@ -1,129 +1,77 @@
import EditorJS from "@editorjs/editorjs";
import {
DocumentAddIcon,
EyeIcon,
PaperAirplaneIcon,
ShareIcon,
} from "@heroicons/react/outline";
import { NoCodeForm } from "@prisma/client";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useCallback, useEffect, useState } from "react";
import { useRef, useState } from "react";
import { toast } from "react-toastify";
import { v4 as uuidv4 } from "uuid";
import { useForm } from "../../lib/forms";
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
import LimitedWidth from "../layout/LimitedWidth";
import SecondNavBar from "../layout/SecondNavBar";
import Loading from "../Loading";
import Page from "./Page";
import LoadingModal from "../LoadingModal";
import ShareModal from "./ShareModal";
let Editor = dynamic(() => import("../editorjs/Editor"), {
ssr: false,
});
export default function Builder({ formId }) {
const router = useRouter();
const { form, isLoadingForm } = useForm(formId);
const editorRef = useRef<EditorJS | null>();
const { isLoadingForm } = useForm(formId);
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
useNoCodeForm(formId);
const [isInitialized, setIsInitialized] = useState(false);
const [openShareModal, setOpenShareModal] = useState(false);
const [loading, setLoading] = useState(false);
const addPage = useCallback(
async (page = undefined) => {
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.pagesDraft.push(
page || { id: uuidv4(), type: "form", 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 addPage = () => {
editorRef.current.blocks.insert("pageTransition", {
submitLabel: "Submit",
});
const block = editorRef.current.blocks.insert("paragraph");
editorRef.current.caret.setToBlock(
editorRef.current.blocks.getBlockIndex(block.id)
);
};
const initPages = useCallback(async () => {
if (!isLoadingNoCodeForm && !isLoadingForm && !isInitialized) {
if (noCodeForm.pagesDraft.length === 0) {
const newNoCodeForm: NoCodeForm = JSON.parse(
JSON.stringify(noCodeForm)
);
newNoCodeForm.pagesDraft = [
{
id: uuidv4(),
type: "form",
blocks: [
{
id: "FrEb9paDoV",
data: {
text: form.name,
level: 1,
},
type: "header",
},
{
id: "qtvg94SRMB",
data: {
placeholder: "",
},
type: "textQuestion",
},
{
id: "e_N-JpRIfL",
data: {
label: "Submit",
},
type: "submitButton",
},
],
},
{
id: uuidv4(),
type: "thankyou",
blocks: [
{
id: "pIcLJUy0SY",
data: {
text: "Thank you for taking the time to fill out this form 🙏",
},
type: "paragraph",
},
],
},
];
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
}
setIsInitialized(true);
}
}, [
isLoadingNoCodeForm,
noCodeForm,
isInitialized,
isLoadingForm,
form,
mutateNoCodeForm,
]);
const initAction = async (editor: EditorJS) => {
editor.blocks.insert("header", {
text: noCodeForm.form.name,
});
const focusBlock = editor.blocks.insert("textQuestion");
editor.blocks.insert("pageTransition", {
submitLabel: "Submit",
});
editor.blocks.insert("header", {
text: "Thank you",
});
editor.blocks.insert("paragraph", {
text: "Thank you for taking the time to fill out this form 🙏",
});
editor.blocks.delete(0); // remove defaultBlock
editor.caret.setToBlock(
editorRef.current.blocks.getBlockIndex(focusBlock.id)
);
};
const publishChanges = async () => {
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.pages = newNoCodeForm.pagesDraft;
newNoCodeForm.published = true;
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
setOpenShareModal(true);
toast("Your changes are now live 🎉");
setLoading(true);
setTimeout(async () => {
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.blocks = newNoCodeForm.blocksDraft;
newNoCodeForm.published = true;
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
setLoading(false);
toast("Your changes are now public 🎉");
}, 500);
};
useEffect(() => {
initPages();
}, [isLoadingNoCodeForm, initPages]);
if (isLoadingNoCodeForm) {
return <Loading />;
}
const noCodeSecondNavigation = [
{
id: "addPage",
@@ -154,22 +102,26 @@ export default function Builder({ formId }) {
},
];
if (isLoadingNoCodeForm || isLoadingForm) {
return <Loading />;
}
return (
<>
<SecondNavBar navItems={noCodeSecondNavigation} />
<div className="w-full bg-ui-gray-lighter">
<div className="flex justify-center w-full">
<div className="grid w-full grid-cols-1">
{noCodeForm.pagesDraft.map((page, pageIdx) => (
<Page
key={page.id}
<div className="w-full h-full mb-20 overflow-auto bg-white">
<div className="flex justify-center w-full pt-10 pb-56">
<LimitedWidth>
{Editor && (
<Editor
id="editor"
formId={formId}
page={page}
pageIdx={pageIdx}
deletePageAction={deletePage}
editorRef={editorRef}
autofocus={true}
initAction={initAction}
/>
))}
</div>
)}
</LimitedWidth>
</div>
</div>
<ShareModal
@@ -177,6 +129,7 @@ export default function Builder({ formId }) {
setOpen={setOpenShareModal}
formId={formId}
/>
<LoadingModal isLoading={loading} />
</>
);
}
-63
View File
@@ -1,63 +0,0 @@
import dynamic from "next/dynamic";
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
import Loading from "../Loading";
import PageToolbar from "./PageToolbar";
let Editor = dynamic(() => import("../editorjs/Editor"), {
ssr: false,
});
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`
);
}
};
const setPageType = async (newType) => {
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.pagesDraft[pageIdx].type = newType;
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
};
if (isLoadingNoCodeForm) {
return <Loading />;
}
return (
<div className="relative w-full bg-white">
{pageIdx !== 0 && (
<div className="z-10">
<PageToolbar
page={page}
pageIdx={pageIdx}
deletePageAction={deletePageAction}
setPageType={setPageType}
/>
</div>
)}
<div className="relative w-full p-10 ">
<div className="relative max-w-5xl mx-auto">
{Editor && (
<Editor
id={`${page.id}-editor`}
autofocus={pageIdx === 0}
onChange={(blocks) => updatePage(blocks)}
value={noCodeForm.pagesDraft[pageIdx]}
/>
)}
</div>
</div>
</div>
);
}
+2
View File
@@ -1,6 +1,7 @@
/* This example requires Tailwind CSS v2.0+ */
import { Dialog, Transition } from "@headlessui/react";
import { InformationCircleIcon, XIcon } from "@heroicons/react/outline";
import { toast } from "react-toastify";
import { Fragment } from "react";
import { useNoCodeForm } from "../../lib/noCodeForm";
import Loading from "../Loading";
@@ -101,6 +102,7 @@ export default function ShareModal({ open, setOpen, formId }) {
<button
onClick={() => {
navigator.clipboard.writeText(getPublicFormUrl());
toast("Link copied to clipboard 🙌");
}}
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"
>
+61 -22
View File
@@ -1,54 +1,91 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Fragment, useEffect, useRef, useState } from "react";
import EditorJS from "@editorjs/editorjs";
import Header from "@editorjs/header";
import Paragraph from "@editorjs/paragraph";
import DragDrop from "editorjs-drag-drop";
import Undo from "editorjs-undo";
import { Fragment, useCallback, useEffect } from "react";
import { toast } from "react-toastify";
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
import Loading from "../Loading";
import PageTransition from "./tools/PageTransition";
import TextQuestion from "./tools/TextQuestion";
import SubmitButton from "./tools/SubmitButton";
import Paragraph from "@editorjs/paragraph";
import Header from "@editorjs/header";
const Editor = ({ id, autofocus = false, onChange, value }) => {
const [blocks, setBlocks] = useState([]);
const ejInstance = useRef<EditorJS | null>();
interface EditorProps {
id: string;
autofocus: boolean;
editorRef: { current: EditorJS | null };
formId: string;
initAction: (editor: EditorJS) => void;
}
const Editor = ({
id,
autofocus = false,
editorRef,
formId,
initAction,
}: EditorProps) => {
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
useNoCodeForm(formId);
const keyPressListener = useCallback((e) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
toast("snoopForms autosaves your work ✌️");
}
}, []);
useEffect(() => {
if (ejInstance.current) {
onChange(blocks);
}
}, [blocks]);
window.addEventListener("keydown", keyPressListener);
// Remove event listeners on cleanup
return () => {
window.removeEventListener("keydown", keyPressListener);
};
}, [keyPressListener]);
// This will run only once
useEffect(() => {
if (!ejInstance.current) {
initEditor();
if (!isLoadingNoCodeForm) {
if (!editorRef.current) {
initEditor();
}
}
return () => {
destroyEditor();
};
async function destroyEditor() {
await ejInstance.current.isReady;
ejInstance.current.destroy();
ejInstance.current = null;
await editorRef.current.isReady;
editorRef.current.destroy();
editorRef.current = null;
}
}, []);
}, [isLoadingNoCodeForm]);
const initEditor = () => {
const editor = new EditorJS({
minHeight: 0,
holder: id,
data: value,
data: { blocks: noCodeForm.blocksDraft },
onReady: () => {
ejInstance.current = editor;
editorRef.current = editor;
new DragDrop(editor);
new Undo({ editor });
if (editor.blocks.getBlocksCount() === 1) {
initAction(editor);
}
},
onChange: async () => {
let content = await editor.saver.save();
setBlocks(content.blocks);
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.blocksDraft = content.blocks;
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
},
autofocus: autofocus,
defaultBlock: "paragraph",
tools: {
textQuestion: TextQuestion,
pageTransition: PageTransition,
paragraph: {
class: Paragraph,
inlineToolbar: true,
@@ -65,12 +102,14 @@ const Editor = ({ id, autofocus = false, onChange, value }) => {
defaultLevel: 1,
},
},
textQuestion: TextQuestion,
submitButton: SubmitButton,
},
});
};
if (isLoadingNoCodeForm) {
return <Loading />;
}
return (
<Fragment>
<div id={id} />
@@ -0,0 +1,73 @@
import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs";
import ReactDOM from "react-dom";
//styles imports in angular.json
interface PageTransitionData extends BlockToolData {
submitLabel: string;
}
export default class PageTransition implements BlockTool {
submitLabel: string;
placeholder: string;
api: API;
/* static get toolbox(): { icon: string; title?: string } {
return {
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /> </svg>`,
title: "New Page",
};
} */
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: PageTransitionData;
}) {
this.submitLabel = data.label || "Submit";
}
save(block: HTMLDivElement) {
return {
submitLabel: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLInputElement
).innerHTML,
};
}
render(): HTMLElement {
const container = document.createElement("div");
const toolView = (
<div className="my-8">
<div className="inline-flex items-center px-4 py-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-gray-500">
<div
contentEditable
id="label"
defaultValue={this.submitLabel}
className="p-0 bg-transparent border-transparent ring-0 active:ring-0 focus:border-transparent focus:ring-0 focus:outline-none placeholder:text-opacity-5"
>
{this.submitLabel}
</div>
{/* <ArrowRightIcon className="w-5 h-5 ml-2 -mr-1" aria-hidden="true" /> */}
</div>
<div className="relative my-4">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center">
<span className="px-2 text-sm text-gray-500 bg-white">
Next Page
</span>
</div>
</div>
</div>
);
ReactDOM.render(toolView, container);
return container;
}
}
@@ -1,57 +0,0 @@
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" 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-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-gray-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;
}
}
+67 -13
View File
@@ -3,13 +3,16 @@ import ReactDOM from "react-dom";
//styles imports in angular.json
interface TextQuestionData extends BlockToolData {
latexString: string;
label: string;
placeholder: string;
required: boolean;
}
export default class TextQuestion implements BlockTool {
label: string;
placeholder: string;
settings: { name: string; icon: string }[];
api: API;
data: any;
wrapper: undefined | HTMLElement;
static get toolbox(): { icon: string; title?: string } {
return {
@@ -25,12 +28,24 @@ export default class TextQuestion implements BlockTool {
config?: ToolConfig;
data?: TextQuestionData;
}) {
this.label = data.label;
this.placeholder = data.placeholder;
this.wrapper = undefined;
this.settings = [
{
name: "required",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512" class="w-3 h-3"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M471.99 334.43L336.06 256l135.93-78.43c7.66-4.42 10.28-14.2 5.86-21.86l-32.02-55.43c-4.42-7.65-14.21-10.28-21.87-5.86l-135.93 78.43V16c0-8.84-7.17-16-16.01-16h-64.04c-8.84 0-16.01 7.16-16.01 16v156.86L56.04 94.43c-7.66-4.42-17.45-1.79-21.87 5.86L2.15 155.71c-4.42 7.65-1.8 17.44 5.86 21.86L143.94 256 8.01 334.43c-7.66 4.42-10.28 14.21-5.86 21.86l32.02 55.43c4.42 7.65 14.21 10.27 21.87 5.86l135.93-78.43V496c0 8.84 7.17 16 16.01 16h64.04c8.84 0 16.01-7.16 16.01-16V339.14l135.93 78.43c7.66 4.42 17.45 1.8 21.87-5.86l32.02-55.43c4.42-7.65 1.8-17.43-5.86-21.85z"/></svg>`,
},
];
this.data = data;
this.data = {
label: data.label || "",
placeholder: data.placeholder || "",
required: data.required !== undefined ? data.required : true,
};
}
save(block: HTMLDivElement) {
return {
...this.data,
label: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLInputElement
@@ -42,31 +57,70 @@ export default class TextQuestion implements BlockTool {
}
renderSettings(): HTMLElement {
return document.createElement("div");
const wrapper = document.createElement("div");
this.settings.forEach((tune) => {
let button = document.createElement("div");
button.classList.add("cdx-settings-button");
button.classList.toggle(
"cdx-settings-button--active",
this.data[tune.name]
);
button.innerHTML = tune.icon;
wrapper.appendChild(button);
button.addEventListener("click", () => {
this._toggleTune(tune.name);
button.classList.toggle("cdx-settings-button--active");
});
});
return wrapper;
}
/**
* @private
* Click on the Settings Button
* @param {string} tune — tune name from this.settings
*/
_toggleTune(tune) {
this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]);
if (tune === "required") {
this.data.required = !this.data.required;
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this
.data.required
? "*"
: "";
}
}
render(): HTMLElement {
const container = document.createElement("div");
this.wrapper = document.createElement("div");
const toolView = (
<div className="pb-5">
<div className="font-bold leading-7 text-gray-800 text-md sm:truncate">
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
<input
type="text"
id="label"
defaultValue={this.label}
className="block w-full p-0 border-0 border-transparent ring-0 focus:ring-0"
defaultValue={this.data.label}
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
placeholder="Your Question"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
*
</div>
</div>
<input
type="text"
className="block w-full mt-1 text-sm text-gray-400 border-gray-300 rounded-md shadow-sm placeholder:text-gray-300"
placeholder="optional placeholder"
defaultValue={this.placeholder}
defaultValue={this.data.placeholder}
/>
</div>
);
ReactDOM.render(toolView, container);
return container;
ReactDOM.render(toolView, this.wrapper);
return this.wrapper;
}
}
+33 -2
View File
@@ -1,6 +1,37 @@
import { SnoopElement, SnoopForm, SnoopPage } from "@snoopforms/react";
import { useMemo } from "react";
import Loading from "../Loading";
export default function App({ id = "", formId, blocks, localOnly = false }) {
const pages = useMemo(() => {
const pages = [];
let currentPage = {
id: formId, // give the first page the formId as id by default
blocks: [],
};
for (const block of blocks) {
if (block.type !== "pageTransition") {
currentPage.blocks.push(block);
} else {
currentPage.blocks.push({
data: {
label: block.data.submitLabel,
},
type: "submitButton",
});
pages.push(currentPage);
currentPage = {
id: block.id,
blocks: [],
};
}
}
pages.push(currentPage);
return pages;
}, [blocks, formId]);
if (!pages) return <Loading />;
export default function App({ id = "", formId, pages, localOnly = false }) {
return (
<div className="w-full px-5 py-5">
<SnoopForm
@@ -37,7 +68,7 @@ export default function App({ id = "", formId, pages, localOnly = false }) {
element:
"flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-base border-gray-300",
}}
required
required={block.data.required}
/>
</div>
) : block.type === "submitButton" ? (
+12 -5
View File
@@ -13,6 +13,7 @@ interface BaseLayoutAuthorizedProps {
steps?: any;
currentStep?: string;
children: React.ReactNode;
bgClass?: string;
limitHeightScreen?: boolean;
}
@@ -22,6 +23,7 @@ export default function BaseLayoutAuthorized({
steps,
currentStep,
children,
bgClass = "bg-ui-gray-lighter",
limitHeightScreen = false,
}: BaseLayoutAuthorizedProps) {
const { data: session, status } = useSession();
@@ -42,15 +44,18 @@ export default function BaseLayoutAuthorized({
</Head>
<div
className={classNames(
limitHeightScreen ? "h-screen" : "min-h-screen",
"flex bg-ui-gray-lighter h-full"
bgClass,
limitHeightScreen
? "h-screen max-h-screen overflow-hidden"
: "min-h-screen",
"flex h-full"
)}
>
<div className="flex flex-col flex-1 h-full">
<header className="w-full">
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-ui-gray-light">
<div className="flex justify-between flex-1">
<div className="inline-flex flex-1 gap-8">
<div className="flex flex-1 space-x-8">
<NewFormNavButton />
<MenuBreadcrumbs breadcrumbs={breadcrumbs} />
</div>
@@ -59,8 +64,10 @@ export default function BaseLayoutAuthorized({
<MenuSteps steps={steps} currentStep={currentStep} />
)}
</div>
<div className="flex items-center justify-end flex-1 mr-4 space-x-2 text-right sm:ml-6 sm:space-x-4">
<MenuProfile />
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:space-x-4">
<div className="mr-6">
<MenuProfile />
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -16,7 +16,7 @@ export default function NewFormNavButton({}) {
<li>
<div className="inline-flex items-center px-6 py-2 text-sm font-medium leading-4 bg-transparent border border-transparent hover:text-white focus:outline-none">
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Start New Form
create form
</div>
</li>
</ol>
+25 -27
View File
@@ -17,33 +17,31 @@ interface Props {
// button component, consuming props
const SecondNavBar: React.FC<Props> = ({ navItems, currentItemId }) => {
return (
<div className="relative flex flex-shrink-0 h-16 border-b border-ui-gray-light bg-ui-gray-lighter">
<div className="flex items-center justify-center flex-1 px-4 py-2">
<nav className="flex space-x-10" aria-label="resultModes">
{navItems.map((navItem) => (
<button
key={navItem.id}
className={classNames(
`h-16 text-xs`,
!navItem.disabled &&
(navItem.id === currentItemId
? "text-red border-b-2 border-red"
: "hover:border-b-2 hover:border-gray-300 text-ui-gray-dark hover:text-red bg-transparent"),
navItem.disabled
? "text-ui-gray-medium"
: "hover:border-b-2 hover:border-red text-ui-gray-dark hover:text-red"
)}
onClick={navItem.onClick}
disabled={navItem.disabled}
>
{navItem.Icon && (
<navItem.Icon className="w-6 h-6 mx-auto mb-1 stroke-1" />
)}
{navItem.label}
</button>
))}
</nav>
</div>
<div className="flex items-center justify-center flex-shrink-0 border-b border-ui-gray-light bg-ui-gray-lighter">
<nav className="flex space-x-10" aria-label="resultModes">
{navItems.map((navItem) => (
<button
key={navItem.id}
className={classNames(
`h-16 text-xs border-b-2 border-transparent`,
!navItem.disabled &&
(navItem.id === currentItemId
? "text-red border-b-2 border-red"
: "hover:border-gray-300 text-ui-gray-dark hover:text-red bg-transparent"),
navItem.disabled
? "text-ui-gray-medium"
: "hover:border-b-2 hover:border-red text-ui-gray-dark hover:text-red"
)}
onClick={navItem.onClick}
disabled={navItem.disabled}
>
{navItem.Icon && (
<navItem.Icon className="w-6 h-6 mx-auto mb-1 stroke-1" />
)}
{navItem.label}
</button>
))}
</nav>
</div>
);
};
+11
View File
@@ -116,3 +116,14 @@ export const timeSince = (dateString: string) => {
addSuffix: true,
});
};
export const generateId = (length) => {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
+15 -1
View File
@@ -29,6 +29,11 @@ export default async function handle(
where: {
formId: formId,
},
include: {
form: {
select: { name: true },
},
},
});
return res.json(data);
}
@@ -37,7 +42,16 @@ export default async function handle(
// Required fields in body: -
// Optional fields in body: title, published, finishedOnboarding, elements, elementsDraft
else if (req.method === "POST") {
const data = { ...req.body, updatedAt: new Date() };
const { id, createdAt, blocks, blocksDraft, published } = req.body;
const data = {
id,
createdAt,
blocks,
blocksDraft,
formId,
published,
updatedAt: new Date(),
};
// create or update record
const prismaRes = await prisma.noCodeForm.upsert({
where: { formId },
+1 -11
View File
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../lib/prisma";
import { getSession } from "next-auth/react";
import { generateId } from "../../../lib/utils";
export default async function handle(
req: NextApiRequest,
@@ -66,17 +67,6 @@ export default async function handle(
}
}
const generateId = (length) => {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
const checkIdAvailability = async (id) => {
const form = await prisma.form.findUnique({
where: { id },
@@ -17,7 +17,7 @@ export default async function handle(
select: {
id: true,
published: true,
pages: true,
blocks: true,
},
});
if (data === null) return res.status(404).json({ error: "not found" });
+2 -9
View File
@@ -2,7 +2,6 @@ import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
import { useMemo } from "react";
import App from "../../components/frontend/App";
import Loading from "../../components/Loading";
import { useNoCodeFormPublic } from "../../lib/noCodeForm";
@@ -13,17 +12,11 @@ export default function Share({}) {
const { noCodeForm, isLoadingNoCodeForm, isErrorNoCodeForm } =
useNoCodeFormPublic(formId);
const pages = useMemo(() => {
if (!isLoadingNoCodeForm && !isErrorNoCodeForm) {
return noCodeForm["pages"];
}
}, [isLoadingNoCodeForm, noCodeForm, isErrorNoCodeForm]);
if (isErrorNoCodeForm) {
return <p>Not found</p>;
}
if (isLoadingNoCodeForm || !pages) {
if (isLoadingNoCodeForm) {
return <Loading />;
}
@@ -32,7 +25,7 @@ export default function Share({}) {
<Head>
<title>SnoopForms</title>
</Head>
<App formId={formId} pages={pages} />
<App formId={formId} blocks={noCodeForm.blocks} />
</>
);
}
+2
View File
@@ -33,6 +33,8 @@ export default function FormPage() {
breadcrumbs={breadcrumbs}
steps={formMenuSteps}
currentStep="form"
bgClass="bg-white"
limitHeightScreen={true}
>
<FullWidth>
<Builder formId={formId} />
+9 -13
View File
@@ -1,7 +1,7 @@
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useMemo, useState } from "react";
import { useState } from "react";
import { toast } from "react-toastify";
import { v4 as uuidv4 } from "uuid";
import App from "../../../components/frontend/App";
@@ -17,22 +17,13 @@ export default function Share({}) {
const [appId, setAppId] = useState(uuidv4());
const { noCodeForm, isLoadingNoCodeForm } = useNoCodeForm(formId);
const pages = useMemo(() => {
if (!isLoadingNoCodeForm) {
return noCodeForm["pagesDraft"];
}
}, [isLoadingNoCodeForm, noCodeForm]);
if (!pages) {
return <Loading />;
}
const resetApp = () => {
setAppId(uuidv4());
toast("Form reset successful");
toast("Form resetted 👌");
};
if (isLoadingForm) {
if (isLoadingForm || isLoadingNoCodeForm) {
return <Loading />;
}
@@ -44,7 +35,12 @@ export default function Share({}) {
return (
<LayoutPreview formId={formId} resetApp={resetApp}>
<App id={appId} pages={pages} localOnly={true} formId={formId} />
<App
id={appId}
blocks={noCodeForm.blocksDraft}
localOnly={true}
formId={formId}
/>
</LayoutPreview>
);
}
@@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the column `pages` on the `NoCodeForm` table. All the data in the column will be lost.
- You are about to drop the column `pagesDraft` on the `NoCodeForm` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "NoCodeForm" DROP COLUMN "pages",
DROP COLUMN "pagesDraft",
ADD COLUMN "blocks" JSONB NOT NULL DEFAULT '[]',
ADD COLUMN "blocksDraft" JSONB NOT NULL DEFAULT '[]';
+2 -2
View File
@@ -36,8 +36,8 @@ model NoCodeForm {
updatedAt DateTime @updatedAt
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String @unique
pages Json @default("[]")
pagesDraft Json @default("[]")
blocks Json @default("[]")
blocksDraft Json @default("[]")
published Boolean @default(false)
}