mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-05 21:32:02 -06:00
add submit button, add form preview, autosave form in builder
This commit is contained in:
@@ -5,6 +5,7 @@ import Loading from "../Loading";
|
||||
import Page from "./Page";
|
||||
import UsageIntro from "./UsageIntro";
|
||||
import LoadingModal from "../LoadingModal";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Builder({ formId }) {
|
||||
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
|
||||
@@ -13,6 +14,13 @@ export default function Builder({ formId }) {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// autosave
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
save();
|
||||
}
|
||||
}, [pagesDraft, isInitialized]);
|
||||
|
||||
const save = async () => {
|
||||
setIsLoading(true);
|
||||
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
|
||||
@@ -61,18 +69,17 @@ export default function Builder({ formId }) {
|
||||
<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={() => save()}
|
||||
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<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>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ 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([]);
|
||||
@@ -43,7 +44,7 @@ const Editor = ({ id, autofocus = false, onChange, value }) => {
|
||||
setBlocks(content.blocks);
|
||||
},
|
||||
autofocus: autofocus,
|
||||
tools: { textQuestion: TextQuestion },
|
||||
tools: { textQuestion: TextQuestion, submitButton: SubmitButton },
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
61
components/editorjs/tools/SubmitButton.tsx
Normal file
61
components/editorjs/tools/SubmitButton.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
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) {
|
||||
console.log(
|
||||
(block.firstElementChild.firstElementChild as HTMLInputElement).innerHTML
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,6 @@ export default class TextQuestion implements BlockTool {
|
||||
}
|
||||
|
||||
save(block: HTMLDivElement) {
|
||||
// console.log(block)
|
||||
// ;(window as any).x = block
|
||||
|
||||
return {
|
||||
label: (block.firstElementChild.firstElementChild as HTMLInputElement)
|
||||
.value,
|
||||
@@ -41,18 +38,12 @@ export default class TextQuestion implements BlockTool {
|
||||
).value,
|
||||
};
|
||||
}
|
||||
/* renderSettings(): HTMLElement {
|
||||
|
||||
renderSettings(): HTMLElement {
|
||||
return document.createElement("div");
|
||||
} */
|
||||
}
|
||||
|
||||
render(): HTMLElement {
|
||||
/* this.wrapperDiv.innerHTML = "";
|
||||
this.latexDiv.innerHTML = "";
|
||||
this.renderLatex();
|
||||
this.wrapperDiv.append(this.latexDiv);
|
||||
this.wrapperDiv.append(this.editTextfield);
|
||||
|
||||
return this.wrapperDiv; */
|
||||
const container = document.createElement("div");
|
||||
const toolView = (
|
||||
<div className="pb-3">
|
||||
@@ -60,12 +51,12 @@ export default class TextQuestion implements BlockTool {
|
||||
type="text"
|
||||
id="label"
|
||||
defaultValue={this.label}
|
||||
className="block p-0 text-sm font-medium text-gray-700 border-0 border-transparent ring-0 focus:ring-0"
|
||||
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-sm placeholder:text-gray-300"
|
||||
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}
|
||||
/>
|
||||
|
||||
62
components/frontend/App.tsx
Normal file
62
components/frontend/App.tsx
Normal 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, mutateNoCodeForm } =
|
||||
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="localhost:3000"
|
||||
protocol="http"
|
||||
formId="kQ1L4BLH"
|
||||
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-red-600 hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-600",
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</SnoopPage>
|
||||
))}
|
||||
</SnoopForm>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
components/layout/LayoutPreview.tsx
Normal file
63
components/layout/LayoutPreview.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
package-lock.json
generated
Normal file
7279
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
||||
"@headlessui/react": "^1.6.1",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@prisma/client": "^3.15.1",
|
||||
"@snoopforms/react": "file:../snoopforms-react",
|
||||
"babel-plugin-superjson-next": "^0.4.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"date-fns": "^2.28.0",
|
||||
|
||||
44
pages/forms/[id]/preview.tsx
Normal file
44
pages/forms/[id]/preview.tsx
Normal 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: {} };
|
||||
};
|
||||
Reference in New Issue
Block a user