mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-25 10:30:30 -06:00
add form frontend
This commit is contained in:
@@ -4,12 +4,14 @@ import { v4 as uuidv4 } from "uuid";
|
||||
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, isLoadingNoCodeForm, mutateNoCodeForm } =
|
||||
useNoCodeForm(formId);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [openShareModal, setOpenShareModal] = useState(false);
|
||||
|
||||
const addPage = useCallback(async () => {
|
||||
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
|
||||
@@ -37,6 +39,14 @@ export default function Builder({ formId }) {
|
||||
}
|
||||
}, [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(() => {
|
||||
initPages();
|
||||
}, [isLoadingNoCodeForm, initPages]);
|
||||
@@ -61,6 +71,18 @@ export default function Builder({ formId }) {
|
||||
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>
|
||||
@@ -85,6 +107,11 @@ export default function Builder({ formId }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ShareModal
|
||||
open={openShareModal}
|
||||
setOpen={setOpenShareModal}
|
||||
formId={formId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
90
components/builder/ShareModal.tsx
Normal file
90
components/builder/ShareModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -31,9 +31,6 @@ export default class TextQuestion implements BlockTool {
|
||||
}
|
||||
|
||||
save(block: HTMLDivElement) {
|
||||
console.log(
|
||||
(block.firstElementChild.firstElementChild as HTMLInputElement).innerHTML
|
||||
);
|
||||
return {
|
||||
label: (block.firstElementChild.firstElementChild as HTMLInputElement)
|
||||
.innerHTML,
|
||||
|
||||
@@ -19,9 +19,10 @@ export default function App({ id = "", formId, draft = false }) {
|
||||
<div className="w-full px-5 py-5">
|
||||
<SnoopForm
|
||||
key={id} // used to reset form
|
||||
domain="localhost:3000"
|
||||
protocol="http"
|
||||
formId="kQ1L4BLH"
|
||||
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) => (
|
||||
@@ -48,7 +49,7 @@ export default function App({ id = "", formId, draft = false }) {
|
||||
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",
|
||||
"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
|
||||
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -51,7 +51,6 @@
|
||||
"../snoopforms-react": {
|
||||
"name": "@snoopforms/react",
|
||||
"version": "0.0.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"@headlessui/react": "^1.6.1",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@prisma/client": "^3.15.1",
|
||||
"@snoopforms/react": "file:../snoopforms-react",
|
||||
"@snoopforms/react": "^0.0.2",
|
||||
"babel-plugin-superjson-next": "^0.4.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"date-fns": "^2.28.0",
|
||||
|
||||
42
pages/f/[id].tsx
Normal file
42
pages/f/[id].tsx
Normal 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: {} };
|
||||
};
|
||||
@@ -252,8 +252,10 @@
|
||||
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz"
|
||||
integrity sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==
|
||||
|
||||
"@snoopforms/react@file:../snoopforms-react":
|
||||
version "0.0.1"
|
||||
"@snoopforms/react@^0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@snoopforms/react/-/react-0.0.2.tgz#b74eaf671309a5c07b176690fd5394384265a6cf"
|
||||
integrity sha512-yXE9w7Wak1RADJjG9ajuniA/gWL283rrUsCQpjDpg8NFgb8u9SklGcuXgybkioTl2FzppN41zSTiZC5kr/GPjA==
|
||||
dependencies:
|
||||
"@fingerprintjs/fingerprintjs" "^3.3.3"
|
||||
"@headlessui/react" "^1.6.4"
|
||||
|
||||
Reference in New Issue
Block a user