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/richtext-lexical": "^3.35.1",
|
||||
"@payloadcms/storage-s3": "^3.35.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"graphql": "^16.8.1",
|
||||
"next": "15.3.0",
|
||||
"payload": "^3.35.1",
|
||||
"qs-esm": "^7.0.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"swiper": "^11.2.6"
|
||||
|
@ -1084,91 +1084,91 @@ $(function () {
|
||||
}
|
||||
|
||||
// RD Search
|
||||
if (plugins.search.length || plugins.searchResults) {
|
||||
var handler = "bat/rd-search.php";
|
||||
var defaultTemplate = '<h5 class="search-title"><a target="_top" href="#{href}" class="search-link">#{title}</a></h5>' +
|
||||
'<p>...#{token}...</p>' +
|
||||
'<p class="match"><em>Terms matched: #{count} - URL: #{href}</em></p>';
|
||||
var defaultFilter = '*.html';
|
||||
// if (plugins.search.length || plugins.searchResults) {
|
||||
// var handler = "bat/rd-search.php";
|
||||
// var defaultTemplate = '<h5 class="search-title"><a target="_top" href="#{href}" class="search-link">#{title}</a></h5>' +
|
||||
// '<p>...#{token}...</p>' +
|
||||
// '<p class="match"><em>Terms matched: #{count} - URL: #{href}</em></p>';
|
||||
// var defaultFilter = '*.html';
|
||||
|
||||
if (plugins.search.length) {
|
||||
for (var i = 0; i < plugins.search.length; i++) {
|
||||
var searchItem = $(plugins.search[i]),
|
||||
options = {
|
||||
element: searchItem,
|
||||
filter: (searchItem.attr('data-search-filter')) ? searchItem.attr('data-search-filter') : defaultFilter,
|
||||
template: (searchItem.attr('data-search-template')) ? searchItem.attr('data-search-template') : defaultTemplate,
|
||||
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,
|
||||
current: 0, processed: 0, timer: {}
|
||||
};
|
||||
// if (plugins.search.length) {
|
||||
// for (var i = 0; i < plugins.search.length; i++) {
|
||||
// var searchItem = $(plugins.search[i]),
|
||||
// options = {
|
||||
// element: searchItem,
|
||||
// filter: (searchItem.attr('data-search-filter')) ? searchItem.attr('data-search-filter') : defaultFilter,
|
||||
// template: (searchItem.attr('data-search-template')) ? searchItem.attr('data-search-template') : defaultTemplate,
|
||||
// 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,
|
||||
// current: 0, processed: 0, timer: {}
|
||||
// };
|
||||
|
||||
var $toggle = $('.rd-navbar-search-toggle');
|
||||
if ($toggle.length) {
|
||||
$toggle.on('click', (function (searchItem) {
|
||||
return function () {
|
||||
if (!($(this).hasClass('active'))) {
|
||||
searchItem.find('input').val('').trigger('propertychange');
|
||||
}
|
||||
}
|
||||
})(searchItem));
|
||||
}
|
||||
// var $toggle = $('.rd-navbar-search-toggle');
|
||||
// if ($toggle.length) {
|
||||
// $toggle.on('click', (function (searchItem) {
|
||||
// return function () {
|
||||
// if (!($(this).hasClass('active'))) {
|
||||
// searchItem.find('input').val('').trigger('propertychange');
|
||||
// }
|
||||
// }
|
||||
// })(searchItem));
|
||||
// }
|
||||
|
||||
if (options.live) {
|
||||
var clearHandler = false;
|
||||
// if (options.live) {
|
||||
// var clearHandler = false;
|
||||
|
||||
searchItem.find('input').on("input propertychange", $.proxy(function () {
|
||||
this.term = this.element.find('input').val().trim();
|
||||
this.spin = this.element.find('.input-group-addon');
|
||||
// searchItem.find('input').on("input propertychange", $.proxy(function () {
|
||||
// this.term = this.element.find('input').val().trim();
|
||||
// this.spin = this.element.find('.input-group-addon');
|
||||
|
||||
clearTimeout(this.timer);
|
||||
// clearTimeout(this.timer);
|
||||
|
||||
if (this.term.length > 2) {
|
||||
this.timer = setTimeout(liveSearch(this), 200);
|
||||
// if (this.term.length > 2) {
|
||||
// this.timer = setTimeout(liveSearch(this), 200);
|
||||
|
||||
if (clearHandler === false) {
|
||||
clearHandler = true;
|
||||
// if (clearHandler === false) {
|
||||
// clearHandler = true;
|
||||
|
||||
$body.on("click", function (e) {
|
||||
if ($(e.toElement).parents('.rd-search').length === 0) {
|
||||
$('#rd-search-results-live').addClass('cleared').html('');
|
||||
}
|
||||
})
|
||||
}
|
||||
// $body.on("click", function (e) {
|
||||
// if ($(e.toElement).parents('.rd-search').length === 0) {
|
||||
// $('#rd-search-results-live').addClass('cleared').html('');
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
} else if (this.term.length === 0) {
|
||||
$('#' + this.live).addClass('cleared').html('');
|
||||
}
|
||||
}, options, this));
|
||||
}
|
||||
// } else if (this.term.length === 0) {
|
||||
// $('#' + this.live).addClass('cleared').html('');
|
||||
// }
|
||||
// }, options, this));
|
||||
// }
|
||||
|
||||
searchItem.submit($.proxy(function () {
|
||||
$('<input />').attr('type', 'hidden')
|
||||
.attr('name', "filter")
|
||||
.attr('value', this.filter)
|
||||
.appendTo(this.element);
|
||||
return true;
|
||||
}, options, this))
|
||||
}
|
||||
}
|
||||
// searchItem.submit($.proxy(function () {
|
||||
// $('<input />').attr('type', 'hidden')
|
||||
// .attr('name', "filter")
|
||||
// .attr('value', this.filter)
|
||||
// .appendTo(this.element);
|
||||
// return true;
|
||||
// }, options, this))
|
||||
// }
|
||||
// }
|
||||
|
||||
if (plugins.searchResults.length) {
|
||||
var regExp = /\?.*s=([^&]+)\&filter=([^&]+)/g;
|
||||
var match = regExp.exec(location.search);
|
||||
// if (plugins.searchResults.length) {
|
||||
// var regExp = /\?.*s=([^&]+)\&filter=([^&]+)/g;
|
||||
// var match = regExp.exec(location.search);
|
||||
|
||||
if (match !== null) {
|
||||
$.get(handler, {
|
||||
s: decodeURI(match[1]),
|
||||
dataType: "html",
|
||||
filter: match[2],
|
||||
template: defaultTemplate,
|
||||
live: ''
|
||||
}, function (data) {
|
||||
plugins.searchResults.html(data);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (match !== null) {
|
||||
// $.get(handler, {
|
||||
// s: decodeURI(match[1]),
|
||||
// dataType: "html",
|
||||
// filter: match[2],
|
||||
// template: defaultTemplate,
|
||||
// live: ''
|
||||
// }, function (data) {
|
||||
// plugins.searchResults.html(data);
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Swiper
|
||||
function makeInterLeaveEffectOptions(interleaveOffset) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import CardBlog from "@/components/CardBlog";
|
||||
import ListOfBlog from "@/components/blogs/ListOfBlog";
|
||||
import HeroImage from "@/components/HeroImage";
|
||||
import { CardBlogData } from "@/schema/blog";
|
||||
import { getDefaultMetadata } from "@/utils/metadata";
|
||||
import { Metadata } from "next";
|
||||
|
||||
@ -15,57 +14,8 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export default function Blog() {
|
||||
const data: CardBlogData[] = [
|
||||
{
|
||||
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: "",
|
||||
},
|
||||
];
|
||||
export default async function Blog({ searchParams }: { searchParams?: Promise<{ s?: string }> }) {
|
||||
const params = await searchParams;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -82,21 +32,14 @@ export default function Blog() {
|
||||
type="text"
|
||||
name="s"
|
||||
autoComplete="off"
|
||||
defaultValue={params?.s}
|
||||
/>
|
||||
</div>
|
||||
<button className="rd-search-submit" type="submit"></button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-5">
|
||||
{data.map((blog) => (
|
||||
<CardBlog key={blog.slug} data={blog} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<button type="submit" className="button button-primary">
|
||||
LOAD MORE...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ListOfBlog searchKeyword={params?.s} />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
|
@ -99,4 +99,9 @@ export const Blogs: CollectionConfig = {
|
||||
group: "Blogs",
|
||||
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 Link from "next/link";
|
||||
|
||||
type CardBlogProps = {
|
||||
data: CardBlogData;
|
||||
data: BlogData;
|
||||
};
|
||||
|
||||
export default function CardBlog({ data }: CardBlogProps) {
|
||||
return (
|
||||
<div>
|
||||
<article className="post-default">
|
||||
<a className="post-default-image" href="blog-post.html">
|
||||
<Image src={data.img} alt={data.title} width="736" height="540" />
|
||||
</a>
|
||||
<div className="h-64 relative">
|
||||
<Link href="#">
|
||||
<Image src={data.img?.url ?? ""} alt={data.img?.alt ?? ""} fill />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="post-default-body">
|
||||
<div className="post-default-title">
|
||||
<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);
|
||||
|
||||
export default buildConfig({
|
||||
cors: [process.env.SITE_URL || ""],
|
||||
csrf: [process.env.SITE_URL || ""],
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
importMap: {
|
||||
|
@ -1,7 +1,7 @@
|
||||
export type CardBlogData = {
|
||||
slug: string;
|
||||
export type BlogData = {
|
||||
slug?: string | null;
|
||||
title: string;
|
||||
description: string;
|
||||
img: string;
|
||||
img?: { url: string; alt?: 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
|
||||
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":
|
||||
version: 4.4.0
|
||||
resolution: "debug@npm:4.4.0"
|
||||
@ -5015,6 +5022,7 @@ __metadata:
|
||||
"@types/node": "npm:^20"
|
||||
"@types/react": "npm:^19"
|
||||
"@types/react-dom": "npm:^19"
|
||||
dayjs: "npm:^1.11.13"
|
||||
eslint: "npm:^9"
|
||||
eslint-config-next: "npm:15.3.0"
|
||||
eslint-config-prettier: "npm:^10.1.2"
|
||||
@ -5023,6 +5031,7 @@ __metadata:
|
||||
next: "npm:15.3.0"
|
||||
payload: "npm:^3.35.1"
|
||||
prettier: "npm:^3.5.3"
|
||||
qs-esm: "npm:^7.0.2"
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
swiper: "npm:^11.2.6"
|
||||
@ -8673,7 +8682,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs-esm@npm:7.0.2":
|
||||
"qs-esm@npm:7.0.2, qs-esm@npm:^7.0.2":
|
||||
version: 7.0.2
|
||||
resolution: "qs-esm@npm:7.0.2"
|
||||
checksum: 10c0/b46e15883b91818fd6b0862cac97439dfe67a1401c00729756b16463fa97e094239017dd4f17369dd0cf586e262305b165ee485c0b1088ca4d2eb7ad11c0c8fe
|
||||
|
Loading…
x
Reference in New Issue
Block a user