Merge pull request 'dev' (#11) from dev into main

Reviewed-on: #11
This commit is contained in:
RizqiSyahrendra 2025-04-24 18:57:54 +00:00
commit 4188e0d869
46 changed files with 2128 additions and 292 deletions

View File

@ -15,6 +15,7 @@
"@payloadcms/db-postgres": "^3.35.1", "@payloadcms/db-postgres": "^3.35.1",
"@payloadcms/next": "^3.35.1", "@payloadcms/next": "^3.35.1",
"@payloadcms/payload-cloud": "^3.35.1", "@payloadcms/payload-cloud": "^3.35.1",
"@payloadcms/plugin-form-builder": "^3.35.1",
"@payloadcms/richtext-lexical": "^3.35.1", "@payloadcms/richtext-lexical": "^3.35.1",
"@payloadcms/storage-s3": "^3.35.1", "@payloadcms/storage-s3": "^3.35.1",
"country-state-city": "^3.2.1", "country-state-city": "^3.2.1",
@ -25,7 +26,9 @@
"qs-esm": "^7.0.2", "qs-esm": "^7.0.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.1",
"react-select": "^5.10.1", "react-select": "^5.10.1",
"react-toastify": "^11.0.5",
"swiper": "^11.2.6" "swiper": "^11.2.6"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1482,168 +1482,168 @@ $(function () {
} }
// Select2 // Select2
if (plugins.selectFilter.length) { // if (plugins.selectFilter.length) {
var i; // var i;
for (i = 0; i < plugins.selectFilter.length; i++) { // for (i = 0; i < plugins.selectFilter.length; i++) {
var select = $(plugins.selectFilter[i]); // var select = $(plugins.selectFilter[i]);
select.select2({ // select.select2({
placeholder: select.attr("data-placeholder") ? select.attr("data-placeholder") : false, // placeholder: select.attr("data-placeholder") ? select.attr("data-placeholder") : false,
minimumResultsForSearch: select.attr("data-minimum-results-search") ? select.attr("data-minimum-results-search") : -1, // minimumResultsForSearch: select.attr("data-minimum-results-search") ? select.attr("data-minimum-results-search") : -1,
maximumSelectionSize: 3, // maximumSelectionSize: 3,
dropdownCssClass: select.attr("data-class") ? select.attr("data-class") : '' // dropdownCssClass: select.attr("data-class") ? select.attr("data-class") : ''
}); // });
} // }
} // }
// RD Mailform // RD Mailform
if (plugins.rdMailForm.length) { // if (plugins.rdMailForm.length) {
var i, j, k, // var i, j, k,
msg = { // msg = {
'MF000': 'Successfully sent!', // 'MF000': 'Successfully sent!',
'MF001': 'Recipients are not set!', // 'MF001': 'Recipients are not set!',
'MF002': 'Form will not work locally!', // 'MF002': 'Form will not work locally!',
'MF003': 'Please, define email field in your form!', // 'MF003': 'Please, define email field in your form!',
'MF004': 'Please, define type of your form!', // 'MF004': 'Please, define type of your form!',
'MF254': 'Something went wrong with PHPMailer!', // 'MF254': 'Something went wrong with PHPMailer!',
'MF255': 'Aw, snap! Something went wrong.' // 'MF255': 'Aw, snap! Something went wrong.'
}; // };
for (i = 0; i < plugins.rdMailForm.length; i++) { // for (i = 0; i < plugins.rdMailForm.length; i++) {
var $form = $(plugins.rdMailForm[i]), // var $form = $(plugins.rdMailForm[i]),
formHasCaptcha = false; // formHasCaptcha = false;
$form.attr('novalidate', 'novalidate').ajaxForm({ // $form.attr('novalidate', 'novalidate').ajaxForm({
data: { // data: {
"form-type": $form.attr("data-form-type") || "contact", // "form-type": $form.attr("data-form-type") || "contact",
"counter": i // "counter": i
}, // },
beforeSubmit: function (arr, $form, options) { // beforeSubmit: function (arr, $form, options) {
if (isNoviBuilder) // if (isNoviBuilder)
return; // return;
var form = $(plugins.rdMailForm[this.extraData.counter]), // var form = $(plugins.rdMailForm[this.extraData.counter]),
inputs = form.find("[data-constraints]"), // inputs = form.find("[data-constraints]"),
output = $("#" + form.attr("data-form-output")), // output = $("#" + form.attr("data-form-output")),
captcha = form.find('.recaptcha'), // captcha = form.find('.recaptcha'),
captchaFlag = true; // captchaFlag = true;
output.removeClass("active error success"); // output.removeClass("active error success");
if (isValidated(inputs, captcha)) { // if (isValidated(inputs, captcha)) {
// veify reCaptcha // // veify reCaptcha
if (captcha.length) { // if (captcha.length) {
var captchaToken = captcha.find('.g-recaptcha-response').val(), // var captchaToken = captcha.find('.g-recaptcha-response').val(),
captchaMsg = { // captchaMsg = {
'CPT001': 'Please, setup you "site key" and "secret key" of reCaptcha', // 'CPT001': 'Please, setup you "site key" and "secret key" of reCaptcha',
'CPT002': 'Something wrong with google reCaptcha' // 'CPT002': 'Something wrong with google reCaptcha'
}; // };
formHasCaptcha = true; // formHasCaptcha = true;
$.ajax({ // $.ajax({
method: "POST", // method: "POST",
url: "bat/reCaptcha.php", // url: "bat/reCaptcha.php",
data: { 'g-recaptcha-response': captchaToken }, // data: { 'g-recaptcha-response': captchaToken },
async: false // async: false
}) // })
.done(function (responceCode) { // .done(function (responceCode) {
if (responceCode !== 'CPT000') { // if (responceCode !== 'CPT000') {
if (output.hasClass("snackbars")) { // if (output.hasClass("snackbars")) {
output.html('<p><span class="icon text-middle mdi mdi-check icon-xxs"></span><span>' + captchaMsg[responceCode] + '</span></p>') // output.html('<p><span class="icon text-middle mdi mdi-check icon-xxs"></span><span>' + captchaMsg[responceCode] + '</span></p>')
setTimeout(function () { // setTimeout(function () {
output.removeClass("active"); // output.removeClass("active");
}, 3500); // }, 3500);
captchaFlag = false; // captchaFlag = false;
} else { // } else {
output.html(captchaMsg[responceCode]); // output.html(captchaMsg[responceCode]);
} // }
output.addClass("active"); // output.addClass("active");
} // }
}); // });
} // }
if (!captchaFlag) { // if (!captchaFlag) {
return false; // return false;
} // }
form.addClass('form-in-process'); // form.addClass('form-in-process');
if (output.hasClass("snackbars")) { // if (output.hasClass("snackbars")) {
output.html('<p><span class="icon text-middle fa fa-circle-o-notch fa-spin icon-xxs"></span><span>Sending</span></p>'); // output.html('<p><span class="icon text-middle fa fa-circle-o-notch fa-spin icon-xxs"></span><span>Sending</span></p>');
output.addClass("active"); // output.addClass("active");
} // }
} else { // } else {
return false; // return false;
} // }
}, // },
error: function (result) { // error: function (result) {
if (isNoviBuilder) // if (isNoviBuilder)
return; // return;
var output = $("#" + $(plugins.rdMailForm[this.extraData.counter]).attr("data-form-output")), // var output = $("#" + $(plugins.rdMailForm[this.extraData.counter]).attr("data-form-output")),
form = $(plugins.rdMailForm[this.extraData.counter]); // form = $(plugins.rdMailForm[this.extraData.counter]);
output.text(msg[result]); // output.text(msg[result]);
form.removeClass('form-in-process'); // form.removeClass('form-in-process');
if (formHasCaptcha) { // if (formHasCaptcha) {
grecaptcha.reset(); // grecaptcha.reset();
} // }
}, // },
success: function (result) { // success: function (result) {
if (isNoviBuilder) // if (isNoviBuilder)
return; // return;
var form = $(plugins.rdMailForm[this.extraData.counter]), // var form = $(plugins.rdMailForm[this.extraData.counter]),
output = $("#" + form.attr("data-form-output")), // output = $("#" + form.attr("data-form-output")),
select = form.find('select'); // select = form.find('select');
form // form
.addClass('success') // .addClass('success')
.removeClass('form-in-process'); // .removeClass('form-in-process');
if (formHasCaptcha) { // if (formHasCaptcha) {
grecaptcha.reset(); // grecaptcha.reset();
} // }
result = result.length === 5 ? result : 'MF255'; // result = result.length === 5 ? result : 'MF255';
output.text(msg[result]); // output.text(msg[result]);
if (result === "MF000") { // if (result === "MF000") {
if (output.hasClass("snackbars")) { // if (output.hasClass("snackbars")) {
output.html('<p><span class="icon text-middle mdi mdi-check icon-xxs"></span><span>' + msg[result] + '</span></p>'); // output.html('<p><span class="icon text-middle mdi mdi-check icon-xxs"></span><span>' + msg[result] + '</span></p>');
} else { // } else {
output.addClass("active success"); // output.addClass("active success");
} // }
} else { // } else {
if (output.hasClass("snackbars")) { // if (output.hasClass("snackbars")) {
output.html(' <p class="snackbars-left"><span class="icon icon-xxs mdi mdi-alert-outline text-middle"></span><span>' + msg[result] + '</span></p>'); // output.html(' <p class="snackbars-left"><span class="icon icon-xxs mdi mdi-alert-outline text-middle"></span><span>' + msg[result] + '</span></p>');
} else { // } else {
output.addClass("active error"); // output.addClass("active error");
} // }
} // }
form.clearForm(); // form.clearForm();
if (select.length) { // if (select.length) {
select.select2("val", ""); // select.select2("val", "");
} // }
form.find('input, textarea').trigger('blur'); // form.find('input, textarea').trigger('blur');
setTimeout(function () { // setTimeout(function () {
output.removeClass("active error success"); // output.removeClass("active error success");
form.removeClass('success'); // form.removeClass('success');
}, 3500); // }, 3500);
} // }
}); // });
} // }
} // }
// RD Filepicker // RD Filepicker
if ((plugins.filePicker.length || plugins.fileDrop.length) && !isNoviBuilder) { if ((plugins.filePicker.length || plugins.fileDrop.length) && !isNoviBuilder) {

View File

@ -41,6 +41,9 @@
.rd-nav-link-custom { .rd-nav-link-custom {
@apply text-colorHeader! lg:text-colorHeaderText! lg:hover:text-colorHeaderTextHover! @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) { @media (prefers-color-scheme: dark) {

View File

@ -6,6 +6,7 @@ import { getDefaultMetadata } from "@/utils/metadata";
import Header from "@/components/layouts/Header"; import Header from "@/components/layouts/Header";
import Footer from "@/components/layouts/Footer"; import Footer from "@/components/layouts/Footer";
import Image from "next/image"; import Image from "next/image";
import { ToastContainer } from "react-toastify";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -86,7 +87,8 @@ export default function RootLayout({
<Footer /> <Footer />
</div> </div>
<div className="snackbars" id="form-output-global"></div> <ToastContainer theme="dark" position="bottom-center" stacked />
{/* <div className="snackbars" id="form-output-global"></div> */}
<InitialScript /> <InitialScript />
</body> </body>
</html> </html>

View File

@ -1,27 +1,15 @@
"use client";
import CardProduct from "@/components/CardProduct";
import ContactFormSection from "@/components/ContactFormSection";
import GoogleReviewBox from "@/components/GoogleReviewBox"; 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 Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRef } from "react"; import { Suspense } from "react";
export default function Home() { export default function Home() {
const formRef = useRef<HTMLDivElement | null>(null);
function scrollToForm() {
formRef.current?.scrollIntoView?.({ behavior: "smooth" });
}
return ( return (
<> <>
<HeroSlider onClickBook={scrollToForm} /> <HomeTopSection />
<div className="lg:hidden" ref={formRef}>
<ContactFormSection />
</div>
<section className="section section-lg bg-colorSection1"> <section className="section section-lg bg-colorSection1">
<div className="container"> <div className="container">
@ -131,29 +119,26 @@ export default function Home() {
<div className="layout-4-item"> <div className="layout-4-item">
<ul className="list-inline-bordered heading-7"> <ul className="list-inline-bordered heading-7">
<li> <li>
<a href="#">For Rent</a> <a href="/listings-for-rent">For Rent</a>
</li> </li>
<li> <li>
<a href="#">For Sale</a> <a href="/listings-for-sale">For Sale</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div className="my-10!"> <div className="my-10!">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"> <Suspense
<CardProduct /> fallback={
<CardProduct /> <>
<CardProduct /> <div className="mt-5">
<CardProduct /> <Loader />
<CardProduct /> </div>
<CardProduct /> </>
</div> }
>
<div className="text-center mt-10!"> <ListOfFeaturedProperty />
<a className="button button-primary" href="/"> </Suspense>
View all properties
</a>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -0,0 +1,5 @@
import LoaderFixed from "@/components/loaders/LoaderFixed";
export default function Loading() {
return <LoaderFixed />;
}

15
src/blocks/Content.ts Normal file
View File

@ -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,
},
],
};

33
src/blocks/Form.ts Normal file
View File

@ -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",
},
};

92
src/collections/Pages.ts Normal file
View File

@ -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",
},
};

View File

@ -179,4 +179,7 @@ export const Properties: CollectionConfig = {
group: "Properties", group: "Properties",
useAsTitle: "name", useAsTitle: "name",
}, },
access: {
read: () => true,
},
}; };

View File

@ -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() { export default function ContactFormBox() {
const form = useFormQuery();
useEffect(() => {
form._fetch(1);
}, []);
return ( return (
<div className="lg:w-1/3 box-1-cell height-fill hidden! lg:flex! context-dark bg-colorContactForm/80! z-20!"> <div className="lg:w-1/3 box-1-cell height-fill hidden! lg:flex! context-dark bg-colorContactForm/80! z-20!">
<div className="box-1-bg-shape"> <div className="box-1-bg-shape">
<img className="box-1-bg-image" src="images/bg-shape-1.png" alt="" role="presentation" /> <img className="box-1-bg-image" src="images/bg-shape-1.png" alt="" role="presentation" />
</div> </div>
<div className="cell-inner box-1-outer"> <div className="cell-inner box-1-outer">
<div className="box-1"> <div className="box-1">{!form.isFetching && !!form.data && <FormBlock form={form.data} />}</div>
<h2>Get in Touch with Us Today!</h2>
<form className="rd-form">
<div className="row row-x-20 row-20">
<div className="col-sm-6 col-lg-12 col-xl-6">
<div className="form-wrap form-wrap-validation">
<input className="form-input" placeholder="First Name" name="first_name" />
</div>
</div>
<div className="col-sm-6 col-lg-12 col-xl-6">
<div className="form-wrap form-wrap-validation">
<input className="form-input" placeholder="Last Name" name="last_name" />
</div>
</div>
<div className="col-sm-6 col-lg-12 col-xl-6">
<div className="form-wrap form-wrap-validation">
<input type="email" className="form-input" placeholder="E-Mail" name="email" />
</div>
</div>
<div className="col-sm-6 col-lg-12 col-xl-6">
<div className="form-wrap form-wrap-validation">
<input className="form-input" placeholder="Phone Number" name="phone" />
</div>
</div>
<div className="col-sm-12 col-lg-12 col-xl-12">
<div className="form-wrap form-wrap-validation">
<select
className="form-input select-filter"
name="interest"
data-style="modern"
data-classname="select-dropdown-context-dark"
data-placeholder="I'm interested in"
data-minimum-results-for-search="Infinity"
data-constraints="@Required"
>
<option label="placeholder"></option>
<option value="2">Buying</option>
<option value="3">Selling</option>
<option value="4">Renting</option>
</select>
<span className="select-arrow"></span>
</div>
</div>
<div className="col-sm-12 col-lg-12 col-xl-12">
<div className="form-wrap form-wrap-validation">
<textarea className="form-input" placeholder="Questions/Comments" name="question"></textarea>
</div>
</div>
<div className="col-sm-6 col-lg-12 col-xl-6">
<a className="button button-primary-outline min-w-[150px]" href="search-results.html">
Send
</a>
</div>
</div>
</form>
</div>
</div> </div>
</div> </div>
); );

View File

@ -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() { export default function ContactFormSection() {
const form = useFormQuery();
useEffect(() => {
form._fetch(1);
}, []);
return ( return (
<div className="box-1-cell height-fill context-dark bg-colorContactForm! z-20!"> <div className="box-1-cell height-fill context-dark bg-colorContactForm! z-20!">
<div className="box-1-bg-shape"> <div className="box-1-bg-shape">
<img className="box-1-bg-image" src="images/bg-shape-1.png" alt="" role="presentation" /> <img className="box-1-bg-image" src="images/bg-shape-1.png" alt="" role="presentation" />
</div> </div>
<div className="cell-inner box-1-outer"> <div className="cell-inner box-1-outer">
<div className="box-1"> <div className="box-1">{!form.isFetching && !!form.data && <FormBlock form={form.data} />}</div>
<h2>Get in Touch with Us Today!</h2>
<form className="rd-form">
<div className="row row-x-20 row-20">
<div className="col-sm-6 col-lg-12 col-xl-6">
<div className="form-wrap form-wrap-validation">
<input className="form-input" placeholder="First Name" name="first_name" />
</div>
</div>
<div className="col-sm-6 col-lg-12 col-xl-6">
<div className="form-wrap form-wrap-validation">
<input className="form-input" placeholder="Last Name" name="last_name" />
</div>
</div>
<div className="col-sm-6 col-lg-12 col-xl-6">
<div className="form-wrap form-wrap-validation">
<input type="email" className="form-input" placeholder="E-Mail" name="email" />
</div>
</div>
<div className="col-sm-6 col-lg-12 col-xl-6">
<div className="form-wrap form-wrap-validation">
<input className="form-input" placeholder="Phone Number" name="phone" />
</div>
</div>
<div className="col-sm-12 col-lg-12 col-xl-12">
<div className="form-wrap form-wrap-validation">
<select
className="form-input select-filter"
name="interest"
data-style="modern"
data-classname="select-dropdown-context-dark"
data-placeholder="I'm interested in"
data-minimum-results-for-search="Infinity"
data-constraints="@Required"
>
<option label="placeholder"></option>
<option value="2">Buying</option>
<option value="3">Selling</option>
<option value="4">Renting</option>
</select>
<span className="select-arrow"></span>
</div>
</div>
<div className="col-sm-12 col-lg-12 col-xl-12">
<div className="form-wrap form-wrap-validation">
<textarea className="form-input" placeholder="Questions/Comments" name="question"></textarea>
</div>
</div>
<div className="col-sm-6 col-lg-12 col-xl-6">
<a className="button button-primary-outline min-w-[150px]" href="search-results.html">
Send
</a>
</div>
</div>
</form>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,10 +1,10 @@
"use client"; "use client";
import Image from "next/image";
import { useState } from "react";
import { Autoplay } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import ContactFormBox from "./ContactFormBox"; import ContactFormBox from "./ContactFormBox";
import { Autoplay } from "swiper/modules";
import { useState } from "react";
import Image from "next/image";
type HeroSliderProps = { type HeroSliderProps = {
onClickBook?: () => void; onClickBook?: () => void;

View File

@ -0,0 +1,26 @@
import { RichText } from "@payloadcms/richtext-lexical/react";
// type Props = extract
export function ContentBlock(props: any) {
return (
<div className="container relative">
<div className="row">
{/* Content */}
<div className="col-md-10 offset-md-1 col-lg-10 offset-lg-1">
{/* Post */}
<div className="blog-item mb-10">
<div className="blog-item-body">
<div>
{/* @ts-ignore */}
<RichText data={props.content} />
</div>
</div>
</div>
{/* End Post */}
</div>
{/* End Content */}
</div>
</div>
);
}

View File

@ -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<Props> = ({
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 (
<Element {...elementProps} className="button button-primary-outline" {...{ disabled: isLoading }}>
<React.Fragment>
{(el === "link" || el === "a") && (
<Link {...newTabProps} href={href || ""}>
{label}
</Link>
)}
{el === "button" && (
<>
{label} {isLoading && <span className="fa fa-spinner animate-spin ml-2" />}
</>
)}
</React.Fragment>
</Element>
);
};

View File

@ -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<any & FieldValues>;
setValue: any;
} & CheckboxField
> = ({ name, errors, label, register, required: requiredFromProps, width }) => {
return (
<Width width={width}>
<div className="form-wrap pr-4">
<div className="flex">
<input className="form-input mr-2 p-2" type="checkbox" {...register(name, { required: requiredFromProps })} />
<label className="form-label" htmlFor={name}>
{label}
</label>
</div>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
);
};

View File

@ -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<any & FieldValues>;
} & EmailField
> = ({ name, errors, label, register, required: requiredFromProps, width }) => {
return (
<Width width={width}>
<div className="form-wrap">
<input
type="email"
className="form-input"
id={name}
{...register(name, { pattern: /^\S[^\s@]*@\S+$/, required: requiredFromProps })}
/>
<label className="form-label" htmlFor={name}>
{label}
</label>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
);
};

View File

@ -0,0 +1,4 @@
.error {
margin-top: 5px;
color: var(--color-red);
}

View File

@ -0,0 +1,5 @@
import * as React from "react";
export const Error: React.FC = () => {
return <div className="text-red-500">This field is required</div>;
};

View File

@ -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<CMSLinkType> = ({
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 (
<a href={url || ""} {...newTabProps} className={className}>
{label && label}
{children ? <>{children}</> : null}
</a>
);
}
if (href) {
return (
<Link href={href} {...newTabProps} className={className} prefetch={false}>
{label && label}
{children ? <>{children}</> : null}
</Link>
);
}
}
const buttonProps = {
appearance,
href,
label,
newTab,
};
return <Button className={className} {...buttonProps} el="link" />;
};

View File

@ -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<any & FieldValues>;
} & TextField
> = ({ name, errors, label, register, required: requiredFromProps, width }) => {
return (
<Width width={width}>
<div className="form-wrap">
<input type="number" className="form-input" id={name} {...register(name, { required: requiredFromProps })} />
<label className="form-label" htmlFor={name}>
{label}
</label>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
);
};

View File

@ -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 (
<div className={[className].filter(Boolean).join(' ')}>
{content &&
!Array.isArray(content) &&
typeof content === 'object' &&
'root' in content &&
serializeLexical({ nodes: content?.root?.children })}
</div>
)
}
export default RichText

View File

@ -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<string | TextFormatType, number> = {
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<string | TextDetailType, number> = {
directionless: IS_DIRECTIONLESS,
unmergeable: IS_UNMERGEABLE,
};
export const ELEMENT_TYPE_TO_FORMAT: Record<Exclude<ElementFormatType, "">, 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<number, ElementFormatType> = {
[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<TextModeType, 0 | 1 | 2> = {
normal: IS_NORMAL,
segmented: IS_SEGMENTED,
token: IS_TOKEN,
};
export const TEXT_TYPE_TO_MODE: Record<number, TextModeType> = {
[IS_NORMAL]: "normal",
[IS_SEGMENTED]: "segmented",
[IS_TOKEN]: "token",
};

View File

@ -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 (
<Fragment>
{nodes?.map((_node, index): JSX.Element | null => {
if (_node.type === "text") {
const node = _node as SerializedTextNode;
let text = <React.Fragment key={index}>{node.text}</React.Fragment>;
if (node.format & IS_BOLD) {
text = <strong key={index}>{text}</strong>;
}
if (node.format & IS_ITALIC) {
text = <em key={index}>{text}</em>;
}
if (node.format & IS_STRIKETHROUGH) {
text = (
<span key={index} style={{ textDecoration: "line-through" }}>
{text}
</span>
);
}
if (node.format & IS_UNDERLINE) {
text = (
<span key={index} style={{ textDecoration: "underline" }}>
{text}
</span>
);
}
if (node.format & IS_CODE) {
text = <code key={index}>{node.text}</code>;
}
if (node.format & IS_SUBSCRIPT) {
text = <sub key={index}>{text}</sub>;
}
if (node.format & IS_SUPERSCRIPT) {
text = <sup key={index}>{text}</sup>;
}
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<keyof JSX.IntrinsicElements, "h1" | "h2" | "h3" | "h4" | "h5">;
const Tag = node?.tag as Heading;
return <Tag key={index}>{serializedChildren}</Tag>;
}
case "linebreak": {
return <br key={index} />;
}
case "link": {
const node = _node as SerializedLinkNode;
const fields: LinkFields = node.fields;
return (
<CMSLink
key={index}
newTab={Boolean(fields?.newTab)}
reference={fields.doc as any}
type={fields.linkType === "internal" ? "reference" : "custom"}
url={fields.url}
>
{serializedChildren}
</CMSLink>
);
}
case "list": {
const node = _node as SerializedListNode;
type List = Extract<keyof JSX.IntrinsicElements, "ol" | "ul">;
const Tag = node?.tag as List;
return (
<Tag className="list" key={index}>
{serializedChildren}
</Tag>
);
}
case "listitem": {
const node = _node as SerializedListItemNode;
if (node?.checked != null) {
return (
<li
aria-checked={node.checked ? "true" : "false"}
className={` ${node.checked ? "" : ""}`}
key={index}
role="checkbox"
tabIndex={-1}
value={node?.value}
>
{serializedChildren}
</li>
);
} else {
return (
<li key={index} value={node?.value}>
{serializedChildren}
</li>
);
}
}
case "paragraph": {
return <p key={index}>{serializedChildren}</p>;
}
case "quote": {
return <blockquote key={index}>{serializedChildren}</blockquote>;
}
default:
return null;
}
})}
</Fragment>
);
}

View File

@ -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<FieldValues, any>;
errors: Partial<
FieldErrorsImpl<{
[x: string]: any;
}>
>;
} & SelectField
> = ({ name, control, errors, label, options, required, width }) => {
return (
<Width width={width}>
<div className="form-wrap relative">
<Controller
control={control}
defaultValue=""
name={name}
render={({ field: { onChange, value } }) => (
<>
{/* <ReactSelect
classNamePrefix="rs"
inputId={name}
instanceId={name}
onChange={(val) => onChange(val ? val.value : "")}
options={options}
value={options.find((s) => s.value === value)}
/> */}
<select className="form-input select-filter" name="interest" onChange={onChange} value={value}>
<option value="">{label}</option>
{options.map((opt, idx) => (
<option key={idx} value={opt.value}>
{opt.label}
</option>
))}
</select>
<span className="absolute right-2 top-3 fa fa-caret-down text-black"></span>
</>
)}
rules={{ required }}
/>
{required && errors[name] && <Error />}
</div>
</Width>
);
};

View File

@ -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<any & FieldValues>;
} & TextField
> = ({ name, errors, label, register, required: requiredFromProps, width }) => {
return (
<Width width={width}>
<div className="form-wrap">
<input type="text" id={name} className="form-input" {...register(name, { required: requiredFromProps })} />
<label className="form-label" htmlFor={name}>
{label}
</label>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
);
};

View File

@ -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<any & FieldValues>;
rows?: number;
} & TextField
> = ({ name, errors, label, register, required: requiredFromProps, rows = 3, width }) => {
return (
<Width width={width}>
<div className="form-wrap">
<textarea className="form-input" id={name} rows={rows} {...register(name, { required: requiredFromProps })} />
<label className="form-label" htmlFor={name}>
{label}
</label>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
);
};

View File

@ -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 <div className={`col-sm-6 col-lg-12 ${colXl}`}>{children}</div>;
};

View File

@ -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]: "",
};
}
}, {});
};

View File

@ -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,
};

View File

@ -0,0 +1,189 @@
// @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";
import { toast } from "react-toastify";
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,
reset,
} = formMethods;
const [isLoading, setIsLoading] = useState(false);
const [hasSubmitted, setHasSubmitted] = useState<boolean>();
const [error, setError] = useState<{ message: string; status?: string } | undefined>();
const router = useRouter();
const onSubmit = useCallback(
(data: Data) => {
let loadingTimerID: ReturnType<typeof setTimeout>;
const submitForm = async () => {
setError(undefined);
const dataToSend = Object.entries(data).map(([name, value]) => ({
field: name,
value,
}));
const toastId = toast.loading("Sending...");
// 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);
toast.error(res.errors?.[0]?.message || "Internal Server Error");
toast.dismiss(toastId);
setError({
message: res.errors?.[0]?.message || "Internal Server Error",
status: res.status,
});
return;
}
toast.success(<RichText content={confirmationMessage} />);
toast.dismiss(toastId);
setIsLoading(false);
setHasSubmitted(true);
reset();
if (confirmationType === "redirect" && redirect) {
const { url } = redirect;
const redirectUrl = url;
if (redirectUrl) router.push(redirectUrl);
}
} catch (err) {
console.warn(err);
toast.error("Something went wrong");
toast.dismiss(toastId);
setIsLoading(false);
setError({
message: "Something went wrong",
});
}
};
void submitForm();
},
[router, formID, redirect, confirmationType]
);
return (
<div>
{!!formFromProps?.title && <h2>{formFromProps.title}</h2>}
<div className="mt-4">
{!!enableIntro && introContent && !hasSubmitted && <RichText content={introContent} />}
{isLoading && !hasSubmitted && <p>Loading, please wait...</p>}
{error && <div className="text-red-500">{`${error.status || "500"}: ${error.message || ""}`}</div>}
<form id={formID} onSubmit={handleSubmit(onSubmit)} className="rd-form">
<div className="row space-y-4">
{formFromProps &&
formFromProps.fields &&
formFromProps.fields.map((field, index) => {
const Field: React.FC<any> = fields?.[field.blockType];
if (Field) {
return (
<React.Fragment key={index}>
<Field
form={formFromProps}
{...field}
{...formMethods}
control={control}
errors={errors}
register={register}
/>
</React.Fragment>
);
}
return null;
})}
</div>
<Button appearance="primary" el="button" form={formID} label={submitButtonLabel} isLoading={isLoading} />
</form>
</div>
</div>
);
};

View File

@ -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);
}
}
}

View File

@ -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 (
<Fragment>
{blocks.map((block, index) => {
const { blockType } = block;
if (blockType && blockType in blockComponents) {
const Block = blockComponents[blockType];
if (!Block) return null;
if (blockType === "formBlock") {
return (
<div className="my-10" key={index}>
<FormBlock
id={block.id}
enableIntro={block.enableIntro}
introContent={block.introContent}
form={block.form}
/>
</div>
);
} else {
return (
<div className="my-10" key={index}>
{/* @ts-ignore */}
<Block {...block} disableInnerContainer />
</div>
);
}
}
return null;
})}
</Fragment>
);
}
return null;
};

View File

@ -0,0 +1,23 @@
"use client";
import HeroSlider from "@/components/HeroSlider";
import { useRef } from "react";
import ContactFormSection from "../ContactFormSection";
export default function HomeTopSection() {
const formRef = useRef<HTMLDivElement | null>(null);
function scrollToForm() {
formRef.current?.scrollIntoView?.({ behavior: "smooth" });
}
return (
<>
<HeroSlider onClickBook={scrollToForm} />
<div className="lg:hidden" ref={formRef}>
<ContactFormSection />
</div>
</>
);
}

View File

@ -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 ( return (
<header className="section page-header"> <header className="section page-header">
<div className="rd-navbar-wrap"> <div className="rd-navbar-wrap">
@ -98,8 +110,8 @@ export default function Header() {
</div> </div>
<div className="rd-navbar-nav-wrap"> <div className="rd-navbar-nav-wrap">
<ul className="rd-navbar-nav"> <ul className="rd-navbar-nav">
<li className="rd-nav-item active"> <li className="rd-nav-item">
<a className="rd-nav-link" href="/"> <a className={`rd-nav-link rd-nav-link-custom ${headerActive("")}`} href="/">
HOME HOME
</a> </a>
</li> </li>
@ -113,17 +125,23 @@ export default function Header() {
</a> </a>
</li> </li>
<li className="rd-nav-item"> <li className="rd-nav-item">
<a className="rd-nav-link rd-nav-link-custom" href="/listings-for-sale"> <a
className={`rd-nav-link rd-nav-link-custom ${headerActive("listings-for-sale")}`}
href="/listings-for-sale"
>
LISTINGS FOR SALE LISTINGS FOR SALE
</a> </a>
</li> </li>
<li className="rd-nav-item"> <li className="rd-nav-item">
<a className="rd-nav-link rd-nav-link-custom" href="/listings-for-rent"> <a
className={`rd-nav-link rd-nav-link-custom ${headerActive("listings-for-rent")}`}
href="/listings-for-rent"
>
LISTINGS FOR RENT LISTINGS FOR RENT
</a> </a>
</li> </li>
<li className="rd-nav-item"> <li className="rd-nav-item">
<a className="rd-nav-link rd-nav-link-custom" href="/blog"> <a className={`rd-nav-link rd-nav-link-custom ${headerActive("blog")}`} href="/blog">
BLOGS BLOGS
</a> </a>
</li> </li>

View File

@ -1,7 +1,6 @@
import { CardPropertyData } from "@/schema/property"; import { CardPropertyData } from "@/schema/property";
import { formatCurrency } from "@/utils/general"; import { formatCurrency } from "@/utils/general";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
type CardPropertyProps = { type CardPropertyProps = {
data: CardPropertyData; data: CardPropertyData;
@ -37,7 +36,7 @@ export default function CardProperty({ data }: CardPropertyProps) {
</div> </div>
</div> </div>
<h4 className="product-classic-title"> <h4 className="product-classic-title">
<Link href={href}>{data.title}</Link> <a href={href}>{data.title}</a>
</h4> </h4>
<div className="product-classic-divider"></div> <div className="product-classic-divider"></div>
<ul className="product-classic-list"> <ul className="product-classic-list">

View File

@ -0,0 +1,23 @@
import { fetchPropertyLatest } from "@/services/payload/property";
import CardProperty from "./CardProperty";
export default async function ListOfFeaturedProperty() {
const latestProperties = await fetchPropertyLatest();
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{latestProperties.formattedData.map((pr, idx) => (
<CardProperty key={idx} data={pr} />
))}
</div>
{latestProperties.formattedData.length > 0 && (
<div className="text-center mt-10!">
<a className="button button-primary" href="/listings-for-sale">
View all properties
</a>
</div>
)}
</>
);
}

View File

@ -74,6 +74,9 @@ export interface Config {
blogs: Blog; blogs: Blog;
propertyFeatures: PropertyFeature; propertyFeatures: PropertyFeature;
properties: Property; properties: Property;
pages: Page;
forms: Form;
'form-submissions': FormSubmission;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
@ -87,6 +90,9 @@ export interface Config {
blogs: BlogsSelect<false> | BlogsSelect<true>; blogs: BlogsSelect<false> | BlogsSelect<true>;
propertyFeatures: PropertyFeaturesSelect<false> | PropertyFeaturesSelect<true>; propertyFeatures: PropertyFeaturesSelect<false> | PropertyFeaturesSelect<true>;
properties: PropertiesSelect<false> | PropertiesSelect<true>; properties: PropertiesSelect<false> | PropertiesSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
forms: FormsSelect<false> | FormsSelect<true>;
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>; 'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@ -285,6 +291,245 @@ export interface Property {
createdAt: string; createdAt: string;
_status?: ('draft' | 'published') | null; _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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
@ -319,6 +564,18 @@ export interface PayloadLockedDocument {
| ({ | ({
relationTo: 'properties'; relationTo: 'properties';
value: number | Property; value: number | Property;
} | null)
| ({
relationTo: 'pages';
value: number | Page;
} | null)
| ({
relationTo: 'forms';
value: number | Form;
} | null)
| ({
relationTo: 'form-submissions';
value: number | FormSubmission;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
@ -494,6 +751,176 @@ export interface PropertiesSelect<T extends boolean = true> {
createdAt?: T; createdAt?: T;
_status?: T; _status?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
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<T extends boolean = true> {
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<T extends boolean = true> {
form?: T;
submissionData?:
| T
| {
field?: T;
value?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select". * via the `definition` "payload-locked-documents_select".

View File

@ -15,6 +15,8 @@ import { BlogCategories } from "@/collections/BlogCategories";
import { Blogs } from "@/collections/Blogs"; import { Blogs } from "@/collections/Blogs";
import { PropertyFeatures } from "@/collections/PropertyFeatures"; import { PropertyFeatures } from "@/collections/PropertyFeatures";
import { Properties } from "./collections/Properties"; import { Properties } from "./collections/Properties";
import { Pages } from "@/collections/Pages";
import { formBuilderPlugin } from "@payloadcms/plugin-form-builder";
const filename = fileURLToPath(import.meta.url); const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename); const dirname = path.dirname(filename);
@ -39,7 +41,7 @@ export default buildConfig({
}, },
theme: "dark", theme: "dark",
}, },
collections: [Users, Media, BlogTags, BlogCategories, Blogs, PropertyFeatures, Properties], collections: [Users, Media, BlogTags, BlogCategories, Blogs, PropertyFeatures, Properties, Pages],
editor: lexicalEditor(), editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || "", secret: process.env.PAYLOAD_SECRET || "",
typescript: { typescript: {
@ -71,5 +73,55 @@ export default buildConfig({
endpoint: process.env.S3_ENDPOINT, 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<string, any> = {};
// 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;
// },
}),
], ],
}); });

View File

@ -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<Form | null>(null);
const [isFetching, setFetching] = useState(false);
async function _fetch(id: number) {
setFetching(true);
const res = await fetchFormREST(id);
setFetching(false);
if (!!res) {
setData(res);
} else {
setData(null);
}
}
return {
_fetch,
data,
isFetching,
};
}

View File

@ -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<CardPropertyData[]>([]);
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,
};
}

View File

@ -0,0 +1,12 @@
import payloadConfig from "@/payload.config";
import { getPayload } from "payload";
export async function fetchForm(formID: number) {
const payload = await getPayload({ config: payloadConfig });
const formRes = await payload.findByID({
collection: "forms",
id: formID,
});
return formRes;
}

View File

@ -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,
};
}

11
src/services/rest/form.ts Normal file
View File

@ -0,0 +1,11 @@
import { Form } from "@/payload-types";
export async function fetchFormREST(id: number) {
const req = await fetch(`/api/forms/${id}`);
if (req.ok) {
const resData = (await req.json()) as Form;
return resData;
} else {
return null;
}
}

View File

@ -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<Property>;
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;
}
}

View File

@ -2673,6 +2673,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@payloadcms/plugin-form-builder@npm:^3.35.1":
version: 3.35.1
resolution: "@payloadcms/plugin-form-builder@npm:3.35.1"
dependencies:
"@payloadcms/ui": "npm:3.35.1"
escape-html: "npm:^1.0.3"
peerDependencies:
payload: 3.35.1
react: ^19.0.0 || ^19.0.0-rc-65a56d0e-20241020
react-dom: ^19.0.0 || ^19.0.0-rc-65a56d0e-20241020
checksum: 10c0/48a7f5ecbf0c7c3b46fcc0a92ca3c2ff3c759f16c568973c19ccd8c38cfc93ffdd8ddbee76266ac7063de3d6b1ba9916da07ea13fd6828ad12ae6be7ea2cca8f
languageName: node
linkType: hard
"@payloadcms/richtext-lexical@npm:^3.35.1": "@payloadcms/richtext-lexical@npm:^3.35.1":
version: 3.35.1 version: 3.35.1
resolution: "@payloadcms/richtext-lexical@npm:3.35.1" resolution: "@payloadcms/richtext-lexical@npm:3.35.1"
@ -5023,6 +5037,7 @@ __metadata:
"@payloadcms/db-postgres": "npm:^3.35.1" "@payloadcms/db-postgres": "npm:^3.35.1"
"@payloadcms/next": "npm:^3.35.1" "@payloadcms/next": "npm:^3.35.1"
"@payloadcms/payload-cloud": "npm:^3.35.1" "@payloadcms/payload-cloud": "npm:^3.35.1"
"@payloadcms/plugin-form-builder": "npm:^3.35.1"
"@payloadcms/richtext-lexical": "npm:^3.35.1" "@payloadcms/richtext-lexical": "npm:^3.35.1"
"@payloadcms/storage-s3": "npm:^3.35.1" "@payloadcms/storage-s3": "npm:^3.35.1"
"@tailwindcss/postcss": "npm:^4" "@tailwindcss/postcss": "npm:^4"
@ -5042,7 +5057,9 @@ __metadata:
qs-esm: "npm:^7.0.2" qs-esm: "npm:^7.0.2"
react: "npm:^19.0.0" react: "npm:^19.0.0"
react-dom: "npm:^19.0.0" react-dom: "npm:^19.0.0"
react-hook-form: "npm:^7.56.1"
react-select: "npm:^5.10.1" react-select: "npm:^5.10.1"
react-toastify: "npm:^11.0.5"
swiper: "npm:^11.2.6" swiper: "npm:^11.2.6"
tailwindcss: "npm:^4" tailwindcss: "npm:^4"
typescript: "npm:^5" typescript: "npm:^5"
@ -5510,7 +5527,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"escape-html@npm:1.0.3": "escape-html@npm:1.0.3, escape-html@npm:^1.0.3":
version: 1.0.3 version: 1.0.3
resolution: "escape-html@npm:1.0.3" resolution: "escape-html@npm:1.0.3"
checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3
@ -8782,6 +8799,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-hook-form@npm:^7.56.1":
version: 7.56.1
resolution: "react-hook-form@npm:7.56.1"
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
checksum: 10c0/26eafd54bf47167628e34c2f22ea27ea7bdd5b4231a56af5be31ccedfb2a721708b308026ea4a17f2fb15c7b5fff83c0feb4acc7aa07bd6faa8506e917810cab
languageName: node
linkType: hard
"react-image-crop@npm:10.1.8": "react-image-crop@npm:10.1.8":
version: 10.1.8 version: 10.1.8
resolution: "react-image-crop@npm:10.1.8" resolution: "react-image-crop@npm:10.1.8"
@ -8838,6 +8864,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-toastify@npm:^11.0.5":
version: 11.0.5
resolution: "react-toastify@npm:11.0.5"
dependencies:
clsx: "npm:^2.1.1"
peerDependencies:
react: ^18 || ^19
react-dom: ^18 || ^19
checksum: 10c0/50f5b81323ebb1957b2efd0963fac24aa1407155d16ab756ffd6d0f42f8af17e796b3958a9fce13e9d1b945d6c3a5a9ebf13529478474d8a2af4bf1dd0db67d2
languageName: node
linkType: hard
"react-transition-group@npm:4.4.5, react-transition-group@npm:^4.3.0": "react-transition-group@npm:4.4.5, react-transition-group@npm:^4.3.0":
version: 4.4.5 version: 4.4.5
resolution: "react-transition-group@npm:4.4.5" resolution: "react-transition-group@npm:4.4.5"