fix: property detail filter and metadata integration

This commit is contained in:
RizqiSyahrendra 2025-04-23 17:39:05 +07:00
parent e483f8f281
commit f5c5f3fd78
8 changed files with 171 additions and 199 deletions

View File

@ -1,10 +1,65 @@
import CardProperty from "@/components/CardProperty"; import CardProperty from "@/components/properties/CardProperty";
import HeroImage from "@/components/HeroImage"; import HeroImage from "@/components/HeroImage";
import { fetchPropertyDetail, fetchPropertySuggestion } from "@/services/payload/property"; import { fetchPropertyDetail, fetchPropertySuggestion } from "@/services/payload/property";
import { RichText } from "@payloadcms/richtext-lexical/react"; import { RichText } from "@payloadcms/richtext-lexical/react";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Image from "next/image"; import Image from "next/image";
import { notFound } from "next/navigation"; 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<Metadata> {
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 ListingsForRentDetail({ params }: { params: Promise<{ slug: string }> }) { export default async function ListingsForRentDetail({ params }: { params: Promise<{ slug: string }> }) {
const slug = (await params).slug; const slug = (await params).slug;
@ -329,136 +384,7 @@ export default async function ListingsForRentDetail({ params }: { params: Promis
<div className="col-lg-5 col-xl-4"> <div className="col-lg-5 col-xl-4">
<div className="row row-50"> <div className="row row-50">
<div className="col-md-6 col-lg-12"> <div className="col-md-6 col-lg-12">
<div className="block-info"> <FilterProperty propertyType={data.property_type} />
<h3>Find Your Property</h3>
<form
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">
<select
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 className="form-wrap form-wrap-validation">
<select
className="form-input select-filter"
data-style="modern"
data-placeholder="Property Status"
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 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 className="form-wrap-group">
<div className="form-wrap form-wrap-validation">
<select
className="form-input select-filter"
data-style="modern"
data-placeholder="Min Price"
data-minimum-results-for-search="Infinity"
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 className="form-wrap form-wrap-validation">
<select
className="form-input select-filter"
data-style="modern"
data-placeholder="Max Price"
data-minimum-results-for-search="Infinity"
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 className="form-wrap-group">
<div className="form-wrap form-wrap-validation">
<select
className="form-input select-filter"
data-style="modern"
data-placeholder="Min Area"
data-minimum-results-for-search="Infinity"
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 className="form-wrap form-wrap-validation">
<select
className="form-input select-filter"
data-style="modern"
data-placeholder="Max Area"
data-minimum-results-for-search="Infinity"
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 className="form-button">
<button className="button button-block button-primary" type="submit">
Search
</button>
</div>
</form>
</div>
</div> </div>
<div className="col-md-6 col-lg-12"> <div className="col-md-6 col-lg-12">
<article className="block-callboard"> <article className="block-callboard">

View File

@ -1,12 +1,11 @@
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 Select from "@/components/Select"; import CardProperty from "@/components/properties/CardProperty";
import FilterProperty from "@/components/properties/FilterProperty";
import { FetchPropertyParams } from "@/schema/services/property"; import { FetchPropertyParams } from "@/schema/services/property";
import { fetchProperty } from "@/services/payload/property"; import { fetchProperty } from "@/services/payload/property";
import { getDefaultMetadata } from "@/utils/metadata"; import { getDefaultMetadata } from "@/utils/metadata";
import { sanitizeNumber, sanitizePageNumber } from "@/utils/sanitize"; 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.";
@ -40,7 +39,6 @@ export default async function ListingsForRent(props: {
location: searchParams?.location, location: searchParams?.location,
}); });
const isEmpty = propertiesData.formattedData.length <= 0; const isEmpty = propertiesData.formattedData.length <= 0;
const statesData = State.getStatesOfCountry("US").map((st) => ({ value: st.name, label: st.name }));
return ( return (
<> <>
@ -81,69 +79,11 @@ export default async function ListingsForRent(props: {
{/* End Pagination */} {/* End Pagination */}
</div> </div>
</div> </div>
<div className="col-lg-5 col-xl-4"> <div className="col-lg-5 col-xl-4">
<div className="row row-50"> <div className="row row-50">
<div className="col-md-6 col-lg-12"> <div className="col-md-6 col-lg-12">
<div className="block-info"> <FilterProperty propertyType="rent" searchParams={searchParams} />
<h3>Find Your Property</h3>
<form className="form-select">
<div className="form-wrap form-wrap-validation">
<input className="form-input" placeholder="Name" name="s" defaultValue={searchParams?.name} />
</div>
<div className="form-wrap form-wrap-validation">
<Select
name="location"
placeholder="Choose Location"
options={statesData}
defaultInputValue={searchParams?.location}
defaultValue={searchParams?.location}
isSearchable
isClearable
/>
</div>
<div className="form-wrap-group">
<div className="form-wrap form-wrap-validation">
<input
className="form-input"
placeholder="Min Area Sqft"
name="min_area"
defaultValue={searchParams?.min_area}
/>
</div>
<div className="form-wrap form-wrap-validation">
<input
className="form-input"
placeholder="Max Area Sqft"
name="max_area"
defaultValue={searchParams?.max_area}
/>
</div>
</div>
<div className="form-wrap-group">
<div className="form-wrap form-wrap-validation">
<input
className="form-input"
placeholder="Min Price ($)"
name="min_price"
defaultValue={searchParams?.min_price}
/>
</div>
<div className="form-wrap form-wrap-validation">
<input
className="form-input"
placeholder="Max Price ($)"
name="max_price"
defaultValue={searchParams?.max_price}
/>
</div>
</div>
<div className="form-button">
<button className="button button-block button-primary" type="submit">
Search
</button>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,5 @@
import formatSlug from "@/utils/payload/formatSlug"; import formatSlug from "@/utils/payload/formatSlug";
import setAuthor from "@/utils/payload/setAuthor";
import type { CollectionConfig } from "payload"; import type { CollectionConfig } from "payload";
export const Properties: CollectionConfig = { export const Properties: CollectionConfig = {
@ -143,6 +144,28 @@ export const Properties: CollectionConfig = {
label: "Embed Google Map URL", label: "Embed Google Map URL",
type: "text", 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: { admin: {
hideAPIURL: true, hideAPIURL: true,

View File

@ -56,7 +56,7 @@ export default function Header() {
<span className="icon text-middle mdi mdi-login"></span> <span className="icon text-middle mdi mdi-login"></span>
</span> </span>
<span className="unit-body"> <span className="unit-body">
<a href="/login">Login</a> <a href="/admin/login">Login</a>
</span> </span>
</div> </div>
</div> </div>

View File

@ -0,0 +1,79 @@
import { State } from "country-state-city";
import Select from "@/components/Select";
import { FetchPropertyParams } from "@/schema/services/property";
type FilterPropertyProps = {
propertyType: "sell" | "rent";
searchParams?: {
[P in keyof FetchPropertyParams]: string | undefined;
};
};
export default function FilterProperty({ propertyType, searchParams }: FilterPropertyProps) {
const statesData = State.getStatesOfCountry("US").map((st) => ({ value: st.name, label: st.name }));
return (
<>
<div className="block-info">
<h3>Find Your Property</h3>
<form className="form-select" action={propertyType === "rent" ? "/listings-for-rent" : "/listings-for-sell"}>
<div className="form-wrap form-wrap-validation">
<input className="form-input" placeholder="Name" name="name" defaultValue={searchParams?.name} />
</div>
<div className="form-wrap form-wrap-validation">
<Select
name="location"
placeholder="Choose Location"
options={statesData}
defaultInputValue={searchParams?.location}
defaultValue={searchParams?.location}
isSearchable
isClearable
/>
</div>
<div className="form-wrap-group">
<div className="form-wrap form-wrap-validation">
<input
className="form-input"
placeholder="Min Area Sqft"
name="min_area"
defaultValue={searchParams?.min_area}
/>
</div>
<div className="form-wrap form-wrap-validation">
<input
className="form-input"
placeholder="Max Area Sqft"
name="max_area"
defaultValue={searchParams?.max_area}
/>
</div>
</div>
<div className="form-wrap-group">
<div className="form-wrap form-wrap-validation">
<input
className="form-input"
placeholder="Min Price ($)"
name="min_price"
defaultValue={searchParams?.min_price}
/>
</div>
<div className="form-wrap form-wrap-validation">
<input
className="form-input"
placeholder="Max Price ($)"
name="max_price"
defaultValue={searchParams?.max_price}
/>
</div>
</div>
<div className="form-button">
<button className="button button-block button-primary" type="submit">
Search
</button>
</div>
</form>
</div>
</>
);
}

View File

@ -279,6 +279,8 @@ export interface Property {
}[] }[]
| null; | null;
embed_map_url?: string | null; embed_map_url?: string | null;
createdBy?: (number | null) | User;
updatedBy?: (number | null) | User;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
_status?: ('draft' | 'published') | null; _status?: ('draft' | 'published') | null;
@ -486,6 +488,8 @@ export interface PropertiesSelect<T extends boolean = true> {
id?: T; id?: T;
}; };
embed_map_url?: T; embed_map_url?: T;
createdBy?: T;
updatedBy?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
_status?: T; _status?: T;

View File

@ -23,7 +23,7 @@ export function sanitizePageNumber(page: any, defaultPage = 1): number {
return parsedPage; return parsedPage;
} }
export function sanitizeBlogContentIntoStringPreview(data: Blog["content"]) { export function sanitizeBlogContentIntoStringPreview(data: Blog["content"], limit = 100) {
// Find the first paragraph that has children with text // Find the first paragraph that has children with text
const firstParagraph = data.root.children.find( const firstParagraph = data.root.children.find(
(node) => (node) =>
@ -40,6 +40,6 @@ export function sanitizeBlogContentIntoStringPreview(data: Blog["content"]) {
// @ts-ignore // @ts-ignore
const text = firstParagraph.children?.[0]?.text ?? ""; const text = firstParagraph.children?.[0]?.text ?? "";
// Limit to 100 characters // Limit characters
return `${text.length > 100 ? text.slice(0, 100) : text}...`; return `${text.length > limit ? text.slice(0, limit) : text}...`;
} }