feat: recent blog post related fetching

This commit is contained in:
RizqiSyahrendra 2025-04-22 00:35:53 +07:00
parent 9f7360dbd0
commit 12e7941f6c
10 changed files with 193 additions and 68 deletions

View File

@ -1,3 +1,4 @@
import ListOfRecentBlog from "@/components/blogs/ListOfRecentBlog";
import HeroImage from "@/components/HeroImage"; import HeroImage from "@/components/HeroImage";
import { fetchBlogDetail } from "@/services/payload/blog"; import { fetchBlogDetail } from "@/services/payload/blog";
import { getDefaultMetadata } from "@/utils/metadata"; import { getDefaultMetadata } from "@/utils/metadata";
@ -110,65 +111,31 @@ export default async function BlogDetail(props: { params: Promise<{ slug: string
<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={shareUrl.facebook}></a> <a
target="_blank"
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-6 fa-twitter" href={shareUrl.twitter}></a> <a
target="_blank"
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-4 fa-linkedin" href={shareUrl.linkedin}></a> <a
target="_blank"
className="icon icon-circle icon-rounded icon-4 fa-linkedin"
href={shareUrl.linkedin}
></a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div className="post-simple-group">
<div className="post-simple-group-title"> <ListOfRecentBlog currentBlogId={blog?.data?.id} />
<h6>Recent Posts</h6>
</div>
<div className="post-simple-group-divider"></div>
<div className="row row-30">
<div className="col-sm-6">
<article className="post-simple">
<div className="post-simple-img">
<Image src="/images/blog-post-03-736x540.jpg" alt="" width="736" height="540" />
</div>
<div className="post-simple-body">
<div className="post-simple-title">
<h4>
<a href="#">Turks and Caicos Villa to be Sold for Record $7.6M</a>
</h4>
</div>
<div className="post-simple-time">
<span className="icon mdi mdi-clock"></span>
<a className="time" href="#">
March 15, 2021
</a>
</div>
</div>
</article>
</div>
<div className="col-sm-6">
<article className="post-simple">
<div className="post-simple-img">
<Image src="/images/blog-post-04-736x540.jpg" alt="" width="736" height="540" />
</div>
<div className="post-simple-body">
<div className="post-simple-title">
<h4>
<a href="#">How We Build a Better LA for Fifth Year in a Row</a>
</h4>
</div>
<div className="post-simple-time">
<span className="icon mdi mdi-clock"></span>
<a className="time" href="#">
March 15, 2021
</a>
</div>
</div>
</article>
</div>
</div>
</div>
</article> </article>
</div> </div>

View File

@ -3,12 +3,47 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
type CardBlogProps = { type CardBlogProps = {
colorPreset?: 1 | 2;
isDescriptionVisible?: boolean;
data: BlogData; data: BlogData;
}; };
export default function CardBlog({ data }: CardBlogProps) { export default function CardBlog({ data, colorPreset = 1, isDescriptionVisible = true }: CardBlogProps) {
const linkDetail = `/blog/${data.slug}`; const linkDetail = `/blog/${data.slug}`;
if (colorPreset === 2) {
return (
<div>
<article className="post-simple">
<div className="h-64 relative">
<Link href={linkDetail}>
<Image src={data.img?.url ?? ""} alt={data.img?.alt ?? ""} fill />
</Link>
</div>
<div className="post-simple-body">
<div className="post-simple-title">
<h4>
<Link href={linkDetail}>{data.title}</Link>
</h4>
</div>
{isDescriptionVisible && !!data?.description && (
<>
<div className="post-simple-divider"></div>
<div className="post-simple-text">
<p>{data.description}</p>
</div>
</>
)}
<div className="post-simple-time">
<span className="icon mdi mdi-clock"></span>
<span>{data.posted_at}</span>
</div>
</div>
</article>
</div>
);
}
return ( return (
<div> <div>
<article className="post-default"> <article className="post-default">
@ -23,10 +58,14 @@ export default function CardBlog({ data }: CardBlogProps) {
<Link href={linkDetail}>{data.title}</Link> <Link href={linkDetail}>{data.title}</Link>
</h4> </h4>
</div> </div>
<div className="post-default-divider"></div> {isDescriptionVisible && !!data?.description && (
<div className="post-default-text"> <>
<p>{data.description}</p> <div className="post-default-divider"></div>
</div> <div className="post-default-text">
<p>{data.description}</p>
</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>
<span>{data.posted_at}</span> <span>{data.posted_at}</span>

View File

@ -36,7 +36,7 @@ export default function ListOfBlog({ searchKeyword }: ListOfBlogProps) {
</div> </div>
<div className="mt-5"> <div className="mt-5">
{blogQuery.isFetching && <Loader />} {blogQuery.isFetching && <Loader />}
{blogQuery.hasNext && ( {!blogQuery.isFetching && blogQuery.hasNext && (
<button onClick={fetchMore} className="button button-primary"> <button onClick={fetchMore} className="button button-primary">
LOAD MORE... LOAD MORE...
</button> </button>

View File

@ -0,0 +1,48 @@
"use client";
import Loader from "@/components/loaders/Loader";
import { useRecentBlogQuery } from "@/services/hooks/blog";
import { useEffect } from "react";
import CardBlog from "./CardBlog";
type ListOfRecentBlogProps = {
currentBlogId?: number;
};
export default function ListOfRecentBlog({ currentBlogId }: ListOfRecentBlogProps) {
const recentBlogQuery = useRecentBlogQuery();
useEffect(() => {
if (!!currentBlogId) {
recentBlogQuery._fetch({
currentBlogId,
});
}
}, [currentBlogId]);
if (recentBlogQuery.isFetching) {
return (
<div className="mt-5 w-full">
<Loader />
</div>
);
}
if (recentBlogQuery.data.length <= 0) return <></>;
return (
<>
<div className="post-simple-group">
<div className="post-simple-group-title">
<h6>Recent Posts</h6>
</div>
<div className="post-simple-group-divider"></div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{recentBlogQuery.data.map((blog) => (
<CardBlog key={blog.slug} data={blog} colorPreset={2} isDescriptionVisible={false} />
))}
</div>
</div>
</>
);
}

View File

@ -18,8 +18,6 @@ const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename); const dirname = path.dirname(filename);
export default buildConfig({ export default buildConfig({
cors: [process.env.SITE_URL || ""],
csrf: [process.env.SITE_URL || ""],
admin: { admin: {
user: Users.slug, user: Users.slug,
importMap: { importMap: {

View File

@ -1,7 +1,7 @@
export type BlogData = { export type BlogData = {
slug?: string | null; slug?: string | null;
title: string; title: string;
description: string; description?: string;
img?: { url: string; alt?: string }; img?: { url: string; alt?: string };
posted_at: string; posted_at: string;
}; };

View File

@ -4,3 +4,7 @@ export type FetchBlogParams = {
categoryId?: number; categoryId?: number;
tagId?: number; tagId?: number;
}; };
export type FetchRecentBlogParams = {
currentBlogId: number;
};

View File

@ -1,7 +1,7 @@
import { BlogData } from "@/schema/blog"; import { BlogData } from "@/schema/blog";
import { FetchBlogParams } from "@/schema/services/blog"; import { FetchBlogParams, FetchRecentBlogParams } from "@/schema/services/blog";
import { useState } from "react"; import { useState } from "react";
import { fetchBlogREST } from "../rest/blog"; import { fetchBlogREST, fetchRecentBlogREST } from "../rest/blog";
export function useBlogQuery() { export function useBlogQuery() {
const [data, setData] = useState<BlogData[]>([]); const [data, setData] = useState<BlogData[]>([]);
@ -14,7 +14,13 @@ export function useBlogQuery() {
setFetching(false); setFetching(false);
if (Array.isArray(res?.formattedData)) { if (Array.isArray(res?.formattedData)) {
setData(res.formattedData); if (!!params.page && params.page > 1) {
setData((currentData) => {
return [...currentData, ...res.formattedData];
});
} else {
setData(res.formattedData);
}
} }
setHasNext(res?.hasNextPage ?? false); setHasNext(res?.hasNextPage ?? false);
} }
@ -26,3 +32,24 @@ export function useBlogQuery() {
hasNext, hasNext,
}; };
} }
export function useRecentBlogQuery() {
const [data, setData] = useState<BlogData[]>([]);
const [isFetching, setFetching] = useState(false);
async function _fetch(params: FetchRecentBlogParams) {
setFetching(true);
const res = await fetchRecentBlogREST(params);
setFetching(false);
if (Array.isArray(res?.formattedData)) {
setData(res.formattedData);
}
}
return {
_fetch,
data,
isFetching,
};
}

View File

@ -37,11 +37,13 @@ export async function fetchBlog({ page, search = "", categoryId, tagId }: FetchB
where: queryCondition, where: queryCondition,
}); });
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),
}; };
}); });

View File

@ -1,6 +1,6 @@
import { Blog } from "@/payload-types"; import { Blog } from "@/payload-types";
import { BlogData } from "@/schema/blog"; import { BlogData } from "@/schema/blog";
import { FetchBlogParams } from "@/schema/services/blog"; import { FetchBlogParams, FetchRecentBlogParams } from "@/schema/services/blog";
import { formatDate } from "@/utils/datetime"; import { formatDate } from "@/utils/datetime";
import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize"; import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize";
import { PaginatedDocs, Where } from "payload"; import { PaginatedDocs, Where } from "payload";
@ -59,3 +59,43 @@ export async function fetchBlogREST({ page, search = "", categoryId, tagId }: Fe
return null; return null;
} }
} }
export async function fetchRecentBlogREST({ currentBlogId }: FetchRecentBlogParams) {
const queryCondition: Where = {
_status: { equals: "published" },
id: {
not_equals: currentBlogId,
},
};
const queryParams = stringify(
{
pagination: true,
limit: 2,
where: queryCondition,
},
{ addQueryPrefix: true }
);
const blogRequest = await fetch(`/api/blogs${queryParams}`);
if (blogRequest.ok) {
const resData = (await blogRequest.json()) as PaginatedDocs<Blog>;
const formattedData: BlogData[] = resData.docs.map((item) => {
return {
slug: item.slug,
title: item.title,
description: sanitizeBlogContentIntoStringPreview(item.content),
img: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined,
posted_at: formatDate(item.createdAt),
};
});
return {
...resData,
formattedData,
};
} else {
return null;
}
}