From d8d7532169e37c31fc741fd7fa1d1bc1365ce989 Mon Sep 17 00:00:00 2001 From: RizqiSyahrendra Date: Thu, 24 Apr 2025 14:47:06 +0700 Subject: [PATCH 1/4] fix: home fetching featured properties and header active styling --- src/app/(main)/globals.css | 3 ++ src/app/(main)/page.tsx | 51 +++++++------------ src/app/(main)/property/[slug]/loading.tsx | 5 ++ src/collections/Properties.ts | 3 ++ src/components/homes/HomeTopSection.tsx | 23 +++++++++ src/components/layouts/Header.tsx | 30 ++++++++--- src/components/properties/CardProperty.tsx | 3 +- .../properties/ListOfFeaturedProperty.tsx | 23 +++++++++ src/services/hooks/property.ts | 24 +++++++++ src/services/payload/property.ts | 30 +++++++++++ src/services/rest/property.ts | 50 ++++++++++++++++++ 11 files changed, 204 insertions(+), 41 deletions(-) create mode 100644 src/app/(main)/property/[slug]/loading.tsx create mode 100644 src/components/homes/HomeTopSection.tsx create mode 100644 src/components/properties/ListOfFeaturedProperty.tsx create mode 100644 src/services/hooks/property.ts create mode 100644 src/services/rest/property.ts diff --git a/src/app/(main)/globals.css b/src/app/(main)/globals.css index 29b5ab8..f7699f9 100644 --- a/src/app/(main)/globals.css +++ b/src/app/(main)/globals.css @@ -41,6 +41,9 @@ .rd-nav-link-custom { @apply text-colorHeader! lg:text-colorHeaderText! lg:hover:text-colorHeaderTextHover! } + .rd-nav-link-custom.active { + @apply text-colorHeader! lg:text-colorHeaderTextHover! lg:hover:text-colorHeaderTextHover! + } } @media (prefers-color-scheme: dark) { diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 579f3fd..a7ec3a8 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -1,27 +1,15 @@ -"use client"; - -import CardProduct from "@/components/CardProduct"; -import ContactFormSection from "@/components/ContactFormSection"; import GoogleReviewBox from "@/components/GoogleReviewBox"; -import HeroSlider from "@/components/HeroSlider"; +import HomeTopSection from "@/components/homes/HomeTopSection"; +import Loader from "@/components/loaders/Loader"; +import ListOfFeaturedProperty from "@/components/properties/ListOfFeaturedProperty"; import Image from "next/image"; import Link from "next/link"; -import { useRef } from "react"; +import { Suspense } from "react"; export default function Home() { - const formRef = useRef(null); - - function scrollToForm() { - formRef.current?.scrollIntoView?.({ behavior: "smooth" }); - } - return ( <> - - -
- -
+
@@ -131,29 +119,26 @@ export default function Home() {
-
- - - - - - -
- - + +
+ +
+ + } + > + +
diff --git a/src/app/(main)/property/[slug]/loading.tsx b/src/app/(main)/property/[slug]/loading.tsx new file mode 100644 index 0000000..779bae8 --- /dev/null +++ b/src/app/(main)/property/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import LoaderFixed from "@/components/loaders/LoaderFixed"; + +export default function Loading() { + return ; +} diff --git a/src/collections/Properties.ts b/src/collections/Properties.ts index 1ee7a3f..3e7d0c2 100644 --- a/src/collections/Properties.ts +++ b/src/collections/Properties.ts @@ -179,4 +179,7 @@ export const Properties: CollectionConfig = { group: "Properties", useAsTitle: "name", }, + access: { + read: () => true, + }, }; diff --git a/src/components/homes/HomeTopSection.tsx b/src/components/homes/HomeTopSection.tsx new file mode 100644 index 0000000..2c30b63 --- /dev/null +++ b/src/components/homes/HomeTopSection.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useRef } from "react"; +import HeroSlider from "@/components/HeroSlider"; +import ContactFormSection from "@/components/ContactFormSection"; + +export default function HomeTopSection() { + const formRef = useRef(null); + + function scrollToForm() { + formRef.current?.scrollIntoView?.({ behavior: "smooth" }); + } + + return ( + <> + + +
+ +
+ + ); +} diff --git a/src/components/layouts/Header.tsx b/src/components/layouts/Header.tsx index 304c89e..fc8378f 100644 --- a/src/components/layouts/Header.tsx +++ b/src/components/layouts/Header.tsx @@ -1,4 +1,16 @@ -export default function Header() { +import { headers } from "next/headers"; + +export default async function Header() { + const headerList = await headers(); + const fullUrl = headerList.get("x-full-url"); + + const headerActive = (pathName: string) => { + if (!fullUrl) return ""; + const splittedUrl = fullUrl.split("/"); + + return splittedUrl[3] === pathName ? "active" : ""; + }; + return (
@@ -98,8 +110,8 @@ export default function Header() {
    -
  • - +
  • + HOME
  • @@ -113,17 +125,23 @@ export default function Header() {
  • - + LISTINGS FOR SALE
  • - + LISTINGS FOR RENT
  • - + BLOGS
  • diff --git a/src/components/properties/CardProperty.tsx b/src/components/properties/CardProperty.tsx index 7bfce0e..8c4cf2b 100644 --- a/src/components/properties/CardProperty.tsx +++ b/src/components/properties/CardProperty.tsx @@ -1,7 +1,6 @@ import { CardPropertyData } from "@/schema/property"; import { formatCurrency } from "@/utils/general"; import Image from "next/image"; -import Link from "next/link"; type CardPropertyProps = { data: CardPropertyData; @@ -37,7 +36,7 @@ export default function CardProperty({ data }: CardPropertyProps) {

- {data.title} + {data.title}

    diff --git a/src/components/properties/ListOfFeaturedProperty.tsx b/src/components/properties/ListOfFeaturedProperty.tsx new file mode 100644 index 0000000..b2e16b0 --- /dev/null +++ b/src/components/properties/ListOfFeaturedProperty.tsx @@ -0,0 +1,23 @@ +import { fetchPropertyLatest } from "@/services/payload/property"; +import CardProperty from "./CardProperty"; + +export default async function ListOfFeaturedProperty() { + const latestProperties = await fetchPropertyLatest(); + + return ( + <> +
    + {latestProperties.formattedData.map((pr, idx) => ( + + ))} +
    + {latestProperties.formattedData.length > 0 && ( + + )} + + ); +} diff --git a/src/services/hooks/property.ts b/src/services/hooks/property.ts new file mode 100644 index 0000000..39ae8d8 --- /dev/null +++ b/src/services/hooks/property.ts @@ -0,0 +1,24 @@ +import { CardPropertyData } from "@/schema/property"; +import { useState } from "react"; +import { fetchLatestPropertyREST } from "../rest/property"; + +export function useLatestPropertyQuery() { + const [data, setData] = useState([]); + const [isFetching, setFetching] = useState(false); + + async function _fetch() { + setFetching(true); + const res = await fetchLatestPropertyREST(); + setFetching(false); + + if (Array.isArray(res?.formattedData)) { + setData(res.formattedData); + } + } + + return { + _fetch, + data, + isFetching, + }; +} diff --git a/src/services/payload/property.ts b/src/services/payload/property.ts index 0f1062a..c4d60ba 100644 --- a/src/services/payload/property.ts +++ b/src/services/payload/property.ts @@ -179,3 +179,33 @@ export async function fetchPropertyDetail({ slug }: FetchPropertyDetailParams) { }, }; } + +export async function fetchPropertyLatest() { + const payload = await getPayload({ config: payloadConfig }); + const limitPerPage = 6; + const dataQuery = await payload.find({ + collection: "properties", + limit: limitPerPage, + }); + + const formattedData: CardPropertyData[] = dataQuery.docs.map((item) => { + return { + slug: item.slug ?? "", + title: item.name, + price: item.base_price, + area: item.aboutGroup.area, + propertyType: item.property_type, + bathrooms_count: item.aboutGroup.bathrooms_count, + bedrooms_count: item.aboutGroup.bedrooms_count, + images: item.images.map((img) => + typeof img !== "number" ? { url: img?.url ?? "", alt: img.alt } : { url: "", alt: "" } + ), + posted_at: formatDate(item.createdAt), + }; + }); + + return { + ...dataQuery, + formattedData, + }; +} diff --git a/src/services/rest/property.ts b/src/services/rest/property.ts new file mode 100644 index 0000000..e0cb3ee --- /dev/null +++ b/src/services/rest/property.ts @@ -0,0 +1,50 @@ +import { Property } from "@/payload-types"; +import { CardPropertyData } from "@/schema/property"; +import { formatDate } from "@/utils/datetime"; +import { PaginatedDocs, Where } from "payload"; +import { stringify } from "qs-esm"; + +export async function fetchLatestPropertyREST() { + const limitPerPage = 6; + + const queryCondition: Where = { + _status: { equals: "published" }, + }; + + const queryParams = stringify( + { + page: 1, + pagination: true, + limit: limitPerPage, + where: queryCondition, + }, + { addQueryPrefix: true } + ); + + const req = await fetch(`/api/properties${queryParams}`); + if (req.ok) { + const resData = (await req.json()) as PaginatedDocs; + const formattedData: CardPropertyData[] = resData.docs.map((item) => { + return { + slug: item.slug ?? "", + title: item.name, + price: item.base_price, + area: item.aboutGroup.area, + propertyType: item.property_type, + bathrooms_count: item.aboutGroup.bathrooms_count, + bedrooms_count: item.aboutGroup.bedrooms_count, + images: item.images.map((img) => + typeof img !== "number" ? { url: img?.url ?? "", alt: img.alt } : { url: "", alt: "" } + ), + posted_at: formatDate(item.createdAt), + }; + }); + + return { + ...resData, + formattedData, + }; + } else { + return null; + } +} From a85fba4882abf30c939504db976d49404dd0e970 Mon Sep 17 00:00:00 2001 From: RizqiSyahrendra Date: Thu, 24 Apr 2025 20:01:01 +0700 Subject: [PATCH 2/4] feat: form submission components blocks and styling --- package.json | 2 + public/js/script.js | 266 +++++------ src/blocks/Content.ts | 15 + src/blocks/Form.ts | 33 ++ src/collections/Pages.ts | 92 ++++ src/components/ContactFormBox.tsx | 69 +-- src/components/ContactFormSection.tsx | 69 +-- src/components/HeroSlider.tsx | 6 +- src/components/blocks/Content/index.tsx | 26 ++ src/components/blocks/Form/Button/index.tsx | 54 +++ src/components/blocks/Form/Checkbox/index.tsx | 34 ++ src/components/blocks/Form/Email/index.tsx | 35 ++ src/components/blocks/Form/Error/index.scss | 4 + src/components/blocks/Form/Error/index.tsx | 5 + src/components/blocks/Form/Link/index.tsx | 72 +++ src/components/blocks/Form/Number/index.tsx | 30 ++ src/components/blocks/Form/RichText/index.tsx | 24 + .../blocks/Form/RichText/nodeFormat.tsx | 118 +++++ .../blocks/Form/RichText/serialize.tsx | 173 +++++++ src/components/blocks/Form/Select/index.tsx | 54 +++ src/components/blocks/Form/Text/index.tsx | 30 ++ src/components/blocks/Form/Textarea/index.tsx | 31 ++ src/components/blocks/Form/Width/index.tsx | 13 + .../blocks/Form/buildInitialFormState.tsx | 43 ++ src/components/blocks/Form/fields.tsx | 15 + src/components/blocks/Form/index.tsx | 180 ++++++++ src/components/blocks/Form/shared.scss | 42 ++ src/components/blocks/RenderBlocks.tsx | 56 +++ src/components/homes/HomeTopSection.tsx | 4 +- src/payload-types.ts | 427 ++++++++++++++++++ src/payload.config.ts | 54 ++- src/services/hooks/form.ts | 26 ++ src/services/payload/form.ts | 12 + src/services/rest/form.ts | 11 + yarn.lock | 27 +- 35 files changed, 1900 insertions(+), 252 deletions(-) create mode 100644 src/blocks/Content.ts create mode 100644 src/blocks/Form.ts create mode 100644 src/collections/Pages.ts create mode 100644 src/components/blocks/Content/index.tsx create mode 100644 src/components/blocks/Form/Button/index.tsx create mode 100644 src/components/blocks/Form/Checkbox/index.tsx create mode 100644 src/components/blocks/Form/Email/index.tsx create mode 100644 src/components/blocks/Form/Error/index.scss create mode 100644 src/components/blocks/Form/Error/index.tsx create mode 100644 src/components/blocks/Form/Link/index.tsx create mode 100644 src/components/blocks/Form/Number/index.tsx create mode 100644 src/components/blocks/Form/RichText/index.tsx create mode 100644 src/components/blocks/Form/RichText/nodeFormat.tsx create mode 100644 src/components/blocks/Form/RichText/serialize.tsx create mode 100644 src/components/blocks/Form/Select/index.tsx create mode 100644 src/components/blocks/Form/Text/index.tsx create mode 100644 src/components/blocks/Form/Textarea/index.tsx create mode 100644 src/components/blocks/Form/Width/index.tsx create mode 100644 src/components/blocks/Form/buildInitialFormState.tsx create mode 100644 src/components/blocks/Form/fields.tsx create mode 100644 src/components/blocks/Form/index.tsx create mode 100644 src/components/blocks/Form/shared.scss create mode 100644 src/components/blocks/RenderBlocks.tsx create mode 100644 src/services/hooks/form.ts create mode 100644 src/services/payload/form.ts create mode 100644 src/services/rest/form.ts 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