fix: property detail filter and metadata integration
This commit is contained in:
parent
e483f8f281
commit
f5c5f3fd78
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
79
src/components/properties/FilterProperty.tsx
Normal file
79
src/components/properties/FilterProperty.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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}...`;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user