diff --git a/package.json b/package.json
index d477a0c..2fc68ab 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"@payloadcms/db-postgres": "^3.35.1",
"@payloadcms/next": "^3.35.1",
"@payloadcms/payload-cloud": "^3.35.1",
+ "@payloadcms/plugin-form-builder": "^3.35.1",
"@payloadcms/richtext-lexical": "^3.35.1",
"@payloadcms/storage-s3": "^3.35.1",
"country-state-city": "^3.2.1",
@@ -25,6 +26,7 @@
"qs-esm": "^7.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-hook-form": "^7.56.1",
"react-select": "^5.10.1",
"swiper": "^11.2.6"
},
diff --git a/public/js/script.js b/public/js/script.js
index 54c4c26..3e0ad30 100644
--- a/public/js/script.js
+++ b/public/js/script.js
@@ -1482,168 +1482,168 @@ $(function () {
}
// Select2
- if (plugins.selectFilter.length) {
- var i;
- for (i = 0; i < plugins.selectFilter.length; i++) {
- var select = $(plugins.selectFilter[i]);
+ // if (plugins.selectFilter.length) {
+ // var i;
+ // for (i = 0; i < plugins.selectFilter.length; i++) {
+ // var select = $(plugins.selectFilter[i]);
- select.select2({
- placeholder: select.attr("data-placeholder") ? select.attr("data-placeholder") : false,
- minimumResultsForSearch: select.attr("data-minimum-results-search") ? select.attr("data-minimum-results-search") : -1,
- maximumSelectionSize: 3,
- dropdownCssClass: select.attr("data-class") ? select.attr("data-class") : ''
- });
- }
- }
+ // select.select2({
+ // placeholder: select.attr("data-placeholder") ? select.attr("data-placeholder") : false,
+ // minimumResultsForSearch: select.attr("data-minimum-results-search") ? select.attr("data-minimum-results-search") : -1,
+ // maximumSelectionSize: 3,
+ // dropdownCssClass: select.attr("data-class") ? select.attr("data-class") : ''
+ // });
+ // }
+ // }
// RD Mailform
- if (plugins.rdMailForm.length) {
- var i, j, k,
- msg = {
- 'MF000': 'Successfully sent!',
- 'MF001': 'Recipients are not set!',
- 'MF002': 'Form will not work locally!',
- 'MF003': 'Please, define email field in your form!',
- 'MF004': 'Please, define type of your form!',
- 'MF254': 'Something went wrong with PHPMailer!',
- 'MF255': 'Aw, snap! Something went wrong.'
- };
+ // if (plugins.rdMailForm.length) {
+ // var i, j, k,
+ // msg = {
+ // 'MF000': 'Successfully sent!',
+ // 'MF001': 'Recipients are not set!',
+ // 'MF002': 'Form will not work locally!',
+ // 'MF003': 'Please, define email field in your form!',
+ // 'MF004': 'Please, define type of your form!',
+ // 'MF254': 'Something went wrong with PHPMailer!',
+ // 'MF255': 'Aw, snap! Something went wrong.'
+ // };
- for (i = 0; i < plugins.rdMailForm.length; i++) {
- var $form = $(plugins.rdMailForm[i]),
- formHasCaptcha = false;
+ // for (i = 0; i < plugins.rdMailForm.length; i++) {
+ // var $form = $(plugins.rdMailForm[i]),
+ // formHasCaptcha = false;
- $form.attr('novalidate', 'novalidate').ajaxForm({
- data: {
- "form-type": $form.attr("data-form-type") || "contact",
- "counter": i
- },
- beforeSubmit: function (arr, $form, options) {
- if (isNoviBuilder)
- return;
+ // $form.attr('novalidate', 'novalidate').ajaxForm({
+ // data: {
+ // "form-type": $form.attr("data-form-type") || "contact",
+ // "counter": i
+ // },
+ // beforeSubmit: function (arr, $form, options) {
+ // if (isNoviBuilder)
+ // return;
- var form = $(plugins.rdMailForm[this.extraData.counter]),
- inputs = form.find("[data-constraints]"),
- output = $("#" + form.attr("data-form-output")),
- captcha = form.find('.recaptcha'),
- captchaFlag = true;
+ // var form = $(plugins.rdMailForm[this.extraData.counter]),
+ // inputs = form.find("[data-constraints]"),
+ // output = $("#" + form.attr("data-form-output")),
+ // captcha = form.find('.recaptcha'),
+ // captchaFlag = true;
- output.removeClass("active error success");
+ // output.removeClass("active error success");
- if (isValidated(inputs, captcha)) {
+ // if (isValidated(inputs, captcha)) {
- // veify reCaptcha
- if (captcha.length) {
- var captchaToken = captcha.find('.g-recaptcha-response').val(),
- captchaMsg = {
- 'CPT001': 'Please, setup you "site key" and "secret key" of reCaptcha',
- 'CPT002': 'Something wrong with google reCaptcha'
- };
+ // // veify reCaptcha
+ // if (captcha.length) {
+ // var captchaToken = captcha.find('.g-recaptcha-response').val(),
+ // captchaMsg = {
+ // 'CPT001': 'Please, setup you "site key" and "secret key" of reCaptcha',
+ // 'CPT002': 'Something wrong with google reCaptcha'
+ // };
- formHasCaptcha = true;
+ // formHasCaptcha = true;
- $.ajax({
- method: "POST",
- url: "bat/reCaptcha.php",
- data: { 'g-recaptcha-response': captchaToken },
- async: false
- })
- .done(function (responceCode) {
- if (responceCode !== 'CPT000') {
- if (output.hasClass("snackbars")) {
- output.html('
' + captchaMsg[responceCode] + '
')
+ // $.ajax({
+ // method: "POST",
+ // url: "bat/reCaptcha.php",
+ // data: { 'g-recaptcha-response': captchaToken },
+ // async: false
+ // })
+ // .done(function (responceCode) {
+ // if (responceCode !== 'CPT000') {
+ // if (output.hasClass("snackbars")) {
+ // output.html('' + captchaMsg[responceCode] + '
')
- setTimeout(function () {
- output.removeClass("active");
- }, 3500);
+ // setTimeout(function () {
+ // output.removeClass("active");
+ // }, 3500);
- captchaFlag = false;
- } else {
- output.html(captchaMsg[responceCode]);
- }
+ // captchaFlag = false;
+ // } else {
+ // output.html(captchaMsg[responceCode]);
+ // }
- output.addClass("active");
- }
- });
- }
+ // output.addClass("active");
+ // }
+ // });
+ // }
- if (!captchaFlag) {
- return false;
- }
+ // if (!captchaFlag) {
+ // return false;
+ // }
- form.addClass('form-in-process');
+ // form.addClass('form-in-process');
- if (output.hasClass("snackbars")) {
- output.html('Sending
');
- output.addClass("active");
- }
- } else {
- return false;
- }
- },
- error: function (result) {
- if (isNoviBuilder)
- return;
+ // if (output.hasClass("snackbars")) {
+ // output.html('Sending
');
+ // output.addClass("active");
+ // }
+ // } else {
+ // return false;
+ // }
+ // },
+ // error: function (result) {
+ // if (isNoviBuilder)
+ // return;
- var output = $("#" + $(plugins.rdMailForm[this.extraData.counter]).attr("data-form-output")),
- form = $(plugins.rdMailForm[this.extraData.counter]);
+ // var output = $("#" + $(plugins.rdMailForm[this.extraData.counter]).attr("data-form-output")),
+ // form = $(plugins.rdMailForm[this.extraData.counter]);
- output.text(msg[result]);
- form.removeClass('form-in-process');
+ // output.text(msg[result]);
+ // form.removeClass('form-in-process');
- if (formHasCaptcha) {
- grecaptcha.reset();
- }
- },
- success: function (result) {
- if (isNoviBuilder)
- return;
+ // if (formHasCaptcha) {
+ // grecaptcha.reset();
+ // }
+ // },
+ // success: function (result) {
+ // if (isNoviBuilder)
+ // return;
- var form = $(plugins.rdMailForm[this.extraData.counter]),
- output = $("#" + form.attr("data-form-output")),
- select = form.find('select');
+ // var form = $(plugins.rdMailForm[this.extraData.counter]),
+ // output = $("#" + form.attr("data-form-output")),
+ // select = form.find('select');
- form
- .addClass('success')
- .removeClass('form-in-process');
+ // form
+ // .addClass('success')
+ // .removeClass('form-in-process');
- if (formHasCaptcha) {
- grecaptcha.reset();
- }
+ // if (formHasCaptcha) {
+ // grecaptcha.reset();
+ // }
- result = result.length === 5 ? result : 'MF255';
- output.text(msg[result]);
+ // result = result.length === 5 ? result : 'MF255';
+ // output.text(msg[result]);
- if (result === "MF000") {
- if (output.hasClass("snackbars")) {
- output.html('' + msg[result] + '
');
- } else {
- output.addClass("active success");
- }
- } else {
- if (output.hasClass("snackbars")) {
- output.html(' ' + msg[result] + '
');
- } else {
- output.addClass("active error");
- }
- }
+ // if (result === "MF000") {
+ // if (output.hasClass("snackbars")) {
+ // output.html('' + msg[result] + '
');
+ // } else {
+ // output.addClass("active success");
+ // }
+ // } else {
+ // if (output.hasClass("snackbars")) {
+ // output.html(' ' + msg[result] + '
');
+ // } else {
+ // output.addClass("active error");
+ // }
+ // }
- form.clearForm();
+ // form.clearForm();
- if (select.length) {
- select.select2("val", "");
- }
+ // if (select.length) {
+ // select.select2("val", "");
+ // }
- form.find('input, textarea').trigger('blur');
+ // form.find('input, textarea').trigger('blur');
- setTimeout(function () {
- output.removeClass("active error success");
- form.removeClass('success');
- }, 3500);
- }
- });
- }
- }
+ // setTimeout(function () {
+ // output.removeClass("active error success");
+ // form.removeClass('success');
+ // }, 3500);
+ // }
+ // });
+ // }
+ // }
// RD Filepicker
if ((plugins.filePicker.length || plugins.fileDrop.length) && !isNoviBuilder) {
diff --git a/src/blocks/Content.ts b/src/blocks/Content.ts
new file mode 100644
index 0000000..fed61d9
--- /dev/null
+++ b/src/blocks/Content.ts
@@ -0,0 +1,15 @@
+import { lexicalEditor } from "@payloadcms/richtext-lexical";
+import { Block } from "payload";
+
+export const ContentBlock: Block = {
+ slug: "contentBlock",
+ labels: { plural: "Contents", singular: "Content" },
+ fields: [
+ {
+ name: "content",
+ type: "richText",
+ editor: lexicalEditor({}),
+ required: true,
+ },
+ ],
+};
diff --git a/src/blocks/Form.ts b/src/blocks/Form.ts
new file mode 100644
index 0000000..dc687b6
--- /dev/null
+++ b/src/blocks/Form.ts
@@ -0,0 +1,33 @@
+import type { Block } from "payload";
+
+export const FormBlock: Block = {
+ slug: "formBlock",
+ fields: [
+ {
+ name: "form",
+ type: "relationship",
+ relationTo: "forms",
+ required: true,
+ },
+ {
+ name: "enableIntro",
+ type: "checkbox",
+ label: "Enable Intro Content",
+ },
+ {
+ name: "introContent",
+ type: "richText",
+ admin: {
+ condition: (_, { enableIntro }) => Boolean(enableIntro),
+ },
+ label: "Intro Content",
+ },
+ ],
+ graphQL: {
+ singularName: "FormBlock",
+ },
+ labels: {
+ plural: "Form Blocks",
+ singular: "Form Block",
+ },
+};
diff --git a/src/collections/Pages.ts b/src/collections/Pages.ts
new file mode 100644
index 0000000..ee23f1e
--- /dev/null
+++ b/src/collections/Pages.ts
@@ -0,0 +1,92 @@
+import { ContentBlock } from "@/blocks/Content";
+import { FormBlock } from "@/blocks/Form";
+import formatSlug from "@/utils/payload/formatSlug";
+import setAuthor from "@/utils/payload/setAuthor";
+import { CollectionConfig } from "payload";
+
+export const Pages: CollectionConfig = {
+ slug: "pages",
+ versions: {
+ drafts: {
+ validate: true,
+ },
+ },
+ fields: [
+ {
+ name: "title",
+ label: "Page Title",
+ type: "text",
+ required: true,
+ },
+ {
+ name: "hero_img",
+ label: "Hero Image",
+ type: "upload",
+ relationTo: "media",
+ },
+ {
+ name: "slug",
+ label: "Page Slug",
+ type: "text",
+ hooks: {
+ beforeValidate: [formatSlug("title")],
+ },
+ },
+ {
+ name: "layout",
+ label: "Page Layout",
+ type: "blocks",
+ minRows: 1,
+ blocks: [ContentBlock, FormBlock],
+ },
+ {
+ name: "meta",
+ label: "Page Meta",
+ type: "group",
+ fields: [
+ {
+ name: "title",
+ label: "Title",
+ type: "text",
+ },
+ {
+ name: "description",
+ label: "Description",
+ type: "textarea",
+ },
+ {
+ name: "canonical_url",
+ label: "Canonical Url",
+ type: "text",
+ },
+ ],
+ },
+ {
+ name: "createdBy",
+ type: "relationship",
+ relationTo: "users",
+ hooks: {
+ beforeChange: [setAuthor],
+ },
+ admin: {
+ hidden: true,
+ },
+ },
+ {
+ name: "updatedBy",
+ type: "relationship",
+ relationTo: "users",
+ hooks: {
+ beforeChange: [setAuthor],
+ },
+ admin: {
+ hidden: true,
+ },
+ },
+ ],
+ admin: {
+ hideAPIURL: true,
+ group: "General",
+ useAsTitle: "title",
+ },
+};
diff --git a/src/components/ContactFormBox.tsx b/src/components/ContactFormBox.tsx
index 9fbc71b..af79ac3 100644
--- a/src/components/ContactFormBox.tsx
+++ b/src/components/ContactFormBox.tsx
@@ -1,66 +1,23 @@
+"use client";
+
+import { useFormQuery } from "@/services/hooks/form";
+import { useEffect } from "react";
+import { FormBlock } from "@/components/blocks/Form";
+
export default function ContactFormBox() {
+ const form = useFormQuery();
+
+ useEffect(() => {
+ form._fetch(1);
+ }, []);
+
return (
-
-
Get in Touch with Us Today!
-
-
+
{!form.isFetching && !!form.data && }
);
diff --git a/src/components/ContactFormSection.tsx b/src/components/ContactFormSection.tsx
index ce1963b..059cda4 100644
--- a/src/components/ContactFormSection.tsx
+++ b/src/components/ContactFormSection.tsx
@@ -1,66 +1,23 @@
+"use client";
+
+import { FormBlock } from "@/components/blocks/Form";
+import { useFormQuery } from "@/services/hooks/form";
+import { useEffect } from "react";
+
export default function ContactFormSection() {
+ const form = useFormQuery();
+
+ useEffect(() => {
+ form._fetch(1);
+ }, []);
+
return (
-
-
Get in Touch with Us Today!
-
-
+
{!form.isFetching && !!form.data && }
);
diff --git a/src/components/HeroSlider.tsx b/src/components/HeroSlider.tsx
index 50575e2..d4326d5 100644
--- a/src/components/HeroSlider.tsx
+++ b/src/components/HeroSlider.tsx
@@ -1,10 +1,10 @@
"use client";
+import Image from "next/image";
+import { useState } from "react";
+import { Autoplay } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/react";
import ContactFormBox from "./ContactFormBox";
-import { Autoplay } from "swiper/modules";
-import { useState } from "react";
-import Image from "next/image";
type HeroSliderProps = {
onClickBook?: () => void;
diff --git a/src/components/blocks/Content/index.tsx b/src/components/blocks/Content/index.tsx
new file mode 100644
index 0000000..38ac7a9
--- /dev/null
+++ b/src/components/blocks/Content/index.tsx
@@ -0,0 +1,26 @@
+import { RichText } from "@payloadcms/richtext-lexical/react";
+
+// type Props = extract
+
+export function ContentBlock(props: any) {
+ return (
+
+
+ {/* Content */}
+
+ {/* Post */}
+
+
+
+ {/* @ts-ignore */}
+
+
+
+
+ {/* End Post */}
+
+ {/* End Content */}
+
+
+ );
+}
diff --git a/src/components/blocks/Form/Button/index.tsx b/src/components/blocks/Form/Button/index.tsx
new file mode 100644
index 0000000..643ed1b
--- /dev/null
+++ b/src/components/blocks/Form/Button/index.tsx
@@ -0,0 +1,54 @@
+import type { ElementType } from "react";
+
+import Link from "next/link";
+import React from "react";
+
+export type Props = {
+ appearance?: "default" | "primary" | "secondary";
+ className?: string;
+ el?: "a" | "button" | "link";
+ form?: string;
+ href?: string;
+ label?: string;
+ newTab?: boolean | null;
+ onClick?: () => void;
+ isLoading?: boolean;
+};
+
+export const Button: React.FC = ({
+ className: classNameFromProps,
+ el = "button",
+ form,
+ href,
+ label,
+ newTab,
+ isLoading,
+}) => {
+ const newTabProps = newTab ? { rel: "noopener noreferrer", target: "_blank" } : {};
+ const Element: ElementType = el;
+ const className = [classNameFromProps].filter(Boolean).join(" ");
+
+ const elementProps = {
+ ...newTabProps,
+ className,
+ form,
+ href,
+ };
+
+ return (
+
+
+ {(el === "link" || el === "a") && (
+
+ {label}
+
+ )}
+ {el === "button" && (
+ <>
+ {label} {isLoading && }
+ >
+ )}
+
+
+ );
+};
diff --git a/src/components/blocks/Form/Checkbox/index.tsx b/src/components/blocks/Form/Checkbox/index.tsx
new file mode 100644
index 0000000..938335c
--- /dev/null
+++ b/src/components/blocks/Form/Checkbox/index.tsx
@@ -0,0 +1,34 @@
+import type { CheckboxField } from "@payloadcms/plugin-form-builder/types";
+import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
+
+import React from "react";
+
+import { Error } from "../Error";
+import { Width } from "../Width";
+
+export const Checkbox: React.FC<
+ {
+ errors: Partial<
+ FieldErrorsImpl<{
+ [x: string]: any;
+ }>
+ >;
+ getValues: any;
+ register: UseFormRegister;
+ setValue: any;
+ } & CheckboxField
+> = ({ name, errors, label, register, required: requiredFromProps, width }) => {
+ return (
+
+
+
+
+
+
+ {requiredFromProps && errors[name] &&
}
+
+
+ );
+};
diff --git a/src/components/blocks/Form/Email/index.tsx b/src/components/blocks/Form/Email/index.tsx
new file mode 100644
index 0000000..841ee92
--- /dev/null
+++ b/src/components/blocks/Form/Email/index.tsx
@@ -0,0 +1,35 @@
+import type { EmailField } from "@payloadcms/plugin-form-builder/types";
+import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
+
+import React from "react";
+
+import { Error } from "../Error";
+import { Width } from "../Width";
+
+export const Email: React.FC<
+ {
+ errors: Partial<
+ FieldErrorsImpl<{
+ [x: string]: any;
+ }>
+ >;
+ register: UseFormRegister;
+ } & EmailField
+> = ({ name, errors, label, register, required: requiredFromProps, width }) => {
+ return (
+
+
+
+
+ {requiredFromProps && errors[name] && }
+
+
+ );
+};
diff --git a/src/components/blocks/Form/Error/index.scss b/src/components/blocks/Form/Error/index.scss
new file mode 100644
index 0000000..38792cc
--- /dev/null
+++ b/src/components/blocks/Form/Error/index.scss
@@ -0,0 +1,4 @@
+.error {
+ margin-top: 5px;
+ color: var(--color-red);
+}
diff --git a/src/components/blocks/Form/Error/index.tsx b/src/components/blocks/Form/Error/index.tsx
new file mode 100644
index 0000000..1324ba9
--- /dev/null
+++ b/src/components/blocks/Form/Error/index.tsx
@@ -0,0 +1,5 @@
+import * as React from "react";
+
+export const Error: React.FC = () => {
+ return This field is required
;
+};
diff --git a/src/components/blocks/Form/Link/index.tsx b/src/components/blocks/Form/Link/index.tsx
new file mode 100644
index 0000000..ab255a2
--- /dev/null
+++ b/src/components/blocks/Form/Link/index.tsx
@@ -0,0 +1,72 @@
+import Link from "next/link";
+import React from "react";
+
+// @ts-ignore
+import type { Page } from "@/payload-types";
+
+import { Button } from "../Button";
+
+export type CMSLinkType = {
+ appearance?: "default" | "primary" | "secondary";
+ children?: React.ReactNode;
+ className?: string;
+ label?: string;
+ newTab?: boolean | null;
+ reference?: {
+ relationTo: "pages";
+ value: number | Page | string;
+ } | null;
+ type?: "custom" | "reference" | null;
+ url?: null | string;
+};
+
+export const CMSLink: React.FC = ({
+ type,
+ appearance,
+ children,
+ className,
+ label,
+ newTab,
+ reference,
+ url,
+}) => {
+ const href =
+ type === "reference" && typeof reference?.value === "object" && reference.value.slug
+ ? `${reference?.relationTo !== "pages" ? `/${reference?.relationTo}` : ""}/${reference.value.slug}`
+ : url;
+
+ if (!href) {
+ return null;
+ }
+
+ if (!appearance) {
+ const newTabProps = newTab ? { rel: "noopener noreferrer", target: "_blank" } : {};
+
+ if (type === "custom") {
+ return (
+
+ {label && label}
+ {children ? <>{children}> : null}
+
+ );
+ }
+
+ if (href) {
+ return (
+
+ {label && label}
+ {children ? <>{children}> : null}
+
+ );
+ }
+ }
+
+ const buttonProps = {
+ appearance,
+ href,
+ label,
+ newTab,
+ };
+
+ return ;
+};
diff --git a/src/components/blocks/Form/Number/index.tsx b/src/components/blocks/Form/Number/index.tsx
new file mode 100644
index 0000000..4a7ad38
--- /dev/null
+++ b/src/components/blocks/Form/Number/index.tsx
@@ -0,0 +1,30 @@
+import type { TextField } from "@payloadcms/plugin-form-builder/types";
+import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
+
+import React from "react";
+
+import { Error } from "../Error";
+import { Width } from "../Width";
+
+export const Number: React.FC<
+ {
+ errors: Partial<
+ FieldErrorsImpl<{
+ [x: string]: any;
+ }>
+ >;
+ register: UseFormRegister;
+ } & TextField
+> = ({ name, errors, label, register, required: requiredFromProps, width }) => {
+ return (
+
+
+
+
+ {requiredFromProps && errors[name] && }
+
+
+ );
+};
diff --git a/src/components/blocks/Form/RichText/index.tsx b/src/components/blocks/Form/RichText/index.tsx
new file mode 100644
index 0000000..ff15d56
--- /dev/null
+++ b/src/components/blocks/Form/RichText/index.tsx
@@ -0,0 +1,24 @@
+import React from 'react'
+
+import { serializeLexical } from './serialize'
+
+const RichText: React.FC<{ className?: string; content: any; enableGutter?: boolean }> = ({
+ className,
+ content,
+}) => {
+ if (!content) {
+ return null
+ }
+
+ return (
+
+ {content &&
+ !Array.isArray(content) &&
+ typeof content === 'object' &&
+ 'root' in content &&
+ serializeLexical({ nodes: content?.root?.children })}
+
+ )
+}
+
+export default RichText
diff --git a/src/components/blocks/Form/RichText/nodeFormat.tsx b/src/components/blocks/Form/RichText/nodeFormat.tsx
new file mode 100644
index 0000000..7a3b416
--- /dev/null
+++ b/src/components/blocks/Form/RichText/nodeFormat.tsx
@@ -0,0 +1,118 @@
+// @ts-nocheck
+//This copy-and-pasted from lexical here here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
+
+import type { ElementFormatType, TextFormatType } from "@payloadcms/richtext-lexical/lexical";
+import type { TextDetailType, TextModeType } from "lexical/nodes/LexicalTextNode";
+
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+// DOM
+export const DOM_ELEMENT_TYPE = 1;
+export const DOM_TEXT_TYPE = 3;
+
+// Reconciling
+export const NO_DIRTY_NODES = 0;
+export const HAS_DIRTY_NODES = 1;
+export const FULL_RECONCILE = 2;
+
+// Text node modes
+export const IS_NORMAL = 0;
+export const IS_TOKEN = 1;
+export const IS_SEGMENTED = 2;
+// IS_INERT = 3
+
+// Text node formatting
+export const IS_BOLD = 1;
+export const IS_ITALIC = 1 << 1;
+export const IS_STRIKETHROUGH = 1 << 2;
+export const IS_UNDERLINE = 1 << 3;
+export const IS_CODE = 1 << 4;
+export const IS_SUBSCRIPT = 1 << 5;
+export const IS_SUPERSCRIPT = 1 << 6;
+export const IS_HIGHLIGHT = 1 << 7;
+
+export const IS_ALL_FORMATTING =
+ IS_BOLD | IS_ITALIC | IS_STRIKETHROUGH | IS_UNDERLINE | IS_CODE | IS_SUBSCRIPT | IS_SUPERSCRIPT | IS_HIGHLIGHT;
+
+// Text node details
+export const IS_DIRECTIONLESS = 1;
+export const IS_UNMERGEABLE = 1 << 1;
+
+// Element node formatting
+export const IS_ALIGN_LEFT = 1;
+export const IS_ALIGN_CENTER = 2;
+export const IS_ALIGN_RIGHT = 3;
+export const IS_ALIGN_JUSTIFY = 4;
+export const IS_ALIGN_START = 5;
+export const IS_ALIGN_END = 6;
+
+// Reconciliation
+export const NON_BREAKING_SPACE = "\u00A0";
+// const ZERO_WIDTH_SPACE = '\u200b'
+
+export const DOUBLE_LINE_BREAK = "\n\n";
+
+// For FF, we need to use a non-breaking space, or it gets composition
+// in a stuck state.
+
+const RTL = "\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC";
+const LTR =
+ "A-Za-z\u00C0-\u00D6\u00D8-\u00F6" +
+ "\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C" +
+ "\uFE00-\uFE6F\uFEFD-\uFFFF";
+
+export const RTL_REGEX = new RegExp("^[^" + LTR + "]*[" + RTL + "]");
+
+export const LTR_REGEX = new RegExp("^[^" + RTL + "]*[" + LTR + "]");
+
+export const TEXT_TYPE_TO_FORMAT: Record = {
+ bold: IS_BOLD,
+ code: IS_CODE,
+ highlight: IS_HIGHLIGHT,
+ italic: IS_ITALIC,
+ strikethrough: IS_STRIKETHROUGH,
+ subscript: IS_SUBSCRIPT,
+ superscript: IS_SUPERSCRIPT,
+ underline: IS_UNDERLINE,
+};
+
+export const DETAIL_TYPE_TO_DETAIL: Record = {
+ directionless: IS_DIRECTIONLESS,
+ unmergeable: IS_UNMERGEABLE,
+};
+
+export const ELEMENT_TYPE_TO_FORMAT: Record, number> = {
+ center: IS_ALIGN_CENTER,
+ end: IS_ALIGN_END,
+ justify: IS_ALIGN_JUSTIFY,
+ left: IS_ALIGN_LEFT,
+ right: IS_ALIGN_RIGHT,
+ start: IS_ALIGN_START,
+};
+
+export const ELEMENT_FORMAT_TO_TYPE: Record = {
+ [IS_ALIGN_CENTER]: "center",
+ [IS_ALIGN_END]: "end",
+ [IS_ALIGN_JUSTIFY]: "justify",
+ [IS_ALIGN_LEFT]: "left",
+ [IS_ALIGN_RIGHT]: "right",
+ [IS_ALIGN_START]: "start",
+};
+
+export const TEXT_MODE_TO_TYPE: Record = {
+ normal: IS_NORMAL,
+ segmented: IS_SEGMENTED,
+ token: IS_TOKEN,
+};
+
+export const TEXT_TYPE_TO_MODE: Record = {
+ [IS_NORMAL]: "normal",
+ [IS_SEGMENTED]: "segmented",
+ [IS_TOKEN]: "token",
+};
diff --git a/src/components/blocks/Form/RichText/serialize.tsx b/src/components/blocks/Form/RichText/serialize.tsx
new file mode 100644
index 0000000..0152ec8
--- /dev/null
+++ b/src/components/blocks/Form/RichText/serialize.tsx
@@ -0,0 +1,173 @@
+// @ts-nocheck
+import type { LinkFields, SerializedLinkNode } from "@payloadcms/richtext-lexical";
+import type {
+ SerializedElementNode,
+ SerializedLexicalNode,
+ SerializedTextNode,
+} from "@payloadcms/richtext-lexical/lexical";
+import type { SerializedListItemNode, SerializedListNode } from "@payloadcms/richtext-lexical/lexical/list";
+import type { SerializedHeadingNode } from "@payloadcms/richtext-lexical/lexical/rich-text";
+import type { JSX } from "react";
+
+import React, { Fragment } from "react";
+
+import { CMSLink } from "../Link";
+import {
+ IS_BOLD,
+ IS_CODE,
+ IS_ITALIC,
+ IS_STRIKETHROUGH,
+ IS_SUBSCRIPT,
+ IS_SUPERSCRIPT,
+ IS_UNDERLINE,
+} from "./nodeFormat";
+
+interface Props {
+ nodes: SerializedLexicalNode[];
+}
+
+export function serializeLexical({ nodes }: Props): JSX.Element {
+ return (
+
+ {nodes?.map((_node, index): JSX.Element | null => {
+ if (_node.type === "text") {
+ const node = _node as SerializedTextNode;
+ let text = {node.text};
+ if (node.format & IS_BOLD) {
+ text = {text};
+ }
+ if (node.format & IS_ITALIC) {
+ text = {text};
+ }
+ if (node.format & IS_STRIKETHROUGH) {
+ text = (
+
+ {text}
+
+ );
+ }
+ if (node.format & IS_UNDERLINE) {
+ text = (
+
+ {text}
+
+ );
+ }
+ if (node.format & IS_CODE) {
+ text = {node.text}
;
+ }
+ if (node.format & IS_SUBSCRIPT) {
+ text = {text};
+ }
+ if (node.format & IS_SUPERSCRIPT) {
+ text = {text};
+ }
+
+ return text;
+ }
+
+ if (_node == null) {
+ return null;
+ }
+
+ // NOTE: Hacky fix for
+ // https://github.com/facebook/lexical/blob/d10c4e6e55261b2fdd7d1845aed46151d0f06a8c/packages/lexical-list/src/LexicalListItemNode.ts#L133
+ // which does not return checked: false (only true - i.e. there is no prop for false)
+ const serializedChildrenFn = (node: SerializedElementNode): JSX.Element | null => {
+ if (node.children == null) {
+ return null;
+ } else {
+ if (node?.type === "list" && (node as SerializedListNode)?.listType === "check") {
+ for (const item of node.children) {
+ if ("checked" in item) {
+ if (!item?.checked) {
+ item.checked = false;
+ }
+ }
+ }
+ return serializeLexical({ nodes: node.children });
+ } else {
+ return serializeLexical({ nodes: node.children });
+ }
+ }
+ };
+
+ const serializedChildren = "children" in _node ? serializedChildrenFn(_node as SerializedElementNode) : "";
+
+ switch (_node.type) {
+ case "heading": {
+ const node = _node as SerializedHeadingNode;
+
+ type Heading = Extract;
+ const Tag = node?.tag as Heading;
+ return {serializedChildren};
+ }
+ case "linebreak": {
+ return
;
+ }
+ case "link": {
+ const node = _node as SerializedLinkNode;
+
+ const fields: LinkFields = node.fields;
+
+ return (
+
+ {serializedChildren}
+
+ );
+ }
+ case "list": {
+ const node = _node as SerializedListNode;
+
+ type List = Extract;
+ const Tag = node?.tag as List;
+ return (
+
+ {serializedChildren}
+
+ );
+ }
+ case "listitem": {
+ const node = _node as SerializedListItemNode;
+
+ if (node?.checked != null) {
+ return (
+
+ {serializedChildren}
+
+ );
+ } else {
+ return (
+
+ {serializedChildren}
+
+ );
+ }
+ }
+ case "paragraph": {
+ return {serializedChildren}
;
+ }
+ case "quote": {
+ return {serializedChildren}
;
+ }
+
+ default:
+ return null;
+ }
+ })}
+
+ );
+}
diff --git a/src/components/blocks/Form/Select/index.tsx b/src/components/blocks/Form/Select/index.tsx
new file mode 100644
index 0000000..8ff6a2f
--- /dev/null
+++ b/src/components/blocks/Form/Select/index.tsx
@@ -0,0 +1,54 @@
+import type { SelectField } from "@payloadcms/plugin-form-builder/types";
+import type { Control, FieldErrorsImpl, FieldValues } from "react-hook-form";
+
+import React from "react";
+import { Controller } from "react-hook-form";
+
+import { Error } from "../Error";
+import { Width } from "../Width";
+
+export const Select: React.FC<
+ {
+ control: Control;
+ errors: Partial<
+ FieldErrorsImpl<{
+ [x: string]: any;
+ }>
+ >;
+ } & SelectField
+> = ({ name, control, errors, label, options, required, width }) => {
+ return (
+
+
+ (
+ <>
+ {/* onChange(val ? val.value : "")}
+ options={options}
+ value={options.find((s) => s.value === value)}
+ /> */}
+
+
+ >
+ )}
+ rules={{ required }}
+ />
+ {required && errors[name] && }
+
+
+ );
+};
diff --git a/src/components/blocks/Form/Text/index.tsx b/src/components/blocks/Form/Text/index.tsx
new file mode 100644
index 0000000..357af00
--- /dev/null
+++ b/src/components/blocks/Form/Text/index.tsx
@@ -0,0 +1,30 @@
+import type { TextField } from "@payloadcms/plugin-form-builder/types";
+import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
+
+import React from "react";
+
+import { Error } from "../Error";
+import { Width } from "../Width";
+
+export const Text: React.FC<
+ {
+ errors: Partial<
+ FieldErrorsImpl<{
+ [x: string]: any;
+ }>
+ >;
+ register: UseFormRegister;
+ } & TextField
+> = ({ name, errors, label, register, required: requiredFromProps, width }) => {
+ return (
+
+
+
+
+ {requiredFromProps && errors[name] && }
+
+
+ );
+};
diff --git a/src/components/blocks/Form/Textarea/index.tsx b/src/components/blocks/Form/Textarea/index.tsx
new file mode 100644
index 0000000..0a3c385
--- /dev/null
+++ b/src/components/blocks/Form/Textarea/index.tsx
@@ -0,0 +1,31 @@
+import type { TextField } from "@payloadcms/plugin-form-builder/types";
+import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
+
+import React from "react";
+
+import { Error } from "../Error";
+import { Width } from "../Width";
+
+export const Textarea: React.FC<
+ {
+ errors: Partial<
+ FieldErrorsImpl<{
+ [x: string]: any;
+ }>
+ >;
+ register: UseFormRegister;
+ rows?: number;
+ } & TextField
+> = ({ name, errors, label, register, required: requiredFromProps, rows = 3, width }) => {
+ return (
+
+
+
+
+ {requiredFromProps && errors[name] && }
+
+
+ );
+};
diff --git a/src/components/blocks/Form/Width/index.tsx b/src/components/blocks/Form/Width/index.tsx
new file mode 100644
index 0000000..90c3d0b
--- /dev/null
+++ b/src/components/blocks/Form/Width/index.tsx
@@ -0,0 +1,13 @@
+import * as React from "react";
+
+export const Width: React.FC<{
+ children: React.ReactNode;
+ width?: number;
+}> = ({ children, width }) => {
+ let colXl = "col-xl-12";
+ if (!!width && width === 50) {
+ colXl = "col-xl-6";
+ }
+
+ return {children}
;
+};
diff --git a/src/components/blocks/Form/buildInitialFormState.tsx b/src/components/blocks/Form/buildInitialFormState.tsx
new file mode 100644
index 0000000..32dabf0
--- /dev/null
+++ b/src/components/blocks/Form/buildInitialFormState.tsx
@@ -0,0 +1,43 @@
+// @ts-nocheck
+import type { FormFieldBlock } from "@payloadcms/plugin-form-builder/types";
+
+export const buildInitialFormState = (fields: FormFieldBlock[]) => {
+ return fields.reduce((initialSchema, field) => {
+ if (field.blockType === "checkbox") {
+ return {
+ ...initialSchema,
+ [field.name]: false,
+ };
+ }
+ if (field.blockType === "country") {
+ return {
+ ...initialSchema,
+ [field.name]: "",
+ };
+ }
+ if (field.blockType === "email") {
+ return {
+ ...initialSchema,
+ [field.name]: "",
+ };
+ }
+ if (field.blockType === "text") {
+ return {
+ ...initialSchema,
+ [field.name]: "",
+ };
+ }
+ if (field.blockType === "select") {
+ return {
+ ...initialSchema,
+ [field.name]: "",
+ };
+ }
+ if (field.blockType === "state") {
+ return {
+ ...initialSchema,
+ [field.name]: "",
+ };
+ }
+ }, {});
+};
diff --git a/src/components/blocks/Form/fields.tsx b/src/components/blocks/Form/fields.tsx
new file mode 100644
index 0000000..9d8643e
--- /dev/null
+++ b/src/components/blocks/Form/fields.tsx
@@ -0,0 +1,15 @@
+import { Checkbox } from "./Checkbox";
+import { Email } from "./Email";
+import { Number } from "./Number";
+import { Select } from "./Select";
+import { Text } from "./Text";
+import { Textarea } from "./Textarea";
+
+export const fields = {
+ checkbox: Checkbox,
+ email: Email,
+ number: Number,
+ select: Select,
+ text: Text,
+ textarea: Textarea,
+};
diff --git a/src/components/blocks/Form/index.tsx b/src/components/blocks/Form/index.tsx
new file mode 100644
index 0000000..74b946d
--- /dev/null
+++ b/src/components/blocks/Form/index.tsx
@@ -0,0 +1,180 @@
+// @ts-nocheck
+"use client";
+
+import { useRouter } from "next/navigation";
+import React, { useCallback, useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { Form } from "@/payload-types";
+import { Button } from "./Button";
+import RichText from "./RichText";
+import { buildInitialFormState } from "./buildInitialFormState";
+import { fields } from "./fields";
+
+export type Value = unknown;
+
+export interface Property {
+ [key: string]: Value;
+}
+
+export interface Data {
+ [key: string]: Property | Property[] | Value;
+}
+
+export type FormBlockType = {
+ blockName?: string;
+ blockType?: "formBlock";
+ enableIntro?: boolean | null;
+ form: number | Form;
+ introContent?: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ("ltr" | "rtl") | null;
+ format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ } | null;
+};
+
+export const FormBlock: React.FC<
+ FormBlockType & {
+ id?: string | null;
+ }
+> = (props) => {
+ const {
+ enableIntro,
+ form: formFromProps,
+ form: { id: formID, confirmationMessage, confirmationType, redirect, submitButtonLabel } = {},
+ introContent,
+ } = props;
+
+ const formMethods = useForm({
+ defaultValues: buildInitialFormState(formFromProps.fields),
+ });
+ const {
+ control,
+ formState: { errors },
+ handleSubmit,
+ register,
+ // getValues,
+ // setValue,
+ } = formMethods;
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [hasSubmitted, setHasSubmitted] = useState();
+ const [error, setError] = useState<{ message: string; status?: string } | undefined>();
+ const router = useRouter();
+
+ const onSubmit = useCallback(
+ (data: Data) => {
+ let loadingTimerID: ReturnType;
+ const submitForm = async () => {
+ setError(undefined);
+
+ const dataToSend = Object.entries(data).map(([name, value]) => ({
+ field: name,
+ value,
+ }));
+
+ // delay loading indicator by 1s
+ loadingTimerID = setTimeout(() => {
+ setIsLoading(true);
+ }, 250);
+
+ try {
+ const req = await fetch(`/api/form-submissions`, {
+ body: JSON.stringify({
+ form: formID,
+ submissionData: dataToSend,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ });
+
+ const res = await req.json();
+
+ clearTimeout(loadingTimerID);
+
+ if (req.status >= 400) {
+ setIsLoading(false);
+
+ setError({
+ message: res.errors?.[0]?.message || "Internal Server Error",
+ status: res.status,
+ });
+
+ return;
+ }
+
+ setIsLoading(false);
+ setHasSubmitted(true);
+
+ if (confirmationType === "redirect" && redirect) {
+ const { url } = redirect;
+
+ const redirectUrl = url;
+
+ if (redirectUrl) router.push(redirectUrl);
+ }
+ } catch (err) {
+ console.warn(err);
+ setIsLoading(false);
+ setError({
+ message: "Something went wrong.",
+ });
+ }
+ };
+
+ void submitForm();
+ },
+ [router, formID, redirect, confirmationType]
+ );
+
+ return (
+
+ {!!formFromProps?.title &&
{formFromProps.title}
}
+
+ {!!enableIntro && introContent && !hasSubmitted &&
}
+ {!isLoading && hasSubmitted && confirmationType === "message" &&
}
+ {isLoading && !hasSubmitted &&
Loading, please wait...
}
+ {error &&
{`${error.status || "500"}: ${error.message || ""}`}
}
+ {!hasSubmitted && (
+
+ )}
+
+
+ );
+};
diff --git a/src/components/blocks/Form/shared.scss b/src/components/blocks/Form/shared.scss
new file mode 100644
index 0000000..99ee80c
--- /dev/null
+++ b/src/components/blocks/Form/shared.scss
@@ -0,0 +1,42 @@
+@use "../css/common.scss" as *;
+
+@mixin formInput() {
+ all: unset;
+ -webkit-appearance: none;
+ box-sizing: border-box;
+ font-family: var(--font-body);
+ width: 100%;
+ border: 1px solid var(--color-black);
+ background: var(--color-white);
+ color: var(--color-black);
+ font-size: 1rem;
+ height: calc(var(--base) * 2.5);
+ line-height: var(--base);
+ padding: calc(var(--base) * 0.75);
+
+ &::-moz-placeholder,
+ &::-webkit-input-placeholder {
+ color: var(--color-mid-gray);
+ font-weight: normal;
+ font-size: 1rem;
+ }
+
+ &:hover {
+ border-color: var(--color-mid-gray);
+ }
+
+ &:focus,
+ &:active {
+ border-color: var(--color-gray);
+ outline: 0;
+ }
+
+ &:disabled {
+ background: var(--color-light-gray);
+ color: var(--color-gray);
+
+ &:hover {
+ border-color: var(--color-light-gray);
+ }
+ }
+}
diff --git a/src/components/blocks/RenderBlocks.tsx b/src/components/blocks/RenderBlocks.tsx
new file mode 100644
index 0000000..477cc9b
--- /dev/null
+++ b/src/components/blocks/RenderBlocks.tsx
@@ -0,0 +1,56 @@
+import React, { Fragment } from "react";
+
+import type { Page } from "@/payload-types";
+import { ContentBlock } from "./Content";
+import { FormBlock } from "./Form";
+
+const blockComponents = {
+ contentBlock: ContentBlock,
+ formBlock: FormBlock,
+};
+
+export const RenderBlocks: React.FC<{
+ blocks: Page["layout"];
+}> = (props) => {
+ const { blocks } = props;
+
+ const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0;
+
+ if (hasBlocks) {
+ return (
+
+ {blocks.map((block, index) => {
+ const { blockType } = block;
+
+ if (blockType && blockType in blockComponents) {
+ const Block = blockComponents[blockType];
+ if (!Block) return null;
+
+ if (blockType === "formBlock") {
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+ {/* @ts-ignore */}
+
+
+ );
+ }
+ }
+ return null;
+ })}
+
+ );
+ }
+
+ return null;
+};
diff --git a/src/components/homes/HomeTopSection.tsx b/src/components/homes/HomeTopSection.tsx
index 2c30b63..67d0940 100644
--- a/src/components/homes/HomeTopSection.tsx
+++ b/src/components/homes/HomeTopSection.tsx
@@ -1,8 +1,8 @@
"use client";
-import { useRef } from "react";
import HeroSlider from "@/components/HeroSlider";
-import ContactFormSection from "@/components/ContactFormSection";
+import { useRef } from "react";
+import ContactFormSection from "../ContactFormSection";
export default function HomeTopSection() {
const formRef = useRef(null);
diff --git a/src/payload-types.ts b/src/payload-types.ts
index 6670958..49ad5b3 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -74,6 +74,9 @@ export interface Config {
blogs: Blog;
propertyFeatures: PropertyFeature;
properties: Property;
+ pages: Page;
+ forms: Form;
+ 'form-submissions': FormSubmission;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -87,6 +90,9 @@ export interface Config {
blogs: BlogsSelect | BlogsSelect;
propertyFeatures: PropertyFeaturesSelect | PropertyFeaturesSelect;
properties: PropertiesSelect | PropertiesSelect;
+ pages: PagesSelect | PagesSelect;
+ forms: FormsSelect | FormsSelect;
+ 'form-submissions': FormSubmissionsSelect | FormSubmissionsSelect;
'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect;
'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect;
'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect;
@@ -285,6 +291,245 @@ export interface Property {
createdAt: string;
_status?: ('draft' | 'published') | null;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "pages".
+ */
+export interface Page {
+ id: number;
+ title: string;
+ hero_img?: (number | null) | Media;
+ slug?: string | null;
+ layout?:
+ | (
+ | {
+ content: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ };
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'contentBlock';
+ }
+ | {
+ form: number | Form;
+ enableIntro?: boolean | null;
+ introContent?: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ } | null;
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'formBlock';
+ }
+ )[]
+ | null;
+ meta?: {
+ title?: string | null;
+ description?: string | null;
+ canonical_url?: string | null;
+ };
+ createdBy?: (number | null) | User;
+ updatedBy?: (number | null) | User;
+ updatedAt: string;
+ createdAt: string;
+ _status?: ('draft' | 'published') | null;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "forms".
+ */
+export interface Form {
+ id: number;
+ title: string;
+ fields?:
+ | (
+ | {
+ name: string;
+ label?: string | null;
+ width?: number | null;
+ required?: boolean | null;
+ defaultValue?: boolean | null;
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'checkbox';
+ }
+ | {
+ name: string;
+ label?: string | null;
+ width?: number | null;
+ required?: boolean | null;
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'email';
+ }
+ | {
+ message?: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ } | null;
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'message';
+ }
+ | {
+ name: string;
+ label?: string | null;
+ width?: number | null;
+ defaultValue?: number | null;
+ required?: boolean | null;
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'number';
+ }
+ | {
+ name: string;
+ label?: string | null;
+ width?: number | null;
+ defaultValue?: string | null;
+ placeholder?: string | null;
+ options?:
+ | {
+ label: string;
+ value: string;
+ id?: string | null;
+ }[]
+ | null;
+ required?: boolean | null;
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'select';
+ }
+ | {
+ name: string;
+ label?: string | null;
+ width?: number | null;
+ defaultValue?: string | null;
+ required?: boolean | null;
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'text';
+ }
+ | {
+ name: string;
+ label?: string | null;
+ width?: number | null;
+ defaultValue?: string | null;
+ required?: boolean | null;
+ id?: string | null;
+ blockName?: string | null;
+ blockType: 'textarea';
+ }
+ )[]
+ | null;
+ submitButtonLabel?: string | null;
+ /**
+ * Choose whether to display an on-page message or redirect to a different page after they submit the form.
+ */
+ confirmationType?: ('message' | 'redirect') | null;
+ confirmationMessage?: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ } | null;
+ redirect?: {
+ url: string;
+ };
+ /**
+ * Send custom emails when the form submits. Use comma separated lists to send the same email to multiple recipients. To reference a value from this form, wrap that field's name with double curly brackets, i.e. {{firstName}}. You can use a wildcard {{*}} to output all data and {{*:table}} to format it as an HTML table in the email.
+ */
+ emails?:
+ | {
+ emailTo?: string | null;
+ cc?: string | null;
+ bcc?: string | null;
+ replyTo?: string | null;
+ emailFrom?: string | null;
+ subject: string;
+ /**
+ * Enter the message that should be sent in this email.
+ */
+ message?: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ } | null;
+ id?: string | null;
+ }[]
+ | null;
+ updatedAt: string;
+ createdAt: string;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "form-submissions".
+ */
+export interface FormSubmission {
+ id: number;
+ form: number | Form;
+ submissionData?:
+ | {
+ field: string;
+ value: string;
+ id?: string | null;
+ }[]
+ | null;
+ updatedAt: string;
+ createdAt: string;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -319,6 +564,18 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'properties';
value: number | Property;
+ } | null)
+ | ({
+ relationTo: 'pages';
+ value: number | Page;
+ } | null)
+ | ({
+ relationTo: 'forms';
+ value: number | Form;
+ } | null)
+ | ({
+ relationTo: 'form-submissions';
+ value: number | FormSubmission;
} | null);
globalSlug?: string | null;
user: {
@@ -494,6 +751,176 @@ export interface PropertiesSelect {
createdAt?: T;
_status?: T;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "pages_select".
+ */
+export interface PagesSelect {
+ title?: T;
+ hero_img?: T;
+ slug?: T;
+ layout?:
+ | T
+ | {
+ contentBlock?:
+ | T
+ | {
+ content?: T;
+ id?: T;
+ blockName?: T;
+ };
+ formBlock?:
+ | T
+ | {
+ form?: T;
+ enableIntro?: T;
+ introContent?: T;
+ id?: T;
+ blockName?: T;
+ };
+ };
+ meta?:
+ | T
+ | {
+ title?: T;
+ description?: T;
+ canonical_url?: T;
+ };
+ createdBy?: T;
+ updatedBy?: T;
+ updatedAt?: T;
+ createdAt?: T;
+ _status?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "forms_select".
+ */
+export interface FormsSelect {
+ title?: T;
+ fields?:
+ | T
+ | {
+ checkbox?:
+ | T
+ | {
+ name?: T;
+ label?: T;
+ width?: T;
+ required?: T;
+ defaultValue?: T;
+ id?: T;
+ blockName?: T;
+ };
+ email?:
+ | T
+ | {
+ name?: T;
+ label?: T;
+ width?: T;
+ required?: T;
+ id?: T;
+ blockName?: T;
+ };
+ message?:
+ | T
+ | {
+ message?: T;
+ id?: T;
+ blockName?: T;
+ };
+ number?:
+ | T
+ | {
+ name?: T;
+ label?: T;
+ width?: T;
+ defaultValue?: T;
+ required?: T;
+ id?: T;
+ blockName?: T;
+ };
+ select?:
+ | T
+ | {
+ name?: T;
+ label?: T;
+ width?: T;
+ defaultValue?: T;
+ placeholder?: T;
+ options?:
+ | T
+ | {
+ label?: T;
+ value?: T;
+ id?: T;
+ };
+ required?: T;
+ id?: T;
+ blockName?: T;
+ };
+ text?:
+ | T
+ | {
+ name?: T;
+ label?: T;
+ width?: T;
+ defaultValue?: T;
+ required?: T;
+ id?: T;
+ blockName?: T;
+ };
+ textarea?:
+ | T
+ | {
+ name?: T;
+ label?: T;
+ width?: T;
+ defaultValue?: T;
+ required?: T;
+ id?: T;
+ blockName?: T;
+ };
+ };
+ submitButtonLabel?: T;
+ confirmationType?: T;
+ confirmationMessage?: T;
+ redirect?:
+ | T
+ | {
+ url?: T;
+ };
+ emails?:
+ | T
+ | {
+ emailTo?: T;
+ cc?: T;
+ bcc?: T;
+ replyTo?: T;
+ emailFrom?: T;
+ subject?: T;
+ message?: T;
+ id?: T;
+ };
+ updatedAt?: T;
+ createdAt?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "form-submissions_select".
+ */
+export interface FormSubmissionsSelect {
+ form?: T;
+ submissionData?:
+ | T
+ | {
+ field?: T;
+ value?: T;
+ id?: T;
+ };
+ updatedAt?: T;
+ createdAt?: T;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
diff --git a/src/payload.config.ts b/src/payload.config.ts
index 9d87b61..ef20223 100644
--- a/src/payload.config.ts
+++ b/src/payload.config.ts
@@ -15,6 +15,8 @@ import { BlogCategories } from "@/collections/BlogCategories";
import { Blogs } from "@/collections/Blogs";
import { PropertyFeatures } from "@/collections/PropertyFeatures";
import { Properties } from "./collections/Properties";
+import { Pages } from "@/collections/Pages";
+import { formBuilderPlugin } from "@payloadcms/plugin-form-builder";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
@@ -39,7 +41,7 @@ export default buildConfig({
},
theme: "dark",
},
- collections: [Users, Media, BlogTags, BlogCategories, Blogs, PropertyFeatures, Properties],
+ collections: [Users, Media, BlogTags, BlogCategories, Blogs, PropertyFeatures, Properties, Pages],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || "",
typescript: {
@@ -71,5 +73,55 @@ export default buildConfig({
endpoint: process.env.S3_ENDPOINT,
},
}),
+ // form builder
+ formBuilderPlugin({
+ fields: {
+ checkbox: true,
+ email: true,
+ number: true,
+ select: true,
+ text: true,
+ textarea: true,
+ country: false,
+ state: false,
+ payment: false,
+ },
+ formOverrides: {
+ fields: undefined,
+ admin: {
+ group: "General",
+ useAsTitle: "title",
+ },
+ },
+ formSubmissionOverrides: {
+ fields: undefined,
+ admin: {
+ group: "General",
+ useAsTitle: "form",
+ },
+ },
+ // beforeEmail: (emailsToSend, beforeChangeParams) => {
+ // if (beforeChangeParams.data.form === 1) {
+ // const submissionData: Record = {};
+ // for (const s of beforeChangeParams.data.submissionData) {
+ // submissionData[s.field] = s.value;
+ // }
+
+ // submissionData["confirmation"] = submissionData["confirmation"]
+ // ? "I consent to receive text messages and emails, and that I am 18+ years old."
+ // : "";
+
+ // // modify the emails in any way before they are sent
+ // return emailsToSend.map((email) => {
+ // return {
+ // ...email,
+ // html: loadEmailTemplate("contact-form-email.html", submissionData),
+ // };
+ // });
+ // }
+
+ // return emailsToSend;
+ // },
+ }),
],
});
diff --git a/src/services/hooks/form.ts b/src/services/hooks/form.ts
new file mode 100644
index 0000000..df6d3e6
--- /dev/null
+++ b/src/services/hooks/form.ts
@@ -0,0 +1,26 @@
+import { Form } from "@/payload-types";
+import { useState } from "react";
+import { fetchFormREST } from "../rest/form";
+
+export function useFormQuery() {
+ const [data, setData] = useState