diff --git a/package.json b/package.json index 66b26e6..d477a0c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@payloadcms/payload-cloud": "^3.35.1", "@payloadcms/richtext-lexical": "^3.35.1", "@payloadcms/storage-s3": "^3.35.1", + "country-state-city": "^3.2.1", "dayjs": "^1.11.13", "graphql": "^16.8.1", "next": "15.3.0", @@ -24,6 +25,7 @@ "qs-esm": "^7.0.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-select": "^5.10.1", "swiper": "^11.2.6" }, "devDependencies": { diff --git a/public/css/style.css b/public/css/style.css index 9b91adc..b223537 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -588,9 +588,9 @@ a:hover { color: #967244; } -a[href*='tel'], a[href*='mailto'] { +/* a[href*='tel'], a[href*='mailto'] { white-space: nowrap; -} +} */ .link-default, .link-default:active, .link-default:focus { color: #424445; @@ -1177,7 +1177,7 @@ a.privacy-link { padding: 12px 11px; color: #9cc1ff; letter-spacing: 0; - background-color: #31323c; + background-color: var(--color-colorContactForm); } .block-callboard a, .block-callboard a:focus, .block-callboard a:active { diff --git a/src/app/(main)/blog/[slug]/page.tsx b/src/app/(main)/blog/[slug]/page.tsx index 204a1f0..8287882 100644 --- a/src/app/(main)/blog/[slug]/page.tsx +++ b/src/app/(main)/blog/[slug]/page.tsx @@ -1,3 +1,4 @@ +import ListOfRecentBlog from "@/components/blogs/ListOfRecentBlog"; import HeroImage from "@/components/HeroImage"; import { fetchBlogDetail } from "@/services/payload/blog"; import { getDefaultMetadata } from "@/utils/metadata"; @@ -110,65 +111,31 @@ export default async function BlogDetail(props: { params: Promise<{ slug: string Share this post
  • - +
  • - +
  • - +
  • -
    -
    -
    Recent Posts
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    + + diff --git a/src/app/(main)/globals.css b/src/app/(main)/globals.css index 08bd10e..29b5ab8 100644 --- a/src/app/(main)/globals.css +++ b/src/app/(main)/globals.css @@ -34,6 +34,7 @@ --color-colorText1: var(--color-colorExt10); --color-colorText2: var(--color-colorExt20); --color-colorLoaderBackground: var(--color-colorExt20); + --color-colorPriceTag: var(--color-colorExt30); } @layer components { diff --git a/src/app/(main)/listings-for-rent/page.tsx b/src/app/(main)/listings-for-rent/page.tsx new file mode 100644 index 0000000..d7c17fe --- /dev/null +++ b/src/app/(main)/listings-for-rent/page.tsx @@ -0,0 +1,95 @@ +import HeroImage from "@/components/HeroImage"; +import Pagination from "@/components/Pagination"; +import CardProperty from "@/components/properties/CardProperty"; +import FilterProperty from "@/components/properties/FilterProperty"; +import { FetchPropertyParams } from "@/schema/services/property"; +import { fetchProperty } from "@/services/payload/property"; +import { getDefaultMetadata } from "@/utils/metadata"; +import { sanitizeNumber, sanitizePageNumber } from "@/utils/sanitize"; +import { Metadata } from "next"; + +const metaDesc = "Explore the latest properties on the Dynamic Realty."; + +export async function generateMetadata(): Promise { + const metadata = await getDefaultMetadata(); + metadata.title = `Listings For Rent - ${metadata.openGraph?.siteName}`; + metadata.description = metaDesc; + + return metadata; +} + +export default async function ListingsForRent(props: { + searchParams?: Promise<{ [P in keyof FetchPropertyParams]: string }>; +}) { + const searchParams = await props?.searchParams; + const page = sanitizePageNumber(searchParams?.page); + const minPrice = sanitizeNumber(searchParams?.min_price); + const maxPrice = sanitizeNumber(searchParams?.max_price); + const minArea = sanitizeNumber(searchParams?.min_area); + const maxArea = sanitizeNumber(searchParams?.max_area); + + const propertiesData = await fetchProperty({ + property_type: "rent", + page, + name: searchParams?.name, + min_price: minPrice, + max_price: maxPrice, + min_area: minArea, + max_area: maxArea, + location: searchParams?.location, + }); + const isEmpty = propertiesData.formattedData.length <= 0; + + return ( + <> + + +
    +
    +
    +
    +
    +
    + {isEmpty && ( +
    +

    No Properties Found

    +

    Looks like we couldn’t find any listings that match your search.

    +
    + )} + {!isEmpty && ( +
    + {propertiesData.formattedData.map((p, idx) => ( + + ))} +
    + )} +
    + + {/* Pagination */} + {propertiesData.totalPages > 1 && ( +
    + +
    + )} + {/* End Pagination */} +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/app/(main)/listings-for-sale/page.tsx b/src/app/(main)/listings-for-sale/page.tsx new file mode 100644 index 0000000..b9757ba --- /dev/null +++ b/src/app/(main)/listings-for-sale/page.tsx @@ -0,0 +1,95 @@ +import HeroImage from "@/components/HeroImage"; +import Pagination from "@/components/Pagination"; +import CardProperty from "@/components/properties/CardProperty"; +import FilterProperty from "@/components/properties/FilterProperty"; +import { FetchPropertyParams } from "@/schema/services/property"; +import { fetchProperty } from "@/services/payload/property"; +import { getDefaultMetadata } from "@/utils/metadata"; +import { sanitizeNumber, sanitizePageNumber } from "@/utils/sanitize"; +import { Metadata } from "next"; + +const metaDesc = "Explore the latest properties on the Dynamic Realty."; + +export async function generateMetadata(): Promise { + const metadata = await getDefaultMetadata(); + metadata.title = `Listings For Sale - ${metadata.openGraph?.siteName}`; + metadata.description = metaDesc; + + return metadata; +} + +export default async function ListingsForRent(props: { + searchParams?: Promise<{ [P in keyof FetchPropertyParams]: string }>; +}) { + const searchParams = await props?.searchParams; + const page = sanitizePageNumber(searchParams?.page); + const minPrice = sanitizeNumber(searchParams?.min_price); + const maxPrice = sanitizeNumber(searchParams?.max_price); + const minArea = sanitizeNumber(searchParams?.min_area); + const maxArea = sanitizeNumber(searchParams?.max_area); + + const propertiesData = await fetchProperty({ + property_type: "sell", + page, + name: searchParams?.name, + min_price: minPrice, + max_price: maxPrice, + min_area: minArea, + max_area: maxArea, + location: searchParams?.location, + }); + const isEmpty = propertiesData.formattedData.length <= 0; + + return ( + <> + + +
    +
    +
    +
    +
    +
    + {isEmpty && ( +
    +

    No Properties Found

    +

    Looks like we couldn’t find any listings that match your search.

    +
    + )} + {!isEmpty && ( +
    + {propertiesData.formattedData.map((p, idx) => ( + + ))} +
    + )} +
    + + {/* Pagination */} + {propertiesData.totalPages > 1 && ( +
    + +
    + )} + {/* End Pagination */} +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/app/(main)/property/[slug]/page.tsx b/src/app/(main)/property/[slug]/page.tsx new file mode 100644 index 0000000..c2e0c13 --- /dev/null +++ b/src/app/(main)/property/[slug]/page.tsx @@ -0,0 +1,473 @@ +import CardProperty from "@/components/properties/CardProperty"; +import HeroImage from "@/components/HeroImage"; +import { fetchPropertyDetail, fetchPropertySuggestion } from "@/services/payload/property"; +import { RichText } from "@payloadcms/richtext-lexical/react"; +import { headers } from "next/headers"; +import Image from "next/image"; +import { notFound } from "next/navigation"; +import FilterProperty from "@/components/properties/FilterProperty"; +import { getDefaultMetadata } from "@/utils/metadata"; +import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize"; +import { Metadata } from "next"; + +export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise { + const metadata = await getDefaultMetadata(); + const params = await props.params; + + let title = "Page"; + let description = "Page"; + let publishedAt = ""; + let updatedAt = ""; + let imgUrl = ""; + let createdByName = ""; + + const property = await fetchPropertyDetail({ slug: params.slug }); + if (!!property) { + // check for property data + title = `${!!property.data?.name ? property.data?.name : ""} - ${metadata.openGraph?.siteName}`; + description = sanitizeBlogContentIntoStringPreview(property.data.aboutGroup.description, 50); + imgUrl = property.formattedData.images.length > 0 ? property.formattedData.images[0].url : ""; + publishedAt = property.data.createdAt; + updatedAt = property.data.updatedAt; + if (!!property?.data?.createdBy && typeof property.data.createdBy !== "number") { + createdByName = property.data.createdBy?.name ?? ""; + } + } + + metadata.title = title; + metadata.description = description; + if (!!metadata.openGraph) { + // @ts-ignore + metadata.openGraph.type = "article"; + metadata.openGraph.title = title; + metadata.openGraph.description = description; + metadata.openGraph.images = !!imgUrl ? [imgUrl] : undefined; + } + metadata.twitter = { + card: "summary_large_image", + title: title, + description: description, + images: !!imgUrl ? [imgUrl] : undefined, + }; + metadata.other = { + "article:published_time": publishedAt, + "article:modified_time": updatedAt, + "twitter:label1": "Written by", + "twitter:data1": !!createdByName ? createdByName : "Admin", + "twitter:label2": "Est. reading time", + "twitter:data2": "3 minutes", + }; + + return metadata; +} + +export default async function PropertyDetail({ params }: { params: Promise<{ slug: string }> }) { + const slug = (await params).slug; + const propertyDetail = await fetchPropertyDetail({ slug }); + if (!propertyDetail) return notFound(); + + const { data, formattedData } = propertyDetail; + const isEmbedMapUrlValid = !!data?.embed_map_url && data.embed_map_url.includes("www.google.com/maps/embed"); + const headersList = await headers(); + const fullUrl = headersList.get("x-full-url"); + const shareUrl = { + facebook: `https://www.facebook.com/sharer/sharer.php?u=${fullUrl}`, + linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${fullUrl}`, + twitter: `https://twitter.com/intent/tweet?url=${fullUrl}`, + }; + + const similarPropertiesData = await fetchPropertySuggestion(); + + return ( + <> + + +
    +
    +
    +
    +
    +
    + {formattedData.price} + {data.property_type === "rent" && "/mo"} +
    + + +
    +
    +
    +
    +
      + {!!data?.aboutGroup?.bathrooms_count && ( +
    • + + {data.aboutGroup.bathrooms_count} Bathrooms +
    • + )} + {!!data?.aboutGroup?.bedrooms_count && ( +
    • + + {data.aboutGroup.bedrooms_count} Bedrooms +
    • + )} + {!!data?.aboutGroup?.area && ( +
    • + + {data.aboutGroup.area} Sq Ft +
    • + )} +
    +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    +
    +
    Address:
    +
    {data?.addressGroup?.address ?? ""}
    +
    +
    +
    State/County:
    +
    {data?.addressGroup?.state_code ?? ""}
    +
    +
    +
    City:
    +
    {data?.addressGroup?.city_code ?? ""}
    +
    +
    +
    Zip:
    +
    {data?.addressGroup?.zip_code ?? ""}
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
      + {Array.isArray(data.features) && + data.features.length > 0 && + data.features.map((ft, idx) =>
    • {typeof ft !== "number" && ft.name}
    • )} +
    +
    +
    +
    +
    + + {data.property_type === "rent" && ( +
    +
    + +
    +
    +
    +
    +
    Base Rent
    +
    {formattedData.price}
    +
    +
    + + {formattedData.additionalPrice.map((p, idx) => ( +
    +
    +
    {p.name}
    +
    {p.price}
    +
    +
    + ))} +
    +
    +
    Est. total monthly*
    +
    {formattedData.totalPrice}
    +
    +
    +
    +
    +
    +
    + )} + + {isEmbedMapUrlValid && ( +
    +

    Property Map

    +
    +
    +