From 313a5761061a07b3d52c188cc5ae9f3aca493cc8 Mon Sep 17 00:00:00 2001 From: RizqiSyahrendra Date: Mon, 3 Feb 2025 12:13:56 +0700 Subject: [PATCH] feat(blog): blog list integration --- src/app/(main)/blog/[slug]/page.tsx | 85 +------- src/app/(main)/{slick-blog => blog}/page.tsx | 41 ++-- .../(main)/slick-blog-single/[id]/page.tsx | 202 ------------------ src/components/Blogs.tsx | 63 ------ src/components/{ => Blogs}/Blog.tsx | 0 src/components/Blogs/BlogCardItem.tsx | 63 ++++++ src/components/{ => Blogs}/BlogComments.tsx | 0 src/components/{ => Blogs}/BlogWidget.tsx | 52 +---- src/components/Blogs/Blogs.tsx | 56 +++++ src/components/Header.tsx | 2 +- src/components/Homepage.tsx | 2 +- src/components/Pagination.tsx | 152 +++++++------ src/data/menu.ts | 4 +- src/services/payload/blog.ts | 52 +++++ src/utils/sanitize.ts | 32 +++ 15 files changed, 323 insertions(+), 483 deletions(-) rename src/app/(main)/{slick-blog => blog}/page.tsx (77%) delete mode 100644 src/app/(main)/slick-blog-single/[id]/page.tsx delete mode 100644 src/components/Blogs.tsx rename src/components/{ => Blogs}/Blog.tsx (100%) create mode 100644 src/components/Blogs/BlogCardItem.tsx rename src/components/{ => Blogs}/BlogComments.tsx (100%) rename src/components/{ => Blogs}/BlogWidget.tsx (53%) create mode 100644 src/components/Blogs/Blogs.tsx create mode 100644 src/services/payload/blog.ts create mode 100644 src/utils/sanitize.ts diff --git a/src/app/(main)/blog/[slug]/page.tsx b/src/app/(main)/blog/[slug]/page.tsx index 08e1ec8..10745bc 100644 --- a/src/app/(main)/blog/[slug]/page.tsx +++ b/src/app/(main)/blog/[slug]/page.tsx @@ -1,44 +1,17 @@ -import BlogComments from "@/components/BlogComments"; -import BlogWidget from "@/components/BlogWidget"; +import BlogComments from "@/components/Blogs/BlogComments"; +import BlogWidget from "@/components/Blogs/BlogWidget"; import CommentForm from "@/components/CommentForm"; -import { allBlogs } from "@/data/blogs"; -import payloadConfig from "@/payload.config"; -import { formatDate } from "@/utils/datetime"; -import Image from "next/image"; -import { getPayload } from "payload"; +import { fetchBlogDetail } from "@/services/payload/blog"; import { RichText } from "@payloadcms/richtext-lexical/react"; import { Metadata } from "next"; - -async function fetchBlog(slug: string) { - const payload = await getPayload({ config: payloadConfig }); - const blogDataQuery = await payload.find({ - collection: "blogs", - where: { - slug: { equals: slug }, - }, - limit: 1, - pagination: false, - }); - - if (!blogDataQuery?.docs?.[0]) return null; - - const data = blogDataQuery?.docs?.[0]; - const createdAt = formatDate(data.createdAt); - const imgUrl = typeof data.img !== "number" ? (data?.img?.url ?? "") : ""; - - return { - data, - createdAt, - imgUrl, - }; -} +import Image from "next/image"; export async function generateMetadata({ params, }: { params: { slug: string }; }): Promise { - const blog = await fetchBlog(params.slug); + const blog = await fetchBlogDetail(params.slug); if (!blog) { return { @@ -65,17 +38,16 @@ export async function generateMetadata({ }; } -export default async function BlogRead({ +export default async function SingleBlogPage({ params, }: { params: Promise<{ slug: string }>; }) { const slug = (await params).slug; - const data = await fetchBlog(slug); + const data = await fetchBlogDetail(slug); if (!data) return <>; - const blog = allBlogs.filter((elm) => elm.id == 34)[0] || allBlogs[0]; return ( <>
-
- - - Author: John Doe - -
- Categories: + Categories:{" "} Design, Branding
- {/* End Author, Categories, Comments */} @@ -158,36 +119,6 @@ export default async function BlogRead({ {/* End Post */} - {/* Comments */} -
-

- Comments (3) -

-
    - -
-
- {/* End Comments */} - {/* Add Comment */} -
-

Leave a comment

- {/* Form */} - - {/* End Form */} -
- {/* End Add Comment */} - {/* Prev/Next Post */} - - {/* End Prev/Next Post */} {/* End Content */} diff --git a/src/app/(main)/slick-blog/page.tsx b/src/app/(main)/blog/page.tsx similarity index 77% rename from src/app/(main)/slick-blog/page.tsx rename to src/app/(main)/blog/page.tsx index a127f25..a7cc3af 100644 --- a/src/app/(main)/slick-blog/page.tsx +++ b/src/app/(main)/blog/page.tsx @@ -1,19 +1,22 @@ -import Blogs from "@/components/Blogs"; -import NewsletterForm from "@/components/NewsletterForm"; +import Blogs from "@/components/Blogs/Blogs"; import { archiveLinks } from "@/data/archieve"; import { categories } from "@/data/categories"; import { tags } from "@/data/tags"; +import { sanitizePageNumber } from "@/utils/sanitize"; import Image from "next/image"; export const metadata = { - title: - "Slick Blogs || Resonance — One & Multi Page React Nextjs Creative Template", - description: - "Resonance — One & Multi Page React Nextjs Creative Template", + title: "Blogs | Cochise Oncology", + description: "Blogs | Cochise Oncology", }; -const onePage = false; -const dark = false; -export default function SlickBlogPage() { + +export default async function BlogPage({ + searchParams, +}: { + searchParams?: { page?: string }; +}) { + const page = sanitizePageNumber(searchParams?.page); + return ( <>
- +
<> @@ -58,7 +61,7 @@ export default function SlickBlogPage() {
-
+
{/* Widget */}

Categories

@@ -77,7 +80,7 @@ export default function SlickBlogPage() {
{/* End Widget */}
-
+
{/* Widget */}

Tags

@@ -93,7 +96,7 @@ export default function SlickBlogPage() {
{/* End Widget */}
-
+
{/* Widget */}

Archive

@@ -111,18 +114,6 @@ export default function SlickBlogPage() {
{/* End Widget */}
-
- {/* Widget */} -
-

Newsletter

-
-
- -
-
-
- {/* End Widget */} -
diff --git a/src/app/(main)/slick-blog-single/[id]/page.tsx b/src/app/(main)/slick-blog-single/[id]/page.tsx deleted file mode 100644 index 3864163..0000000 --- a/src/app/(main)/slick-blog-single/[id]/page.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import BlogComments from "@/components/BlogComments"; -import BlogWidget from "@/components/BlogWidget"; -import CommentForm from "@/components/CommentForm"; -import { allBlogs } from "@/data/blogs"; -import Image from "next/image"; -export const metadata = { - title: - "Slick Blogs Single || Resonance — One & Multi Page React Nextjs Creative Template", - description: - "Resonance — One & Multi Page React Nextjs Creative Template", -}; - -export default async function asyncSlickBlogSinglePage( - params: Promise<{ id: number }> -) { - const id = (await params).id; - const blog = allBlogs.filter((elm) => elm.id == id)[0] || allBlogs[0]; - return ( - <> -
- {/* */} -
- -
- {/* */} - -
-
-
-

- {/* @ts-ignore */} - {blog?.title || blog?.postTitle} -

- {/* Author, Categories, Comments */} - - {/* End Author, Categories, Comments */} -
-
-
-
- <> - {/* Section */} -
-
-
- {/* Content */} -
- {/* Post */} -
-
-
- Image Description -
-

- Morbi lacus massa, euismod ut turpis molestie, tristique - sodales est. Integer sit amet mi id sapien tempor molestie - in nec massa. Fusce non ante sed lorem rutrum feugiat. - Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Mauris non laoreet dui. Morbi lacus massa, euismod ut - turpis molestie, tristique sodales est. Integer sit amet - mi id sapien tempor molestie in nec massa. -

-

- Fusce non ante sed lorem rutrum feugiat. Vestibulum - pellentesque, purus ut dignissim consectetur, nulla - erat ultrices purus, ut consequat sem elit non sem. - Morbi lacus massa, euismod ut turpis molestie, tristique - sodales est. Integer sit amet mi id sapien tempor molestie - in nec massa. Fusce non ante sed lorem rutrum feugiat. -

-
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Integer posuere erat a ante. Vestibulum - pellentesque, purus ut dignissim consectetur, nulla erat - ultrices purus. -

-
- Someone famous in - Source Title -
-
-

- Praesent ultricies ut ipsum non laoreet. Nunc ac - ultricies leo. Nulla ac ultrices arcu. - Nullam adipiscing lacus in consectetur posuere. Nunc - malesuada tellus turpis, ac pretium orci molestie vel. - Morbi lacus massa, euismod ut turpis molestie, tristique - sodales est. Integer sit amet mi id sapien tempor molestie - in nec massa. Fusce non ante sed lorem rutrum feugiat. -

-
    -
  • First item of the list
  • -
  • Second item of the list
  • -
  • Third item of the list
  • -
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Mauris non laoreet dui. Morbi lacus massa, euismod ut - turpis molestie, tristique sodales est. Integer sit amet - mi id sapien tempor molestie in nec massa. Fusce non ante - sed lorem rutrum feugiat. Vestibulum pellentesque, purus - ut dignissim consectetur, nulla erat ultrices purus, - ut consequat sem elit non sem. -

-
-
- {/* End Post */} - {/* Comments */} -
-

- Comments (3) -

-
    - -
-
- {/* End Comments */} - {/* Add Comment */} -
-

Leave a comment

- {/* Form */} - - {/* End Form */} -
- {/* End Add Comment */} - {/* Prev/Next Post */} - - {/* End Prev/Next Post */} -
- {/* End Content */} -
-
-
- {/* End Section */} - {/* Divider */} -
- {/* End Divider */} - {/* Section */} -
-
-
- -
-
-
- - - ); -} diff --git a/src/components/Blogs.tsx b/src/components/Blogs.tsx deleted file mode 100644 index 5931843..0000000 --- a/src/components/Blogs.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import Pagination from "@/components/Pagination"; -import { blogs11 } from "@/data/blogs"; -import Image from "next/image"; -import Link from "next/link"; -import React from "react"; - -export default function Blogs() { - return ( -
- {/* Blog Posts Grid */} -
- {/* Post Item */} - {blogs11.map((elm, i) => ( -
-
-
- - Add Image Description - -
-
-

- {elm.title} -

-
{elm.text}
-
- - -
-
-
-
- ))} - {/* End Post Item */} -
- {/* End Blog Posts Grid */} - {/* Pagination */} - - {/* End Pagination */} -
- ); -} diff --git a/src/components/Blog.tsx b/src/components/Blogs/Blog.tsx similarity index 100% rename from src/components/Blog.tsx rename to src/components/Blogs/Blog.tsx diff --git a/src/components/Blogs/BlogCardItem.tsx b/src/components/Blogs/BlogCardItem.tsx new file mode 100644 index 0000000..32767cd --- /dev/null +++ b/src/components/Blogs/BlogCardItem.tsx @@ -0,0 +1,63 @@ +import Image from "next/image"; +import Link from "next/link"; + +export interface BlogCardItemProps { + data: { + slug: string; + title: string; + img?: { + url: string; + alt: string; + }; + contentPreview: string; + author?: { + name: string; + img: string; + }; + date: string; + }; +} + +export function BlogCardItem({ data }: BlogCardItemProps) { + return ( +
+
+
+ + {data?.img?.alt + +
+
+

+ {data.title} +

+
{data.contentPreview}
+
+ {!!data?.author?.name && ( + + )} + +
+
+
+
+ ); +} diff --git a/src/components/BlogComments.tsx b/src/components/Blogs/BlogComments.tsx similarity index 100% rename from src/components/BlogComments.tsx rename to src/components/Blogs/BlogComments.tsx diff --git a/src/components/BlogWidget.tsx b/src/components/Blogs/BlogWidget.tsx similarity index 53% rename from src/components/BlogWidget.tsx rename to src/components/Blogs/BlogWidget.tsx index cf68673..53d2bfa 100644 --- a/src/components/BlogWidget.tsx +++ b/src/components/Blogs/BlogWidget.tsx @@ -10,7 +10,7 @@ export default function BlogWidget({ }) { return ( <> -
+
{/* Widget */}

Categories

@@ -29,7 +29,7 @@ export default function BlogWidget({
{/* End Widget */}
-
+
{/* Widget */}

Tags

@@ -45,7 +45,7 @@ export default function BlogWidget({
{/* End Widget */}
-
+
{/* Widget */}

Archive

@@ -63,52 +63,6 @@ export default function BlogWidget({
{/* End Widget */}
- -
-
-

Newsletter

- -
-
-
e.preventDefault()} - className="form" - id="mailchimp" - > -
Stay informed with our newsletter.
- -
- - -
- -
- - By sending the form you agree to the - Terms & Conditions and - Privacy Policy. -
- -
-
-
-
-
-
); } diff --git a/src/components/Blogs/Blogs.tsx b/src/components/Blogs/Blogs.tsx new file mode 100644 index 0000000..485e317 --- /dev/null +++ b/src/components/Blogs/Blogs.tsx @@ -0,0 +1,56 @@ +import Pagination from "@/components/Pagination"; +import { fetchBlog } from "@/services/payload/blog"; +import { BlogCardItem } from "./BlogCardItem"; +import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize"; + +export interface BlogsProps { + page: number; +} + +export default async function Blogs({ page }: BlogsProps) { + const data = await fetchBlog(page); + + if (!data?.totalDocs) return <>; + + const handleClickPage = (clickedPage: number) => { + if (typeof window === "undefined") return; + + window.location.href = `/blog/?page=${clickedPage}`; + }; + + return ( +
+ {/* Blog Posts Grid */} +
+ {/* Post Item */} + {data.formattedData.map((blog, i) => ( + + ))} + {/* End Post Item */} +
+ {/* End Blog Posts Grid */} + {/* Pagination */} + {data.totalPages > 1 && ( + + )} + {/* End Pagination */} +
+ ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index af4181d..ba64c8f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -13,7 +13,7 @@ const links = [ import Image from "next/image"; import LanguageSelect from "./LanguageSelect"; -export default function Header9({ links }: any) { +export default function Header({ links }: any) { return (
{/* Logo (* Add your text or image to the link tag. Use SVG or PNG image format. diff --git a/src/components/Homepage.tsx b/src/components/Homepage.tsx index e217e22..316a68c 100644 --- a/src/components/Homepage.tsx +++ b/src/components/Homepage.tsx @@ -5,7 +5,7 @@ import Service from "./Service"; import Portfolio from "./Portfolio"; import Image from "next/image"; import Testimonials from "./Testimonials"; -import Blog from "./Blog"; +import Blog from "./Blogs/Blog"; import Newsletter from "./Newsletter"; import Contact from "./Contact"; import Link from "next/link"; diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index a599fc1..ff07298 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -1,78 +1,104 @@ "use client"; -import React, { useState } from "react"; -export default function Pagination({ className }: { className?: string }) { - const [activePage, setActivePage] = useState(1); // Initialize active page +import React from "react"; + +interface PaginationProps { + page: number; + hasPreviousPage: boolean; + hasNextPage: boolean; + totalPages: number; + onClickPage?: (page: number) => void; +} + +export default function Pagination({ + page, + hasPreviousPage, + hasNextPage, + totalPages, + onClickPage, +}: PaginationProps) { + const activePage = page; // Function to handle page change - const handlePageChange = (page: number) => { - setActivePage(page); + const handlePageChange = (page: string | number) => { + if (typeof page === "string") return; + onClickPage?.(page); + }; + + const getPageNumbers = () => { + const pages = []; + const showEllipsisStart = activePage > 4; + const showEllipsisEnd = activePage < totalPages - 3; + + if (totalPages <= 7) { + // Show all pages if total is 7 or less + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + // Always show first page + pages.push(1); + + if (showEllipsisStart) { + pages.push("..."); + } + + // Show pages around current page + const start = showEllipsisStart ? Math.max(2, activePage - 1) : 2; + const end = showEllipsisEnd + ? Math.min(totalPages - 1, activePage + 1) + : totalPages - 1; + + for (let i = start; i <= end; i++) { + pages.push(i); + } + + if (showEllipsisEnd) { + pages.push("..."); + } + + // Always show last page + pages.push(totalPages); + } + + return pages; }; return ( -
+
{/* Previous Page Button */} - activePage > 1 && handlePageChange(activePage - 1)} - className={activePage === 1 ? "disabled" : ""} - > - - Previous page - - - {/* Page Number 1 */} - handlePageChange(1)} - className={activePage === 1 ? "active" : ""} - > - 1 - - - {/* Page Number 2 */} - handlePageChange(2)} - className={activePage === 2 ? "active" : ""} - > - 2 - - - {/* Page Number 3 */} - handlePageChange(3)} - className={activePage === 3 ? "active" : ""} - > - 3 - - - {activePage > 4 && activePage < 8 && ( - ... + {hasPreviousPage && ( + activePage > 1 && handlePageChange(activePage - 1)} + className={activePage === 1 ? "disabled" : ""} + > + + Previous page + )} - {activePage > 3 && activePage < 8 && ( - {activePage} - )} - - {/* Ellipsis */} - ... - {activePage == 8 && {8}} - {/* Page Number 9 */} - handlePageChange(9)} - className={activePage === 9 ? "active" : ""} - > - 9 - + {getPageNumbers().map((page, key) => ( + handlePageChange(page)} + className={activePage === page ? "active" : ""} + > + {page} + + ))} {/* Next Page Button */} - activePage < 9 && handlePageChange(activePage + 1)} - className={activePage === 9 ? "disabled" : ""} - > - - Next page - + {hasNextPage && ( + + activePage < totalPages && handlePageChange(activePage + 1) + } + className={activePage === totalPages ? "disabled" : ""} + > + + Next page + + )}
); } diff --git a/src/data/menu.ts b/src/data/menu.ts index 6b6aa6c..fead10e 100644 --- a/src/data/menu.ts +++ b/src/data/menu.ts @@ -28,7 +28,7 @@ export const slickMultipages = [ { href: "/slick-about", text: "About", class: "active" }, { href: "/slick-services", text: "Services" }, { href: "/slick-portfolio", text: "Portfolio" }, - { href: "/slick-blog", text: "Blog" }, + { href: "/blog", text: "Blog" }, { href: "/slick-contact", text: "Contact" }, ]; export const slickMultipagesDark = [ @@ -36,7 +36,7 @@ export const slickMultipagesDark = [ { href: "/slick-about-dark", text: "About", class: "active" }, { href: "/slick-services-dark", text: "Services" }, { href: "/slick-portfolio-dark", text: "Portfolio" }, - { href: "/slick-blog-dark", text: "Blog" }, + { href: "/blog", text: "Blog" }, { href: "/slick-contact-dark", text: "Contact" }, ]; export const slickOnepage = [ diff --git a/src/services/payload/blog.ts b/src/services/payload/blog.ts new file mode 100644 index 0000000..77fae5c --- /dev/null +++ b/src/services/payload/blog.ts @@ -0,0 +1,52 @@ +import payloadConfig from "@/payload.config"; +import { formatDate } from "@/utils/datetime"; +import { getPayload } from "payload"; + +export async function fetchBlog(page: number = 1) { + const payload = await getPayload({ config: payloadConfig }); + const blogDataQuery = await payload.find({ + collection: "blogs", + page, + pagination: true, + }); + + 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) { + const payload = await getPayload({ config: payloadConfig }); + const blogDataQuery = await payload.find({ + collection: "blogs", + where: { + slug: { equals: slug }, + }, + limit: 1, + pagination: false, + }); + + if (!blogDataQuery?.docs?.[0]) return null; + + const data = blogDataQuery?.docs?.[0]; + const createdAt = formatDate(data.createdAt); + const imgUrl = typeof data.img !== "number" ? (data?.img?.url ?? "") : ""; + + return { + data, + createdAt, + imgUrl, + }; +} diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts new file mode 100644 index 0000000..34432f6 --- /dev/null +++ b/src/utils/sanitize.ts @@ -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}...`; +}