feat: blog list fetching and related
This commit is contained in:
parent
14e5b30281
commit
455a9785bd
@ -17,9 +17,11 @@
|
|||||||
"@payloadcms/payload-cloud": "^3.35.1",
|
"@payloadcms/payload-cloud": "^3.35.1",
|
||||||
"@payloadcms/richtext-lexical": "^3.35.1",
|
"@payloadcms/richtext-lexical": "^3.35.1",
|
||||||
"@payloadcms/storage-s3": "^3.35.1",
|
"@payloadcms/storage-s3": "^3.35.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"next": "15.3.0",
|
"next": "15.3.0",
|
||||||
"payload": "^3.35.1",
|
"payload": "^3.35.1",
|
||||||
|
"qs-esm": "^7.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"swiper": "^11.2.6"
|
"swiper": "^11.2.6"
|
||||||
@ -39,4 +41,4 @@
|
|||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.1"
|
"packageManager": "yarn@4.9.1"
|
||||||
}
|
}
|
||||||
|
@ -1084,91 +1084,91 @@ $(function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RD Search
|
// RD Search
|
||||||
if (plugins.search.length || plugins.searchResults) {
|
// if (plugins.search.length || plugins.searchResults) {
|
||||||
var handler = "bat/rd-search.php";
|
// var handler = "bat/rd-search.php";
|
||||||
var defaultTemplate = '<h5 class="search-title"><a target="_top" href="#{href}" class="search-link">#{title}</a></h5>' +
|
// var defaultTemplate = '<h5 class="search-title"><a target="_top" href="#{href}" class="search-link">#{title}</a></h5>' +
|
||||||
'<p>...#{token}...</p>' +
|
// '<p>...#{token}...</p>' +
|
||||||
'<p class="match"><em>Terms matched: #{count} - URL: #{href}</em></p>';
|
// '<p class="match"><em>Terms matched: #{count} - URL: #{href}</em></p>';
|
||||||
var defaultFilter = '*.html';
|
// var defaultFilter = '*.html';
|
||||||
|
|
||||||
if (plugins.search.length) {
|
// if (plugins.search.length) {
|
||||||
for (var i = 0; i < plugins.search.length; i++) {
|
// for (var i = 0; i < plugins.search.length; i++) {
|
||||||
var searchItem = $(plugins.search[i]),
|
// var searchItem = $(plugins.search[i]),
|
||||||
options = {
|
// options = {
|
||||||
element: searchItem,
|
// element: searchItem,
|
||||||
filter: (searchItem.attr('data-search-filter')) ? searchItem.attr('data-search-filter') : defaultFilter,
|
// filter: (searchItem.attr('data-search-filter')) ? searchItem.attr('data-search-filter') : defaultFilter,
|
||||||
template: (searchItem.attr('data-search-template')) ? searchItem.attr('data-search-template') : defaultTemplate,
|
// template: (searchItem.attr('data-search-template')) ? searchItem.attr('data-search-template') : defaultTemplate,
|
||||||
live: (searchItem.attr('data-search-live')) ? searchItem.attr('data-search-live') : false,
|
// live: (searchItem.attr('data-search-live')) ? searchItem.attr('data-search-live') : false,
|
||||||
liveCount: (searchItem.attr('data-search-live-count')) ? parseInt(searchItem.attr('data-search-live'), 10) : 4,
|
// liveCount: (searchItem.attr('data-search-live-count')) ? parseInt(searchItem.attr('data-search-live'), 10) : 4,
|
||||||
current: 0, processed: 0, timer: {}
|
// current: 0, processed: 0, timer: {}
|
||||||
};
|
// };
|
||||||
|
|
||||||
var $toggle = $('.rd-navbar-search-toggle');
|
// var $toggle = $('.rd-navbar-search-toggle');
|
||||||
if ($toggle.length) {
|
// if ($toggle.length) {
|
||||||
$toggle.on('click', (function (searchItem) {
|
// $toggle.on('click', (function (searchItem) {
|
||||||
return function () {
|
// return function () {
|
||||||
if (!($(this).hasClass('active'))) {
|
// if (!($(this).hasClass('active'))) {
|
||||||
searchItem.find('input').val('').trigger('propertychange');
|
// searchItem.find('input').val('').trigger('propertychange');
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
})(searchItem));
|
// })(searchItem));
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (options.live) {
|
// if (options.live) {
|
||||||
var clearHandler = false;
|
// var clearHandler = false;
|
||||||
|
|
||||||
searchItem.find('input').on("input propertychange", $.proxy(function () {
|
// searchItem.find('input').on("input propertychange", $.proxy(function () {
|
||||||
this.term = this.element.find('input').val().trim();
|
// this.term = this.element.find('input').val().trim();
|
||||||
this.spin = this.element.find('.input-group-addon');
|
// this.spin = this.element.find('.input-group-addon');
|
||||||
|
|
||||||
clearTimeout(this.timer);
|
// clearTimeout(this.timer);
|
||||||
|
|
||||||
if (this.term.length > 2) {
|
// if (this.term.length > 2) {
|
||||||
this.timer = setTimeout(liveSearch(this), 200);
|
// this.timer = setTimeout(liveSearch(this), 200);
|
||||||
|
|
||||||
if (clearHandler === false) {
|
// if (clearHandler === false) {
|
||||||
clearHandler = true;
|
// clearHandler = true;
|
||||||
|
|
||||||
$body.on("click", function (e) {
|
// $body.on("click", function (e) {
|
||||||
if ($(e.toElement).parents('.rd-search').length === 0) {
|
// if ($(e.toElement).parents('.rd-search').length === 0) {
|
||||||
$('#rd-search-results-live').addClass('cleared').html('');
|
// $('#rd-search-results-live').addClass('cleared').html('');
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
} else if (this.term.length === 0) {
|
// } else if (this.term.length === 0) {
|
||||||
$('#' + this.live).addClass('cleared').html('');
|
// $('#' + this.live).addClass('cleared').html('');
|
||||||
}
|
// }
|
||||||
}, options, this));
|
// }, options, this));
|
||||||
}
|
// }
|
||||||
|
|
||||||
searchItem.submit($.proxy(function () {
|
// searchItem.submit($.proxy(function () {
|
||||||
$('<input />').attr('type', 'hidden')
|
// $('<input />').attr('type', 'hidden')
|
||||||
.attr('name', "filter")
|
// .attr('name', "filter")
|
||||||
.attr('value', this.filter)
|
// .attr('value', this.filter)
|
||||||
.appendTo(this.element);
|
// .appendTo(this.element);
|
||||||
return true;
|
// return true;
|
||||||
}, options, this))
|
// }, options, this))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (plugins.searchResults.length) {
|
// if (plugins.searchResults.length) {
|
||||||
var regExp = /\?.*s=([^&]+)\&filter=([^&]+)/g;
|
// var regExp = /\?.*s=([^&]+)\&filter=([^&]+)/g;
|
||||||
var match = regExp.exec(location.search);
|
// var match = regExp.exec(location.search);
|
||||||
|
|
||||||
if (match !== null) {
|
// if (match !== null) {
|
||||||
$.get(handler, {
|
// $.get(handler, {
|
||||||
s: decodeURI(match[1]),
|
// s: decodeURI(match[1]),
|
||||||
dataType: "html",
|
// dataType: "html",
|
||||||
filter: match[2],
|
// filter: match[2],
|
||||||
template: defaultTemplate,
|
// template: defaultTemplate,
|
||||||
live: ''
|
// live: ''
|
||||||
}, function (data) {
|
// }, function (data) {
|
||||||
plugins.searchResults.html(data);
|
// plugins.searchResults.html(data);
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Swiper
|
// Swiper
|
||||||
function makeInterLeaveEffectOptions(interleaveOffset) {
|
function makeInterLeaveEffectOptions(interleaveOffset) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import CardBlog from "@/components/CardBlog";
|
import ListOfBlog from "@/components/blogs/ListOfBlog";
|
||||||
import HeroImage from "@/components/HeroImage";
|
import HeroImage from "@/components/HeroImage";
|
||||||
import { CardBlogData } from "@/schema/blog";
|
|
||||||
import { getDefaultMetadata } from "@/utils/metadata";
|
import { getDefaultMetadata } from "@/utils/metadata";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
@ -15,57 +14,8 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Blog() {
|
export default async function Blog({ searchParams }: { searchParams?: Promise<{ s?: string }> }) {
|
||||||
const data: CardBlogData[] = [
|
const params = await searchParams;
|
||||||
{
|
|
||||||
slug: "introducing-a-ray-kappe",
|
|
||||||
title: "Introducing A Ray Kappe-Designed Masterpiece",
|
|
||||||
description:
|
|
||||||
"The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
|
|
||||||
img: "/images/blog-04-736x540.jpg",
|
|
||||||
posted_at: "March 15, 2021",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: "24234",
|
|
||||||
title: "Introducing A Ray Kappe-Designed Masterpiece",
|
|
||||||
description:
|
|
||||||
"The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
|
|
||||||
img: "/images/blog-04-736x540.jpg",
|
|
||||||
posted_at: "March 15, 2021",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: "wkejrh2k3jr",
|
|
||||||
title: "Introducing A Ray Kappe-Designed Masterpiece",
|
|
||||||
description:
|
|
||||||
"The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
|
|
||||||
img: "/images/blog-04-736x540.jpg",
|
|
||||||
posted_at: "March 15, 2021",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: "1l2kj4lw34",
|
|
||||||
title: "Introducing A Ray Kappe-Designed Masterpiece",
|
|
||||||
description:
|
|
||||||
"The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
|
|
||||||
img: "/images/blog-04-736x540.jpg",
|
|
||||||
posted_at: "March 15, 2021",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: "asflj2lj53545",
|
|
||||||
title: "Introducing A Ray Kappe-Designed Masterpiece",
|
|
||||||
description:
|
|
||||||
"The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
|
|
||||||
img: "/images/blog-04-736x540.jpg",
|
|
||||||
posted_at: "March 15, 2021",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: "adflkj2oj545",
|
|
||||||
title: "Introducing A Ray Kappe-Designed Masterpiece",
|
|
||||||
description:
|
|
||||||
"The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
|
|
||||||
img: "/images/blog-04-736x540.jpg",
|
|
||||||
posted_at: "",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -82,21 +32,14 @@ export default function Blog() {
|
|||||||
type="text"
|
type="text"
|
||||||
name="s"
|
name="s"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
defaultValue={params?.s}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button className="rd-search-submit" type="submit"></button>
|
<button className="rd-search-submit" type="submit"></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-5">
|
|
||||||
{data.map((blog) => (
|
<ListOfBlog searchKeyword={params?.s} />
|
||||||
<CardBlog key={blog.slug} data={blog} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-5">
|
|
||||||
<button type="submit" className="button button-primary">
|
|
||||||
LOAD MORE...
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
@ -99,4 +99,9 @@ export const Blogs: CollectionConfig = {
|
|||||||
group: "Blogs",
|
group: "Blogs",
|
||||||
useAsTitle: "title",
|
useAsTitle: "title",
|
||||||
},
|
},
|
||||||
|
access: {
|
||||||
|
read: ({ req: { user } }) => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
17
src/components/Loader.tsx
Normal file
17
src/components/Loader.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export default function Loader() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row justify-center">
|
||||||
|
<div className="w-[72px] h-[72px]">
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
<div className="banter-loader__box"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,17 +1,20 @@
|
|||||||
import { CardBlogData } from "@/schema/blog";
|
import { BlogData } from "@/schema/blog";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
type CardBlogProps = {
|
type CardBlogProps = {
|
||||||
data: CardBlogData;
|
data: BlogData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CardBlog({ data }: CardBlogProps) {
|
export default function CardBlog({ data }: CardBlogProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<article className="post-default">
|
<article className="post-default">
|
||||||
<a className="post-default-image" href="blog-post.html">
|
<div className="h-64 relative">
|
||||||
<Image src={data.img} alt={data.title} width="736" height="540" />
|
<Link href="#">
|
||||||
</a>
|
<Image src={data.img?.url ?? ""} alt={data.img?.alt ?? ""} fill />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<div className="post-default-body">
|
<div className="post-default-body">
|
||||||
<div className="post-default-title">
|
<div className="post-default-title">
|
||||||
<h4>
|
<h4>
|
47
src/components/blogs/ListOfBlog.tsx
Normal file
47
src/components/blogs/ListOfBlog.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Loader from "@/components/Loader";
|
||||||
|
import { useBlogQuery } from "@/services/hooks/blog";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import CardBlog from "./CardBlog";
|
||||||
|
|
||||||
|
type ListOfBlogProps = {
|
||||||
|
searchKeyword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ListOfBlog({ searchKeyword }: ListOfBlogProps) {
|
||||||
|
const pageRef = useRef(1);
|
||||||
|
const blogQuery = useBlogQuery();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
blogQuery._fetch({
|
||||||
|
search: searchKeyword,
|
||||||
|
page: pageRef.current,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function fetchMore() {
|
||||||
|
blogQuery._fetch({
|
||||||
|
search: searchKeyword,
|
||||||
|
page: ++pageRef.current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-5">
|
||||||
|
{blogQuery.data.map((blog) => (
|
||||||
|
<CardBlog key={blog.slug} data={blog} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
{blogQuery.isFetching && <Loader />}
|
||||||
|
{blogQuery.hasNext && (
|
||||||
|
<button onClick={fetchMore} className="button button-primary">
|
||||||
|
LOAD MORE...
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -18,6 +18,8 @@ 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: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export type CardBlogData = {
|
export type BlogData = {
|
||||||
slug: string;
|
slug?: string | null;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
img: string;
|
img?: { url: string; alt?: string };
|
||||||
posted_at: string;
|
posted_at: string;
|
||||||
};
|
};
|
||||||
|
6
src/schema/services/blog.ts
Normal file
6
src/schema/services/blog.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type FetchBlogParams = {
|
||||||
|
page?: number;
|
||||||
|
search?: string;
|
||||||
|
categoryId?: number;
|
||||||
|
tagId?: number;
|
||||||
|
};
|
28
src/services/hooks/blog.ts
Normal file
28
src/services/hooks/blog.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { BlogData } from "@/schema/blog";
|
||||||
|
import { FetchBlogParams } from "@/schema/services/blog";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { fetchBlogREST } from "../rest/blog";
|
||||||
|
|
||||||
|
export function useBlogQuery() {
|
||||||
|
const [data, setData] = useState<BlogData[]>([]);
|
||||||
|
const [isFetching, setFetching] = useState(false);
|
||||||
|
const [hasNext, setHasNext] = useState(false);
|
||||||
|
|
||||||
|
async function _fetch(params: FetchBlogParams = {}) {
|
||||||
|
setFetching(true);
|
||||||
|
const res = await fetchBlogREST(params);
|
||||||
|
setFetching(false);
|
||||||
|
|
||||||
|
if (Array.isArray(res?.formattedData)) {
|
||||||
|
setData(res.formattedData);
|
||||||
|
}
|
||||||
|
setHasNext(res?.hasNextPage ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_fetch,
|
||||||
|
data,
|
||||||
|
isFetching,
|
||||||
|
hasNext,
|
||||||
|
};
|
||||||
|
}
|
147
src/services/payload/blog.ts
Normal file
147
src/services/payload/blog.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import payloadConfig from "@/payload.config";
|
||||||
|
import { FetchBlogParams } from "@/schema/services/blog";
|
||||||
|
import { formatDate } from "@/utils/datetime";
|
||||||
|
import { getRandomNumber } from "@/utils/general";
|
||||||
|
import { getPayload, Where } from "payload";
|
||||||
|
|
||||||
|
export async function fetchBlog({ page, search = "", categoryId, tagId }: FetchBlogParams = {}) {
|
||||||
|
const payload = await getPayload({ config: payloadConfig });
|
||||||
|
|
||||||
|
const queryCondition: Where = {
|
||||||
|
_status: { equals: "published" },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!!search) {
|
||||||
|
queryCondition["title"] = {
|
||||||
|
contains: search,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!!categoryId) {
|
||||||
|
queryCondition["categories"] = {
|
||||||
|
equals: categoryId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!!tagId) {
|
||||||
|
queryCondition["tags"] = {
|
||||||
|
equals: tagId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const blogDataQuery = await payload.find({
|
||||||
|
collection: "blogs",
|
||||||
|
page,
|
||||||
|
pagination: true,
|
||||||
|
limit: 9,
|
||||||
|
where: queryCondition,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedData = blogDataQuery.docs.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
imgFormatted: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined,
|
||||||
|
createdAtFormatted: formatDate(item.createdAt),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...blogDataQuery,
|
||||||
|
formattedData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBlogSuggestion() {
|
||||||
|
const payload = await getPayload({ config: payloadConfig });
|
||||||
|
const limitPerPage = 2;
|
||||||
|
const blogCountQuery = await payload.count({
|
||||||
|
collection: "blogs",
|
||||||
|
where: { _status: { equals: "published" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// randomize page
|
||||||
|
let page = 1;
|
||||||
|
const totalDocs = blogCountQuery.totalDocs;
|
||||||
|
if (totalDocs > limitPerPage) {
|
||||||
|
const totalPage = Math.ceil(totalDocs / limitPerPage);
|
||||||
|
page = getRandomNumber(totalPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blogDataQuery = await payload.find({
|
||||||
|
collection: "blogs",
|
||||||
|
page,
|
||||||
|
limit: limitPerPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedData = blogDataQuery.docs.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
imgFormatted: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined,
|
||||||
|
createdAtFormatted: formatDate(item.createdAt),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...blogDataQuery,
|
||||||
|
formattedData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBlogDetail(slug: string | undefined) {
|
||||||
|
const payload = await getPayload({ config: payloadConfig });
|
||||||
|
const blogDataQuery = await payload.find({
|
||||||
|
collection: "blogs",
|
||||||
|
where: {
|
||||||
|
_status: { equals: "published" },
|
||||||
|
slug: { equals: slug },
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!blogDataQuery?.docs?.[0]) return null;
|
||||||
|
|
||||||
|
const data = blogDataQuery?.docs?.[0];
|
||||||
|
const createdAt = formatDate(data.createdAt);
|
||||||
|
const updatedAt = formatDate(data.updatedAt);
|
||||||
|
const imgUrl = typeof data.img !== "number" ? (data?.img?.url ?? "") : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
imgUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBlogCategoryBySlug(slug: string) {
|
||||||
|
const payload = await getPayload({ config: payloadConfig });
|
||||||
|
const category = await payload.find({
|
||||||
|
collection: "blogCategories",
|
||||||
|
where: {
|
||||||
|
_status: { equals: "published" },
|
||||||
|
slug: { equals: slug },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!category?.docs?.[0]) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: category.docs[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBlogTagBySlug(slug: string) {
|
||||||
|
const payload = await getPayload({ config: payloadConfig });
|
||||||
|
const tag = await payload.find({
|
||||||
|
collection: "blogTags",
|
||||||
|
where: {
|
||||||
|
_status: { equals: "published" },
|
||||||
|
slug: { equals: slug },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tag?.docs?.[0]) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: tag.docs[0],
|
||||||
|
};
|
||||||
|
}
|
61
src/services/rest/blog.ts
Normal file
61
src/services/rest/blog.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { Blog } from "@/payload-types";
|
||||||
|
import { BlogData } from "@/schema/blog";
|
||||||
|
import { FetchBlogParams } from "@/schema/services/blog";
|
||||||
|
import { formatDate } from "@/utils/datetime";
|
||||||
|
import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize";
|
||||||
|
import { PaginatedDocs, Where } from "payload";
|
||||||
|
import { stringify } from "qs-esm";
|
||||||
|
|
||||||
|
export async function fetchBlogREST({ page, search = "", categoryId, tagId }: FetchBlogParams = {}) {
|
||||||
|
const queryCondition: Where = {
|
||||||
|
_status: { equals: "published" },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!!search) {
|
||||||
|
queryCondition["title"] = {
|
||||||
|
contains: search,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!!categoryId) {
|
||||||
|
queryCondition["categories"] = {
|
||||||
|
equals: categoryId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!!tagId) {
|
||||||
|
queryCondition["tags"] = {
|
||||||
|
equals: tagId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = stringify(
|
||||||
|
{
|
||||||
|
page,
|
||||||
|
pagination: true,
|
||||||
|
limit: 9,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
5
src/utils/datetime.ts
Normal file
5
src/utils/datetime.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export function formatDate(iso: string, format: string = "MMM, D YYYY") {
|
||||||
|
return dayjs(iso).format(format);
|
||||||
|
}
|
7
src/utils/general.ts
Normal file
7
src/utils/general.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function limitString(text: string) {
|
||||||
|
return `${text.length > 100 ? `${text.slice(0, 100)}...` : text}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomNumber(range: number): number {
|
||||||
|
return Math.floor(Math.random() * range) + 1;
|
||||||
|
}
|
32
src/utils/sanitize.ts
Normal file
32
src/utils/sanitize.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Blog } from "@/payload-types";
|
||||||
|
|
||||||
|
export function sanitizePageNumber(page: any, defaultPage = 1): number {
|
||||||
|
const parsedPage = Number(page);
|
||||||
|
|
||||||
|
if (isNaN(parsedPage) || parsedPage < 1 || !Number.isInteger(parsedPage)) {
|
||||||
|
return defaultPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeBlogContentIntoStringPreview(data: Blog["content"]) {
|
||||||
|
// Find the first paragraph that has children with text
|
||||||
|
const firstParagraph = data.root.children.find(
|
||||||
|
(node) =>
|
||||||
|
node.type === "paragraph" &&
|
||||||
|
Array.isArray(node.children) &&
|
||||||
|
node.children.length > 0 &&
|
||||||
|
!!node.children?.[0]?.text
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!firstParagraph) {
|
||||||
|
return "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const text = firstParagraph.children?.[0]?.text ?? "";
|
||||||
|
|
||||||
|
// Limit to 100 characters
|
||||||
|
return `${text.length > 100 ? text.slice(0, 100) : text}...`;
|
||||||
|
}
|
11
yarn.lock
11
yarn.lock
@ -4759,6 +4759,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dayjs@npm:^1.11.13":
|
||||||
|
version: 1.11.13
|
||||||
|
resolution: "dayjs@npm:1.11.13"
|
||||||
|
checksum: 10c0/a3caf6ac8363c7dade9d1ee797848ddcf25c1ace68d9fe8678ecf8ba0675825430de5d793672ec87b24a69bf04a1544b176547b2539982275d5542a7955f35b7
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.0":
|
"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.0":
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
resolution: "debug@npm:4.4.0"
|
resolution: "debug@npm:4.4.0"
|
||||||
@ -5015,6 +5022,7 @@ __metadata:
|
|||||||
"@types/node": "npm:^20"
|
"@types/node": "npm:^20"
|
||||||
"@types/react": "npm:^19"
|
"@types/react": "npm:^19"
|
||||||
"@types/react-dom": "npm:^19"
|
"@types/react-dom": "npm:^19"
|
||||||
|
dayjs: "npm:^1.11.13"
|
||||||
eslint: "npm:^9"
|
eslint: "npm:^9"
|
||||||
eslint-config-next: "npm:15.3.0"
|
eslint-config-next: "npm:15.3.0"
|
||||||
eslint-config-prettier: "npm:^10.1.2"
|
eslint-config-prettier: "npm:^10.1.2"
|
||||||
@ -5023,6 +5031,7 @@ __metadata:
|
|||||||
next: "npm:15.3.0"
|
next: "npm:15.3.0"
|
||||||
payload: "npm:^3.35.1"
|
payload: "npm:^3.35.1"
|
||||||
prettier: "npm:^3.5.3"
|
prettier: "npm:^3.5.3"
|
||||||
|
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"
|
||||||
swiper: "npm:^11.2.6"
|
swiper: "npm:^11.2.6"
|
||||||
@ -8673,7 +8682,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"qs-esm@npm:7.0.2":
|
"qs-esm@npm:7.0.2, qs-esm@npm:^7.0.2":
|
||||||
version: 7.0.2
|
version: 7.0.2
|
||||||
resolution: "qs-esm@npm:7.0.2"
|
resolution: "qs-esm@npm:7.0.2"
|
||||||
checksum: 10c0/b46e15883b91818fd6b0862cac97439dfe67a1401c00729756b16463fa97e094239017dd4f17369dd0cf586e262305b165ee485c0b1088ca4d2eb7ad11c0c8fe
|
checksum: 10c0/b46e15883b91818fd6b0862cac97439dfe67a1401c00729756b16463fa97e094239017dd4f17369dd0cf586e262305b165ee485c0b1088ca4d2eb7ad11c0c8fe
|
||||||
|
Loading…
x
Reference in New Issue
Block a user