From 1acb2978181f77cc73551dc8e16cb9f82ff0d54f Mon Sep 17 00:00:00 2001 From: RizqiSyahrendra Date: Wed, 23 Apr 2025 11:57:06 +0700 Subject: [PATCH] feat: listings for rent FE integration --- package.json | 1 + .../(main)/listings-for-rent/[slug]/page.tsx | 20 +- src/app/(main)/listings-for-rent/page.tsx | 310 +++++------------- src/collections/Properties.ts | 32 +- src/components/CardProperty.tsx | 94 +++--- src/components/Select.tsx | 8 + src/payload-types.ts | 8 +- src/schema/property.ts | 10 +- src/schema/services/property.ts | 9 + src/services/payload/property.ts | 81 +++++ src/utils/sanitize.ts | 13 + yarn.lock | 21 ++ 12 files changed, 291 insertions(+), 316 deletions(-) create mode 100644 src/components/Select.tsx create mode 100644 src/schema/services/property.ts create mode 100644 src/services/payload/property.ts diff --git a/package.json b/package.json index c2f98df..d477a0c 100644 --- a/package.json +++ b/package.json @@ -25,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/src/app/(main)/listings-for-rent/[slug]/page.tsx b/src/app/(main)/listings-for-rent/[slug]/page.tsx index a6dada7..92fef63 100644 --- a/src/app/(main)/listings-for-rent/[slug]/page.tsx +++ b/src/app/(main)/listings-for-rent/[slug]/page.tsx @@ -5,7 +5,6 @@ import { formatCurrency } from "@/utils/general"; const similarPropertiesData: CardPropertyData[] = [ { - id: 1, title: "401 Biscayne Boulevard, Miami", slug: "401-biscayne-boulevard", images: [ @@ -16,23 +15,20 @@ const similarPropertiesData: CardPropertyData[] = [ price: 5000, propertyType: "rent", posted_at: "", - area: 480, - bathrooms_count: 2, - bedrooms_count: 2, - is_available: true, + area: "480", + bathrooms_count: "2", + bedrooms_count: "2", }, { - id: 2, title: "402 Biscayne Boulevard, Miami", slug: "402-biscayne-boulevard", images: [{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }], price: 5000, propertyType: "rent", posted_at: "", - area: 480, - bathrooms_count: 2, - bedrooms_count: 2, - is_available: true, + area: "480", + bathrooms_count: "2", + bedrooms_count: "2", }, ]; @@ -367,8 +363,8 @@ export default function ListingsForRentDetail() {

Similar Properties

- {similarPropertiesData.map((p) => ( - + {similarPropertiesData.map((p, idx) => ( + ))}
diff --git a/src/app/(main)/listings-for-rent/page.tsx b/src/app/(main)/listings-for-rent/page.tsx index 107775c..bdd1cce 100644 --- a/src/app/(main)/listings-for-rent/page.tsx +++ b/src/app/(main)/listings-for-rent/page.tsx @@ -1,8 +1,12 @@ import CardProperty from "@/components/CardProperty"; import HeroImage from "@/components/HeroImage"; import Pagination from "@/components/Pagination"; -import { CardPropertyData } from "@/schema/property"; +import Select from "@/components/Select"; +import { FetchPropertyParams } from "@/schema/services/property"; +import { fetchProperty } from "@/services/payload/property"; import { getDefaultMetadata } from "@/utils/metadata"; +import { sanitizeNumber, sanitizePageNumber } from "@/utils/sanitize"; +import { State } from "country-state-city"; import { Metadata } from "next"; const metaDesc = "Explore the latest properties on the Dynamic Realty."; @@ -15,66 +19,28 @@ export async function generateMetadata(): Promise { return metadata; } -const propertiesData: CardPropertyData[] = [ - { - id: 1, - title: "401 Biscayne Boulevard, Miami", - slug: "401-biscayne-boulevard", - images: [ - { url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }, - { url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }, - { url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }, - ], - price: 5000, - propertyType: "rent", - posted_at: "", - area: 480, - bathrooms_count: 2, - bedrooms_count: 2, - is_available: true, - }, - { - id: 2, - title: "402 Biscayne Boulevard, Miami", - slug: "402-biscayne-boulevard", - images: [{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }], - price: 5000, - propertyType: "rent", - posted_at: "", - area: 480, - bathrooms_count: 2, - bedrooms_count: 2, - is_available: true, - }, - { - id: 3, - title: "403 Biscayne Boulevard, Miami", - slug: "403-biscayne-boulevard", - images: [{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }], - price: 5000, - propertyType: "rent", - posted_at: "", - area: 480, - bathrooms_count: 2, - bedrooms_count: 2, - is_available: true, - }, - { - id: 4, - title: "404 Biscayne Boulevard, Miami", - slug: "404-biscayne-boulevard", - images: [{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }], - price: 5000, - propertyType: "rent", - posted_at: "", - area: 480, - bathrooms_count: 2, - bedrooms_count: 2, - is_available: true, - }, -]; +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({ + 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; + const statesData = State.getStatesOfCountry("US").map((st) => ({ value: st.name, label: st.name })); -export default function ListingsForRent() { return ( <> @@ -85,67 +51,33 @@ export default function ListingsForRent() {
-
    -
  • -
    -
    - -
    -
    - -
    -
    -
  • -
+ {isEmpty && ( +
+

No Properties Found

+

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

+
+ )} + {!isEmpty && ( +
+ {propertiesData.formattedData.map((p, idx) => ( + + ))} +
+ )}
-
-
- {propertiesData.map((p) => ( - - ))} + + {/* Pagination */} + {propertiesData.totalPages > 1 && ( +
+
-
-
- {/* */} - -
+ )} + {/* End Pagination */}
@@ -153,125 +85,55 @@ export default function ListingsForRent() {

Find Your Property

-
+
- - +
- - -
-
- - + - - - - - - +
- - +
- - +
- - +
diff --git a/src/collections/Properties.ts b/src/collections/Properties.ts index 446bf1b..8e40c19 100644 --- a/src/collections/Properties.ts +++ b/src/collections/Properties.ts @@ -4,6 +4,11 @@ import type { CollectionConfig } from "payload"; export const Properties: CollectionConfig = { slug: "properties", labels: { plural: "Properties", singular: "Property" }, + versions: { + drafts: { + validate: true, + }, + }, fields: [ { name: "propertyType", @@ -76,14 +81,7 @@ export const Properties: CollectionConfig = { { name: "country_code", label: "Country", - type: "select", - options: [ - { - label: "United States", - value: "US", - }, - ], - required: true, + type: "text", // admin: { // components: { // Field: { @@ -95,26 +93,12 @@ export const Properties: CollectionConfig = { { name: "state_code", label: "State", - type: "select", - options: [ - { - label: "Washington", - value: "WA", - }, - ], - required: true, + type: "text", }, { name: "city_code", label: "City", - type: "select", - options: [ - { - label: "Davenport", - value: "Davenport", - }, - ], - required: true, + type: "text", }, { name: "zip_code", diff --git a/src/components/CardProperty.tsx b/src/components/CardProperty.tsx index 1f4b4cf..e87c242 100644 --- a/src/components/CardProperty.tsx +++ b/src/components/CardProperty.tsx @@ -9,55 +9,55 @@ type CardPropertyProps = { export default function CardProperty({ data }: CardPropertyProps) { return ( - <> -
-
-
-
- {Array.isArray(data.images) && - data.images.map((img, idx) => ( - {img.alt - ))} -
-
- - {formatCurrency(data.price)} - {data.propertyType === "rent" && `/mo`} - -
+
+
+
+
+ {Array.isArray(data.images) && + data.images.map((img, idx) => ( +
+ {img.alt +
+ ))}
-

- {data.title} -

-
-
    +
    + + {formatCurrency(data.price)} + {data.propertyType === "rent" && `/mo`} + +
    +
+

+ {data.title} +

+
+
    +
  • + + {data.area} Sq Ft +
  • + {data.bathrooms_count && (
  • - - {data.area} Sq Ft + + {data.bathrooms_count} Bathrooms
  • - {data.bathrooms_count && ( -
  • - - {data.bathrooms_count} Bathrooms -
  • - )} - {data.bedrooms_count && ( -
  • - - {data.bedrooms_count} Bedrooms -
  • - )} -
-
-
- + )} + {data.bedrooms_count && ( +
  • + + {data.bedrooms_count} Bedrooms +
  • + )} + +
    +
    ); } diff --git a/src/components/Select.tsx b/src/components/Select.tsx new file mode 100644 index 0000000..30a28e8 --- /dev/null +++ b/src/components/Select.tsx @@ -0,0 +1,8 @@ +"use client"; + +import { ComponentProps } from "react"; +import ReactSelect from "react-select"; + +export default function Select(props: ComponentProps) { + return ; +} diff --git a/src/payload-types.ts b/src/payload-types.ts index 4740ae2..6a1abbd 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -264,9 +264,9 @@ export interface Property { bedrooms_count: string; }; addressGroup: { - country_code: 'US'; - state_code: 'WA'; - city_code: 'Davenport'; + country_code?: string | null; + state_code?: string | null; + city_code?: string | null; zip_code: string; address: string; }; @@ -282,6 +282,7 @@ export interface Property { embed_map_url?: string | null; updatedAt: string; createdAt: string; + _status?: ('draft' | 'published') | null; } /** * This interface was referenced by `Config`'s JSON-Schema @@ -489,6 +490,7 @@ export interface PropertiesSelect { embed_map_url?: T; updatedAt?: T; createdAt?: T; + _status?: T; } /** * This interface was referenced by `Config`'s JSON-Schema diff --git a/src/schema/property.ts b/src/schema/property.ts index 37eca2b..2268791 100644 --- a/src/schema/property.ts +++ b/src/schema/property.ts @@ -1,16 +1,14 @@ export type CardPropertyData = { - id: number; - slug?: string | null; + slug: string; title: string; price: number; images?: { url: string; alt?: string }[]; /** * in sqft */ - area: number; - bedrooms_count?: number; - bathrooms_count?: number; + area: string; + bedrooms_count?: string; + bathrooms_count?: string; posted_at: string; propertyType: "rent" | "sell"; - is_available: boolean; }; diff --git a/src/schema/services/property.ts b/src/schema/services/property.ts new file mode 100644 index 0000000..e570b1c --- /dev/null +++ b/src/schema/services/property.ts @@ -0,0 +1,9 @@ +export type FetchPropertyParams = { + page?: number; + name?: string; + min_area?: number; + max_area?: number; + min_price?: number; + max_price?: number; + location?: string; +}; diff --git a/src/services/payload/property.ts b/src/services/payload/property.ts new file mode 100644 index 0000000..aaba941 --- /dev/null +++ b/src/services/payload/property.ts @@ -0,0 +1,81 @@ +import payloadConfig from "@/payload.config"; +import { CardPropertyData } from "@/schema/property"; +import { FetchPropertyParams } from "@/schema/services/property"; +import { formatDate } from "@/utils/datetime"; +import { getPayload, Where } from "payload"; + +export async function fetchProperty({ + page, + name = "", + location, + min_price, + max_price, + min_area, + max_area, +}: FetchPropertyParams = {}) { + const payload = await getPayload({ config: payloadConfig }); + + const queryCondition: Where = { + _status: { equals: "published" }, + }; + + if (!!name) { + queryCondition["name"] = { + contains: name, + }; + } + if (!!min_price) { + queryCondition["base_price"] = { + greater_than_equal: min_price, + }; + } + if (!!max_price) { + queryCondition["base_price"] = { + less_than_equal: max_price, + }; + } + if (!!min_area) { + queryCondition["aboutGroup.area"] = { + greater_than_equal: min_area, + }; + } + if (!!max_area) { + queryCondition["aboutGroup.area"] = { + less_than_equal: max_area, + }; + } + if (!!location) { + queryCondition["addressGroup.state_code"] = { + equals: location, + }; + } + + const dataQuery = await payload.find({ + collection: "properties", + page, + pagination: true, + limit: 10, + where: queryCondition, + }); + + const formattedData: CardPropertyData[] = dataQuery.docs.map((item) => { + return { + slug: "", + title: item.name, + price: item.base_price, + area: item.aboutGroup.area, + propertyType: item.propertyType, + 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/utils/sanitize.ts b/src/utils/sanitize.ts index 34432f6..01b04b3 100644 --- a/src/utils/sanitize.ts +++ b/src/utils/sanitize.ts @@ -1,5 +1,18 @@ import { Blog } from "@/payload-types"; +export function sanitizeNumber(input: string | undefined | null): number { + if (!input) return 0; + + const sanitized = parseFloat(input.replace(/[^0-9.-]+/g, "")); + + // Check if the result is a valid number and not NaN or Infinity + if (isNaN(sanitized) || !isFinite(sanitized)) { + return 0; + } + + return sanitized; +} + export function sanitizePageNumber(page: any, defaultPage = 1): number { const parsedPage = Number(page); diff --git a/yarn.lock b/yarn.lock index e7a4fbd..7605fe5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5042,6 +5042,7 @@ __metadata: qs-esm: "npm:^7.0.2" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" + react-select: "npm:^5.10.1" swiper: "npm:^11.2.6" tailwindcss: "npm:^4" typescript: "npm:^5" @@ -8817,6 +8818,26 @@ __metadata: languageName: node linkType: hard +"react-select@npm:^5.10.1": + version: 5.10.1 + resolution: "react-select@npm:5.10.1" + dependencies: + "@babel/runtime": "npm:^7.12.0" + "@emotion/cache": "npm:^11.4.0" + "@emotion/react": "npm:^11.8.1" + "@floating-ui/dom": "npm:^1.0.1" + "@types/react-transition-group": "npm:^4.4.0" + memoize-one: "npm:^6.0.0" + prop-types: "npm:^15.6.0" + react-transition-group: "npm:^4.3.0" + use-isomorphic-layout-effect: "npm:^1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/0d10a249b96150bd648f2575d59c848b8fac7f4d368a97ae84e4aaba5bbc1035deba4cdc82e49a43904b79ec50494505809618b0e98022b2d51e7629551912ed + languageName: node + linkType: hard + "react-transition-group@npm:4.4.5, react-transition-group@npm:^4.3.0": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5"