feat: listings for rent FE integration
This commit is contained in:
parent
2dbf9e1ad4
commit
1acb297818
@ -25,6 +25,7 @@
|
|||||||
"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-select": "^5.10.1",
|
||||||
"swiper": "^11.2.6"
|
"swiper": "^11.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -5,7 +5,6 @@ import { formatCurrency } from "@/utils/general";
|
|||||||
|
|
||||||
const similarPropertiesData: CardPropertyData[] = [
|
const similarPropertiesData: CardPropertyData[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
|
||||||
title: "401 Biscayne Boulevard, Miami",
|
title: "401 Biscayne Boulevard, Miami",
|
||||||
slug: "401-biscayne-boulevard",
|
slug: "401-biscayne-boulevard",
|
||||||
images: [
|
images: [
|
||||||
@ -16,23 +15,20 @@ const similarPropertiesData: CardPropertyData[] = [
|
|||||||
price: 5000,
|
price: 5000,
|
||||||
propertyType: "rent",
|
propertyType: "rent",
|
||||||
posted_at: "",
|
posted_at: "",
|
||||||
area: 480,
|
area: "480",
|
||||||
bathrooms_count: 2,
|
bathrooms_count: "2",
|
||||||
bedrooms_count: 2,
|
bedrooms_count: "2",
|
||||||
is_available: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
|
||||||
title: "402 Biscayne Boulevard, Miami",
|
title: "402 Biscayne Boulevard, Miami",
|
||||||
slug: "402-biscayne-boulevard",
|
slug: "402-biscayne-boulevard",
|
||||||
images: [{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }],
|
images: [{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }],
|
||||||
price: 5000,
|
price: 5000,
|
||||||
propertyType: "rent",
|
propertyType: "rent",
|
||||||
posted_at: "",
|
posted_at: "",
|
||||||
area: 480,
|
area: "480",
|
||||||
bathrooms_count: 2,
|
bathrooms_count: "2",
|
||||||
bedrooms_count: 2,
|
bedrooms_count: "2",
|
||||||
is_available: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -367,8 +363,8 @@ export default function ListingsForRentDetail() {
|
|||||||
<div className="block-group-item">
|
<div className="block-group-item">
|
||||||
<h3>Similar Properties</h3>
|
<h3>Similar Properties</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{similarPropertiesData.map((p) => (
|
{similarPropertiesData.map((p, idx) => (
|
||||||
<CardProperty key={p.id} data={p} />
|
<CardProperty key={idx} data={p} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import CardProperty from "@/components/CardProperty";
|
import CardProperty from "@/components/CardProperty";
|
||||||
import HeroImage from "@/components/HeroImage";
|
import HeroImage from "@/components/HeroImage";
|
||||||
import Pagination from "@/components/Pagination";
|
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 { getDefaultMetadata } from "@/utils/metadata";
|
||||||
|
import { sanitizeNumber, sanitizePageNumber } from "@/utils/sanitize";
|
||||||
|
import { State } from "country-state-city";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
const metaDesc = "Explore the latest properties on the Dynamic Realty.";
|
const metaDesc = "Explore the latest properties on the Dynamic Realty.";
|
||||||
@ -15,66 +19,28 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
const propertiesData: CardPropertyData[] = [
|
export default async function ListingsForRent(props: {
|
||||||
{
|
searchParams?: Promise<{ [P in keyof FetchPropertyParams]: string }>;
|
||||||
id: 1,
|
}) {
|
||||||
title: "401 Biscayne Boulevard, Miami",
|
const searchParams = await props?.searchParams;
|
||||||
slug: "401-biscayne-boulevard",
|
const page = sanitizePageNumber(searchParams?.page);
|
||||||
images: [
|
const minPrice = sanitizeNumber(searchParams?.min_price);
|
||||||
{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" },
|
const maxPrice = sanitizeNumber(searchParams?.max_price);
|
||||||
{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" },
|
const minArea = sanitizeNumber(searchParams?.min_area);
|
||||||
{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" },
|
const maxArea = sanitizeNumber(searchParams?.max_area);
|
||||||
],
|
|
||||||
price: 5000,
|
const propertiesData = await fetchProperty({
|
||||||
propertyType: "rent",
|
page,
|
||||||
posted_at: "",
|
name: searchParams?.name,
|
||||||
area: 480,
|
min_price: minPrice,
|
||||||
bathrooms_count: 2,
|
max_price: maxPrice,
|
||||||
bedrooms_count: 2,
|
min_area: minArea,
|
||||||
is_available: true,
|
max_area: maxArea,
|
||||||
},
|
location: searchParams?.location,
|
||||||
{
|
});
|
||||||
id: 2,
|
const isEmpty = propertiesData.formattedData.length <= 0;
|
||||||
title: "402 Biscayne Boulevard, Miami",
|
const statesData = State.getStatesOfCountry("US").map((st) => ({ value: st.name, label: st.name }));
|
||||||
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 function ListingsForRent() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeroImage title="Listings For Rent" />
|
<HeroImage title="Listings For Rent" />
|
||||||
@ -85,67 +51,33 @@ export default function ListingsForRent() {
|
|||||||
<div className="col-lg-7 col-xl-8">
|
<div className="col-lg-7 col-xl-8">
|
||||||
<div className="row row-30">
|
<div className="row row-30">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<ul className="block-info-1">
|
{isEmpty && (
|
||||||
<li>
|
<div className="text-center mt-40">
|
||||||
<div className="form-wrap-group-1">
|
<h3 className="text-spacing-20">No Properties Found</h3>
|
||||||
<div className="form-wrap">
|
<p className="heading-5 mt-3">Looks like we couldn’t find any listings that match your search.</p>
|
||||||
<select
|
</div>
|
||||||
className="form-input select-filter"
|
)}
|
||||||
data-style="modern"
|
{!isEmpty && (
|
||||||
data-placeholder="Publication Date"
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
data-minimum-results-for-search="Infinity"
|
{propertiesData.formattedData.map((p, idx) => (
|
||||||
data-constraints="@Required"
|
<CardProperty key={idx} data={p} />
|
||||||
>
|
))}
|
||||||
<option label="placeholder"></option>
|
</div>
|
||||||
<option value="2">Monday</option>
|
)}
|
||||||
<option value="3">Tuesday</option>
|
|
||||||
<option value="4">Wednesday</option>
|
|
||||||
<option value="5">Thursday</option>
|
|
||||||
<option value="6">Friday</option>
|
|
||||||
<option value="7">Saturday</option>
|
|
||||||
<option value="8">Sunday</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="form-wrap">
|
|
||||||
<select
|
|
||||||
className="form-input select-filter"
|
|
||||||
data-style="modern"
|
|
||||||
data-placeholder="Price Low to High"
|
|
||||||
data-minimum-results-for-search="Infinity"
|
|
||||||
data-constraints="@Required"
|
|
||||||
>
|
|
||||||
<option label="placeholder"></option>
|
|
||||||
<option value="1">Price Low to High</option>
|
|
||||||
<option value="2">Price High to Low</option>
|
|
||||||
<option value="3">Most Popular</option>
|
|
||||||
<option value="4">Top Rated</option>
|
|
||||||
<option value="5">Best Sellers</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
{/* Pagination */}
|
||||||
{propertiesData.map((p) => (
|
{propertiesData.totalPages > 1 && (
|
||||||
<CardProperty key={p.id} data={p} />
|
<div className="col-12">
|
||||||
))}
|
<Pagination
|
||||||
|
page={propertiesData.page ?? 1}
|
||||||
|
hasNextPage={propertiesData.hasNextPage}
|
||||||
|
hasPreviousPage={propertiesData.hasPrevPage}
|
||||||
|
totalPages={propertiesData.totalPages}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="col-12">
|
{/* End Pagination */}
|
||||||
{/* <ul className="pagination-custom">
|
|
||||||
<li>
|
|
||||||
<a className="active" href="#">
|
|
||||||
1
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">2</a>
|
|
||||||
</li>
|
|
||||||
</ul> */}
|
|
||||||
<Pagination hasNextPage={true} hasPreviousPage={true} totalPages={10} page={3} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-5 col-xl-4">
|
<div className="col-lg-5 col-xl-4">
|
||||||
@ -153,125 +85,55 @@ export default function ListingsForRent() {
|
|||||||
<div className="col-md-6 col-lg-12">
|
<div className="col-md-6 col-lg-12">
|
||||||
<div className="block-info">
|
<div className="block-info">
|
||||||
<h3>Find Your Property</h3>
|
<h3>Find Your Property</h3>
|
||||||
<form
|
<form className="form-select">
|
||||||
className="rd-mailform form-select"
|
|
||||||
data-form-output="form-output-global"
|
|
||||||
data-form-type="contact"
|
|
||||||
method="post"
|
|
||||||
action="bat/rd-mailform.php"
|
|
||||||
>
|
|
||||||
<div className="form-wrap form-wrap-validation">
|
<div className="form-wrap form-wrap-validation">
|
||||||
<select
|
<input className="form-input" placeholder="Name" name="s" defaultValue={searchParams?.name} />
|
||||||
className="form-input select-filter"
|
|
||||||
data-style="modern"
|
|
||||||
data-placeholder="Choose Location"
|
|
||||||
data-minimum-results-for-search="Infinity"
|
|
||||||
data-constraints="@Required"
|
|
||||||
>
|
|
||||||
<option label="placeholder"></option>
|
|
||||||
<option value="2">Alaska</option>
|
|
||||||
<option value="3">Arizona</option>
|
|
||||||
<option value="4">Arkansas</option>
|
|
||||||
<option value="5">California</option>
|
|
||||||
<option value="6">Colorado</option>
|
|
||||||
<option value="7">Connecticut</option>
|
|
||||||
<option value="8">Delaware</option>
|
|
||||||
<option value="9">Florida</option>
|
|
||||||
</select>
|
|
||||||
<span className="select-arrow"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-wrap form-wrap-validation">
|
<div className="form-wrap form-wrap-validation">
|
||||||
<select
|
<Select
|
||||||
className="form-input select-filter"
|
name="location"
|
||||||
data-style="modern"
|
placeholder="Choose Location"
|
||||||
data-placeholder="Property Status"
|
options={statesData}
|
||||||
data-minimum-results-for-search="Infinity"
|
defaultInputValue={searchParams?.location}
|
||||||
data-constraints="@Required"
|
defaultValue={searchParams?.location}
|
||||||
>
|
isSearchable
|
||||||
<option label="placeholder"></option>
|
isClearable
|
||||||
<option value="2">Low</option>
|
/>
|
||||||
<option value="3">Middle</option>
|
|
||||||
<option value="4">Primary</option>
|
|
||||||
</select>
|
|
||||||
<span className="select-arrow"></span>
|
|
||||||
</div>
|
|
||||||
<div className="form-wrap form-wrap-validation">
|
|
||||||
<select
|
|
||||||
className="form-input select-filter"
|
|
||||||
data-style="modern"
|
|
||||||
data-placeholder="Property Type"
|
|
||||||
data-minimum-results-for-search="Infinity"
|
|
||||||
data-constraints="@Required"
|
|
||||||
>
|
|
||||||
<option label="placeholder"></option>
|
|
||||||
<option value="2">Low</option>
|
|
||||||
<option value="3">Middle</option>
|
|
||||||
<option value="4">Primary</option>
|
|
||||||
</select>
|
|
||||||
<span className="select-arrow"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-wrap-group">
|
<div className="form-wrap-group">
|
||||||
<div className="form-wrap form-wrap-validation">
|
<div className="form-wrap form-wrap-validation">
|
||||||
<select
|
<input
|
||||||
className="form-input select-filter"
|
className="form-input"
|
||||||
data-style="modern"
|
placeholder="Min Area Sqft"
|
||||||
data-placeholder="Min Price"
|
name="min_area"
|
||||||
data-minimum-results-for-search="Infinity"
|
defaultValue={searchParams?.min_area}
|
||||||
data-constraints="@Required"
|
/>
|
||||||
>
|
|
||||||
<option label="placeholder"></option>
|
|
||||||
<option value="2">100 $</option>
|
|
||||||
<option value="3">200 $</option>
|
|
||||||
<option value="4">300 $</option>
|
|
||||||
</select>
|
|
||||||
<span className="select-arrow"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-wrap form-wrap-validation">
|
<div className="form-wrap form-wrap-validation">
|
||||||
<select
|
<input
|
||||||
className="form-input select-filter"
|
className="form-input"
|
||||||
data-style="modern"
|
placeholder="Max Area Sqft"
|
||||||
data-placeholder="Max Price"
|
name="max_area"
|
||||||
data-minimum-results-for-search="Infinity"
|
defaultValue={searchParams?.max_area}
|
||||||
data-constraints="@Required"
|
/>
|
||||||
>
|
|
||||||
<option label="placeholder"></option>
|
|
||||||
<option value="2">1000 $</option>
|
|
||||||
<option value="3">2000 $</option>
|
|
||||||
<option value="4">3000 $</option>
|
|
||||||
</select>
|
|
||||||
<span className="select-arrow"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-wrap-group">
|
<div className="form-wrap-group">
|
||||||
<div className="form-wrap form-wrap-validation">
|
<div className="form-wrap form-wrap-validation">
|
||||||
<select
|
<input
|
||||||
className="form-input select-filter"
|
className="form-input"
|
||||||
data-style="modern"
|
placeholder="Min Price ($)"
|
||||||
data-placeholder="Min Area"
|
name="min_price"
|
||||||
data-minimum-results-for-search="Infinity"
|
defaultValue={searchParams?.min_price}
|
||||||
data-constraints="@Required"
|
/>
|
||||||
>
|
|
||||||
<option label="placeholder"></option>
|
|
||||||
<option value="2">100 Sq Ft</option>
|
|
||||||
<option value="3">200 Sq Ft</option>
|
|
||||||
<option value="4">300 Sq Ft</option>
|
|
||||||
</select>
|
|
||||||
<span className="select-arrow"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-wrap form-wrap-validation">
|
<div className="form-wrap form-wrap-validation">
|
||||||
<select
|
<input
|
||||||
className="form-input select-filter"
|
className="form-input"
|
||||||
data-style="modern"
|
placeholder="Max Price ($)"
|
||||||
data-placeholder="Max Area"
|
name="max_price"
|
||||||
data-minimum-results-for-search="Infinity"
|
defaultValue={searchParams?.max_price}
|
||||||
data-constraints="@Required"
|
/>
|
||||||
>
|
|
||||||
<option label="placeholder"></option>
|
|
||||||
<option value="2">1000 Sq Ft</option>
|
|
||||||
<option value="3">2000 Sq Ft</option>
|
|
||||||
<option value="4">3000 Sq Ft</option>
|
|
||||||
</select>
|
|
||||||
<span className="select-arrow"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-button">
|
<div className="form-button">
|
||||||
|
@ -4,6 +4,11 @@ import type { CollectionConfig } from "payload";
|
|||||||
export const Properties: CollectionConfig = {
|
export const Properties: CollectionConfig = {
|
||||||
slug: "properties",
|
slug: "properties",
|
||||||
labels: { plural: "Properties", singular: "Property" },
|
labels: { plural: "Properties", singular: "Property" },
|
||||||
|
versions: {
|
||||||
|
drafts: {
|
||||||
|
validate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: "propertyType",
|
name: "propertyType",
|
||||||
@ -76,14 +81,7 @@ export const Properties: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: "country_code",
|
name: "country_code",
|
||||||
label: "Country",
|
label: "Country",
|
||||||
type: "select",
|
type: "text",
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "United States",
|
|
||||||
value: "US",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
required: true,
|
|
||||||
// admin: {
|
// admin: {
|
||||||
// components: {
|
// components: {
|
||||||
// Field: {
|
// Field: {
|
||||||
@ -95,26 +93,12 @@ export const Properties: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: "state_code",
|
name: "state_code",
|
||||||
label: "State",
|
label: "State",
|
||||||
type: "select",
|
type: "text",
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "Washington",
|
|
||||||
value: "WA",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "city_code",
|
name: "city_code",
|
||||||
label: "City",
|
label: "City",
|
||||||
type: "select",
|
type: "text",
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "Davenport",
|
|
||||||
value: "Davenport",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "zip_code",
|
name: "zip_code",
|
||||||
|
@ -9,55 +9,55 @@ type CardPropertyProps = {
|
|||||||
|
|
||||||
export default function CardProperty({ data }: CardPropertyProps) {
|
export default function CardProperty({ data }: CardPropertyProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<div>
|
<article className="product-classic">
|
||||||
<article className="product-classic">
|
<div className="product-classic-media">
|
||||||
<div className="product-classic-media">
|
<div
|
||||||
<div
|
className="owl-carousel"
|
||||||
className="owl-carousel"
|
data-items="1"
|
||||||
data-items="1"
|
data-nav="true"
|
||||||
data-nav="true"
|
data-stage-padding="0"
|
||||||
data-stage-padding="0"
|
data-loop="false"
|
||||||
data-loop="false"
|
data-margin="0"
|
||||||
data-margin="0"
|
data-mouse-drag="false"
|
||||||
data-mouse-drag="false"
|
>
|
||||||
>
|
{Array.isArray(data.images) &&
|
||||||
{Array.isArray(data.images) &&
|
data.images.map((img, idx) => (
|
||||||
data.images.map((img, idx) => (
|
<div key={idx} className="w-full h-52 bg-colorImgPlaceholder">
|
||||||
<Image key={idx} src={img.url} alt={img.alt ?? ""} width="480" height="287" />
|
<Image src={img.url} alt={img.alt ?? ""} fill className="object-cover" />
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
<div className="product-classic-price bg-colorPriceTag/80!">
|
|
||||||
<span>
|
|
||||||
{formatCurrency(data.price)}
|
|
||||||
{data.propertyType === "rent" && `/mo`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h4 className="product-classic-title">
|
<div className="product-classic-price bg-colorPriceTag/90!">
|
||||||
<Link href="/listings-for-rent/slug">{data.title}</Link>
|
<span>
|
||||||
</h4>
|
{formatCurrency(data.price)}
|
||||||
<div className="product-classic-divider"></div>
|
{data.propertyType === "rent" && `/mo`}
|
||||||
<ul className="product-classic-list">
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4 className="product-classic-title">
|
||||||
|
<Link href="/listings-for-rent/slug">{data.title}</Link>
|
||||||
|
</h4>
|
||||||
|
<div className="product-classic-divider"></div>
|
||||||
|
<ul className="product-classic-list">
|
||||||
|
<li>
|
||||||
|
<span className="icon mdi mdi-vector-square"></span>
|
||||||
|
<span>{data.area} Sq Ft</span>
|
||||||
|
</li>
|
||||||
|
{data.bathrooms_count && (
|
||||||
<li>
|
<li>
|
||||||
<span className="icon mdi mdi-vector-square"></span>
|
<span className="icon hotel-icon-10"></span>
|
||||||
<span>{data.area} Sq Ft</span>
|
<span>{data.bathrooms_count} Bathrooms</span>
|
||||||
</li>
|
</li>
|
||||||
{data.bathrooms_count && (
|
)}
|
||||||
<li>
|
{data.bedrooms_count && (
|
||||||
<span className="icon hotel-icon-10"></span>
|
<li>
|
||||||
<span>{data.bathrooms_count} Bathrooms</span>
|
<span className="icon hotel-icon-05"></span>
|
||||||
</li>
|
<span>{data.bedrooms_count} Bedrooms</span>
|
||||||
)}
|
</li>
|
||||||
{data.bedrooms_count && (
|
)}
|
||||||
<li>
|
</ul>
|
||||||
<span className="icon hotel-icon-05"></span>
|
</article>
|
||||||
<span>{data.bedrooms_count} Bedrooms</span>
|
</div>
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
8
src/components/Select.tsx
Normal file
8
src/components/Select.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ComponentProps } from "react";
|
||||||
|
import ReactSelect from "react-select";
|
||||||
|
|
||||||
|
export default function Select(props: ComponentProps<typeof ReactSelect>) {
|
||||||
|
return <ReactSelect {...props} />;
|
||||||
|
}
|
@ -264,9 +264,9 @@ export interface Property {
|
|||||||
bedrooms_count: string;
|
bedrooms_count: string;
|
||||||
};
|
};
|
||||||
addressGroup: {
|
addressGroup: {
|
||||||
country_code: 'US';
|
country_code?: string | null;
|
||||||
state_code: 'WA';
|
state_code?: string | null;
|
||||||
city_code: 'Davenport';
|
city_code?: string | null;
|
||||||
zip_code: string;
|
zip_code: string;
|
||||||
address: string;
|
address: string;
|
||||||
};
|
};
|
||||||
@ -282,6 +282,7 @@ export interface Property {
|
|||||||
embed_map_url?: string | null;
|
embed_map_url?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@ -489,6 +490,7 @@ export interface PropertiesSelect<T extends boolean = true> {
|
|||||||
embed_map_url?: T;
|
embed_map_url?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
|
_status?: T;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
export type CardPropertyData = {
|
export type CardPropertyData = {
|
||||||
id: number;
|
slug: string;
|
||||||
slug?: string | null;
|
|
||||||
title: string;
|
title: string;
|
||||||
price: number;
|
price: number;
|
||||||
images?: { url: string; alt?: string }[];
|
images?: { url: string; alt?: string }[];
|
||||||
/**
|
/**
|
||||||
* in sqft
|
* in sqft
|
||||||
*/
|
*/
|
||||||
area: number;
|
area: string;
|
||||||
bedrooms_count?: number;
|
bedrooms_count?: string;
|
||||||
bathrooms_count?: number;
|
bathrooms_count?: string;
|
||||||
posted_at: string;
|
posted_at: string;
|
||||||
propertyType: "rent" | "sell";
|
propertyType: "rent" | "sell";
|
||||||
is_available: boolean;
|
|
||||||
};
|
};
|
||||||
|
9
src/schema/services/property.ts
Normal file
9
src/schema/services/property.ts
Normal file
@ -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;
|
||||||
|
};
|
81
src/services/payload/property.ts
Normal file
81
src/services/payload/property.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
}
|
@ -1,5 +1,18 @@
|
|||||||
import { Blog } from "@/payload-types";
|
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 {
|
export function sanitizePageNumber(page: any, defaultPage = 1): number {
|
||||||
const parsedPage = Number(page);
|
const parsedPage = Number(page);
|
||||||
|
|
||||||
|
21
yarn.lock
21
yarn.lock
@ -5042,6 +5042,7 @@ __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-select: "npm:^5.10.1"
|
||||||
swiper: "npm:^11.2.6"
|
swiper: "npm:^11.2.6"
|
||||||
tailwindcss: "npm:^4"
|
tailwindcss: "npm:^4"
|
||||||
typescript: "npm:^5"
|
typescript: "npm:^5"
|
||||||
@ -8817,6 +8818,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user