Merge pull request 'dev' (#7) from dev into main

Reviewed-on: #7
This commit is contained in:
RizqiSyahrendra 2025-04-21 04:36:52 +00:00
commit 727ae4d439
11 changed files with 127 additions and 41 deletions

View File

@ -0,0 +1,5 @@
import LoaderFixed from "@/components/loaders/LoaderFixed";
export default function Loading() {
return <LoaderFixed />;
}

View File

@ -1,23 +1,89 @@
import HeroImage from "@/components/HeroImage"; import HeroImage from "@/components/HeroImage";
import { fetchBlogDetail } from "@/services/payload/blog";
import { getDefaultMetadata } from "@/utils/metadata"; import { getDefaultMetadata } from "@/utils/metadata";
import { RichText } from "@payloadcms/richtext-lexical/react";
import { Metadata } from "next"; import { Metadata } from "next";
import { headers } from "next/headers";
import Image from "next/image"; import Image from "next/image";
import { notFound } from "next/navigation";
const metaDesc = const metaDesc =
"Explore the latest insights, news, and resources on the Dynamic Realty blog. Read our articles today."; "Explore the latest insights, news, and resources on the Dynamic Realty blog. Read our articles today.";
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const metadata = await getDefaultMetadata(); const metadata = await getDefaultMetadata();
metadata.title = `Blog - ${metadata.openGraph?.siteName}`; const params = await props.params;
metadata.description = metaDesc;
let title = "Page";
let description = "Page";
let publishedAt = "";
let updatedAt = "";
let imgUrl = "";
let createdByName = "";
let canonicalUrl = "";
const blog = await fetchBlogDetail(params.slug);
if (!!blog) {
// check for blog data
title = `${!!blog.data?.meta?.title ? blog.data?.meta?.title : blog.data.title} - ${metadata.openGraph?.siteName}`;
description = `${!!blog.data?.meta?.description ? blog.data?.meta?.description : blog.data.title}`;
imgUrl = blog.img.url;
publishedAt = blog.data.createdAt;
updatedAt = blog.data.updatedAt;
if (!!blog.data?.meta?.canonical_url) {
canonicalUrl = blog.data.meta.canonical_url;
}
if (!!blog?.data?.createdBy && typeof blog.data.createdBy !== "number") {
createdByName = blog.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;
}
if (!!metadata.alternates && !!canonicalUrl) {
metadata.alternates.canonical = canonicalUrl;
}
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; return metadata;
} }
export default function BlogDetail() { export default async function BlogDetail(props: { params: Promise<{ slug: string }> }) {
const params = await props.params;
const headersList = await headers();
const fullUrl = headersList.get("x-full-url");
const blog = await fetchBlogDetail(params.slug);
if (!blog) return notFound();
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}`,
};
return ( return (
<> <>
<HeroImage /> <HeroImage title={blog.data.title} />
<section className="section section-md bg-colorSection1"> <section className="section section-md bg-colorSection1">
<div className="container"> <div className="container">
@ -25,10 +91,11 @@ export default function BlogDetail() {
<div className="col-lg-8"> <div className="col-lg-8">
<article className="blog-post-solo"> <article className="blog-post-solo">
<div className="blog-post-solo-part"> <div className="blog-post-solo-part">
<p> <div className="w-full h-56 md:h-80 bg-colorImgPlaceholder/50 relative">
Showcasing a warm, traditional-style exterior and the highest caliber of contemporary European <Image className="object-cover" src={blog.img.url} alt={blog.img.alt} fill />
finishes throughout, towering glass doors open to grand-scale living spaces. </div>
</p>
<RichText className="mt-4" data={blog.data.content} />
</div> </div>
<div className="blog-post-solo-footer"> <div className="blog-post-solo-footer">
@ -36,7 +103,7 @@ export default function BlogDetail() {
<ul className="blog-post-solo-footer-list"> <ul className="blog-post-solo-footer-list">
<li> <li>
<span className="icon mdi mdi-clock"></span> <span className="icon mdi mdi-clock"></span>
<a href="#">February 10, 2021</a> <a href="#">{blog.createdAt}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -46,16 +113,13 @@ export default function BlogDetail() {
<span>Share this post</span> <span>Share this post</span>
</li> </li>
<li> <li>
<a className="icon icon-circle icon-rounded icon-5 fa-facebook" href="#"></a> <a className="icon icon-circle icon-rounded icon-5 fa-facebook" href={shareUrl.facebook}></a>
</li> </li>
<li> <li>
<a className="icon icon-circle icon-rounded icon-4 fa-google-plus" href="#"></a> <a className="icon icon-circle icon-rounded icon-6 fa-twitter" href={shareUrl.twitter}></a>
</li> </li>
<li> <li>
<a className="icon icon-circle icon-rounded icon-6 fa-twitter" href="#"></a> <a className="icon icon-circle icon-rounded icon-4 fa-linkedin" href={shareUrl.linkedin}></a>
</li>
<li>
<a className="icon icon-circle icon-rounded icon-6 fa-pinterest-p" href="#"></a>
</li> </li>
</ul> </ul>
</div> </div>
@ -116,22 +180,18 @@ export default function BlogDetail() {
<div className="pdl-xl-40"> <div className="pdl-xl-40">
<div className="row row-60"> <div className="row row-60">
<div className="col-md-6 col-lg-12"> <div className="col-md-6 col-lg-12">
<form className="form-lg rd-search rd-search-classic"> <form action="/blog" className="form-lg rd-search rd-search-classic">
<div className="form-wrap"> <div className="form-wrap">
<label className="form-label" htmlFor="rd-search-form-input">
Search the blog...
</label>
<input <input
className="form-input" className="form-input"
id="rd-search-form-input" id="rd-search-form-input"
type="text" type="text"
name="s" name="s"
autoComplete="off" autoComplete="off"
placeholder="Search blog..."
/> />
</div> </div>
<button className="rd-search-submit" type="submit"> <button className="rd-search-submit" type="submit"></button>
{" "}
</button>
</form> </form>
</div> </div>
<div className="col-md-6 col-lg-12"> <div className="col-md-6 col-lg-12">

View File

@ -14,12 +14,12 @@ export async function generateMetadata(): Promise<Metadata> {
return metadata; return metadata;
} }
export default async function Blog({ searchParams }: { searchParams?: Promise<{ s?: string }> }) { export default async function Blog(props: { searchParams?: Promise<{ s?: string }> }) {
const params = await searchParams; const searchParams = await props?.searchParams;
return ( return (
<> <>
<HeroImage /> <HeroImage title="Blog" />
<section className="section section-md bg-colorSection2"> <section className="section section-md bg-colorSection2">
<div className="container"> <div className="container">
<div> <div>
@ -32,14 +32,14 @@ export default async function Blog({ searchParams }: { searchParams?: Promise<{
type="text" type="text"
name="s" name="s"
autoComplete="off" autoComplete="off"
defaultValue={params?.s} defaultValue={searchParams?.s}
/> />
</div> </div>
<button className="rd-search-submit" type="submit"></button> <button className="rd-search-submit" type="submit"></button>
</form> </form>
</div> </div>
<ListOfBlog searchKeyword={params?.s} /> <ListOfBlog searchKeyword={searchParams?.s} />
</div> </div>
</section> </section>
</> </>

View File

@ -24,6 +24,7 @@
--color-colorHeaderText: var(--color-colorExt20); --color-colorHeaderText: var(--color-colorExt20);
--color-colorHeaderTextHover: var(--color-colorext40); --color-colorHeaderTextHover: var(--color-colorext40);
--color-colorHeroOverlay: var(--color-colorExt50); --color-colorHeroOverlay: var(--color-colorExt50);
--color-colorImgPlaceholder: var(--color-colorExt50);
--color-colorFooter: var(--color-colorExt10); --color-colorFooter: var(--color-colorExt10);
--color-colorFooter2: var(--color-colorExt50); --color-colorFooter2: var(--color-colorExt50);
--color-colorFooterText: var(--color-colorExt20); --color-colorFooterText: var(--color-colorExt20);
@ -32,6 +33,7 @@
--color-colorContactForm: var(--color-colorExt50); --color-colorContactForm: var(--color-colorExt50);
--color-colorText1: var(--color-colorExt10); --color-colorText1: var(--color-colorExt10);
--color-colorText2: var(--color-colorExt20); --color-colorText2: var(--color-colorExt20);
--color-colorLoaderBackground: var(--color-colorExt20);
} }
@layer components { @layer components {

View File

@ -1,10 +1,11 @@
import Image from "next/image"; import Image from "next/image";
type HeroImageProps = { type HeroImageProps = {
title?: string;
imgSrc?: string; imgSrc?: string;
}; };
export default function HeroImage({ imgSrc = "/images/breadcrumbs-bg-05-1922x441.jpg" }: HeroImageProps) { export default function HeroImage({ title = "", imgSrc = "/images/breadcrumbs-bg-05-1922x441.jpg" }: HeroImageProps) {
return ( return (
<section className="breadcrumbs-custom bg-image context-dark"> <section className="breadcrumbs-custom bg-image context-dark">
<Image <Image
@ -19,7 +20,7 @@ export default function HeroImage({ imgSrc = "/images/breadcrumbs-bg-05-1922x441
/> />
<div className="bg-colorHeroOverlay/50 w-full h-full absolute top-0 left-0" /> <div className="bg-colorHeroOverlay/50 w-full h-full absolute top-0 left-0" />
<div className="container relative"> <div className="container relative">
<h2 className="breadcrumbs-custom-title">Blog</h2> <h2 className="breadcrumbs-custom-title">{title}</h2>
</div> </div>
</section> </section>
); );

View File

@ -7,18 +7,20 @@ type CardBlogProps = {
}; };
export default function CardBlog({ data }: CardBlogProps) { export default function CardBlog({ data }: CardBlogProps) {
const linkDetail = `/blog/${data.slug}`;
return ( return (
<div> <div>
<article className="post-default"> <article className="post-default">
<div className="h-64 relative"> <div className="h-64 relative">
<Link href="#"> <Link href={linkDetail}>
<Image src={data.img?.url ?? ""} alt={data.img?.alt ?? ""} fill /> <Image src={data.img?.url ?? ""} alt={data.img?.alt ?? ""} fill />
</Link> </Link>
</div> </div>
<div className="post-default-body"> <div className="post-default-body">
<div className="post-default-title"> <div className="post-default-title">
<h4> <h4>
<a href="blog-post.html">{data.title}</a> <Link href={linkDetail}>{data.title}</Link>
</h4> </h4>
</div> </div>
<div className="post-default-divider"></div> <div className="post-default-divider"></div>
@ -27,7 +29,7 @@ export default function CardBlog({ data }: CardBlogProps) {
</div> </div>
<div className="post-default-time"> <div className="post-default-time">
<span className="icon mdi mdi-clock"></span> <span className="icon mdi mdi-clock"></span>
<a href="#">{data.posted_at}</a> <span>{data.posted_at}</span>
</div> </div>
</div> </div>
</article> </article>

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import Loader from "@/components/Loader"; import Loader from "@/components/loaders/Loader";
import { useBlogQuery } from "@/services/hooks/blog"; import { useBlogQuery } from "@/services/hooks/blog";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import CardBlog from "./CardBlog"; import CardBlog from "./CardBlog";

View File

@ -0,0 +1,9 @@
import Loader from "./Loader";
export default function LoaderFixed() {
return (
<div className="fixed top-0 left-0 flex justify-center items-center w-screen h-screen bg-colorLoaderBackground z-50">
<Loader />
</div>
);
}

View File

@ -1,7 +1,9 @@
import payloadConfig from "@/payload.config"; import payloadConfig from "@/payload.config";
import { BlogData } from "@/schema/blog";
import { FetchBlogParams } from "@/schema/services/blog"; import { FetchBlogParams } from "@/schema/services/blog";
import { formatDate } from "@/utils/datetime"; import { formatDate } from "@/utils/datetime";
import { getRandomNumber } from "@/utils/general"; import { getRandomNumber } from "@/utils/general";
import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize";
import { getPayload, Where } from "payload"; import { getPayload, Where } from "payload";
export async function fetchBlog({ page, search = "", categoryId, tagId }: FetchBlogParams = {}) { export async function fetchBlog({ page, search = "", categoryId, tagId }: FetchBlogParams = {}) {
@ -71,11 +73,13 @@ export async function fetchBlogSuggestion() {
limit: limitPerPage, limit: limitPerPage,
}); });
const formattedData = blogDataQuery.docs.map((item) => { const formattedData: BlogData[] = blogDataQuery.docs.map((item) => {
return { return {
...item, slug: item.slug,
imgFormatted: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined, title: item.title,
createdAtFormatted: formatDate(item.createdAt), description: sanitizeBlogContentIntoStringPreview(item.content),
img: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined,
posted_at: formatDate(item.createdAt),
}; };
}); });
@ -102,13 +106,16 @@ export async function fetchBlogDetail(slug: string | undefined) {
const data = blogDataQuery?.docs?.[0]; const data = blogDataQuery?.docs?.[0];
const createdAt = formatDate(data.createdAt); const createdAt = formatDate(data.createdAt);
const updatedAt = formatDate(data.updatedAt); const updatedAt = formatDate(data.updatedAt);
const imgUrl = typeof data.img !== "number" ? (data?.img?.url ?? "") : ""; const img = {
url: typeof data.img !== "number" ? (data?.img?.url ?? "") : "",
alt: typeof data.img !== "number" ? (data?.img?.alt ?? "") : "",
};
return { return {
data, data,
createdAt, createdAt,
updatedAt, updatedAt,
imgUrl, img,
}; };
} }

View File

@ -1,5 +1,5 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
export function formatDate(iso: string, format: string = "MMM, D YYYY") { export function formatDate(iso: string, format: string = "MMMM DD, YYYY") {
return dayjs(iso).format(format); return dayjs(iso).format(format);
} }