feat(blog): blog list integration
This commit is contained in:
parent
e7268f644d
commit
313a576106
@ -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<Metadata> {
|
||||
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 (
|
||||
<>
|
||||
<section
|
||||
@ -111,22 +83,11 @@ export default async function BlogRead({
|
||||
{data.createdAt}
|
||||
</a>
|
||||
</div>
|
||||
<div className="d-inline-block me-3">
|
||||
<a href="#">
|
||||
<i className="mi-user size-16" />
|
||||
<span className="visually-hidden">Author:</span> John Doe
|
||||
</a>
|
||||
</div>
|
||||
<div className="d-inline-block me-3">
|
||||
<i className="mi-folder size-16" />
|
||||
<span className="visually-hidden">Categories:</span>
|
||||
<span className="visually-hidden">Categories:</span>{" "}
|
||||
<a href="#">Design</a>, <a href="#">Branding</a>
|
||||
</div>
|
||||
<div className="d-inline-block me-3">
|
||||
<a href="#">
|
||||
<i className="mi-message size-16" /> 5 Comments
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Author, Categories, Comments */}
|
||||
</div>
|
||||
@ -158,36 +119,6 @@ export default async function BlogRead({
|
||||
</div>
|
||||
</div>
|
||||
{/* End Post */}
|
||||
{/* Comments */}
|
||||
<div className="mb-80 mb-xs-40">
|
||||
<h4 className="blog-page-title">
|
||||
Comments <small className="number">(3)</small>
|
||||
</h4>
|
||||
<ul className="media-list comment-list clearlist">
|
||||
<BlogComments />
|
||||
</ul>
|
||||
</div>
|
||||
{/* End Comments */}
|
||||
{/* Add Comment */}
|
||||
<div className="mb-80 mb-xs-40">
|
||||
<h4 className="blog-page-title">Leave a comment</h4>
|
||||
{/* Form */}
|
||||
<CommentForm />
|
||||
{/* End Form */}
|
||||
</div>
|
||||
{/* End Add Comment */}
|
||||
{/* Prev/Next Post */}
|
||||
<div className="clearfix mt-40">
|
||||
<a href="#" className="blog-item-more circle left">
|
||||
<i className="mi-chevron-left" />
|
||||
Prev post
|
||||
</a>
|
||||
<a href="#" className="blog-item-more circle right">
|
||||
Next post
|
||||
<i className="mi-chevron-right" />
|
||||
</a>
|
||||
</div>
|
||||
{/* End Prev/Next Post */}
|
||||
</div>
|
||||
{/* End Content */}
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<>
|
||||
<section
|
||||
@ -47,7 +50,7 @@ export default function SlickBlogPage() {
|
||||
</div>
|
||||
</section>
|
||||
<section className="page-section" id="blog">
|
||||
<Blogs />
|
||||
<Blogs page={page} />
|
||||
</section>
|
||||
|
||||
<>
|
||||
@ -58,7 +61,7 @@ export default function SlickBlogPage() {
|
||||
<section className="page-section">
|
||||
<div className="container relative">
|
||||
<div className="row mt-n60">
|
||||
<div className="col-sm-6 col-lg-3 mt-60">
|
||||
<div className="col-sm-6 col-lg-4 mt-60">
|
||||
{/* Widget */}
|
||||
<div className="widget mb-0">
|
||||
<h3 className="widget-title">Categories</h3>
|
||||
@ -77,7 +80,7 @@ export default function SlickBlogPage() {
|
||||
</div>
|
||||
{/* End Widget */}
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3 mt-60">
|
||||
<div className="col-sm-6 col-lg-4 mt-60">
|
||||
{/* Widget */}
|
||||
<div className="widget mb-0">
|
||||
<h3 className="widget-title">Tags</h3>
|
||||
@ -93,7 +96,7 @@ export default function SlickBlogPage() {
|
||||
</div>
|
||||
{/* End Widget */}
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3 mt-60">
|
||||
<div className="col-sm-6 col-lg-4 mt-60">
|
||||
{/* Widget */}
|
||||
<div className="widget mb-0">
|
||||
<h3 className="widget-title">Archive</h3>
|
||||
@ -111,18 +114,6 @@ export default function SlickBlogPage() {
|
||||
</div>
|
||||
{/* End Widget */}
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3 mt-60">
|
||||
{/* Widget */}
|
||||
<div className="widget mb-0">
|
||||
<h3 className="widget-title">Newsletter</h3>
|
||||
<div className="widget-body">
|
||||
<div className="widget-text clearfix">
|
||||
<NewsletterForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Widget */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -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 (
|
||||
<>
|
||||
<section
|
||||
className="page-section bg-gradient-gray-light-1 bg-scroll overflow-hidden"
|
||||
id="home"
|
||||
>
|
||||
{/* <!-- Background Shape --> */}
|
||||
<div className="bg-shape-1 wow fadeIn">
|
||||
<Image
|
||||
src="/assets/images/demo-fancy/bg-shape-1.svg"
|
||||
width={1443}
|
||||
height={844}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
{/* <!-- End Background Shape --> */}
|
||||
|
||||
<div className="container position-relative pt-sm-40 text-center">
|
||||
<div className="row">
|
||||
<div className="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
|
||||
<h1 className="hs-title-10 mb-10 wow fadeInUp">
|
||||
{/* @ts-ignore */}
|
||||
{blog?.title || blog?.postTitle}
|
||||
</h1>
|
||||
{/* Author, Categories, Comments */}
|
||||
<div
|
||||
className="blog-item-data mb-0 wow fadeIn"
|
||||
data-wow-delay="0.2s"
|
||||
>
|
||||
<div className="d-inline-block me-3">
|
||||
<a href="#">
|
||||
<i className="mi-clock size-16" />
|
||||
<span className="visually-hidden">Date:</span> December 25
|
||||
</a>
|
||||
</div>
|
||||
<div className="d-inline-block me-3">
|
||||
<a href="#">
|
||||
<i className="mi-user size-16" />
|
||||
<span className="visually-hidden">Author:</span> John Doe
|
||||
</a>
|
||||
</div>
|
||||
<div className="d-inline-block me-3">
|
||||
<i className="mi-folder size-16" />
|
||||
<span className="visually-hidden">Categories:</span>
|
||||
<a href="#">Design</a>, <a href="#">Branding</a>
|
||||
</div>
|
||||
<div className="d-inline-block me-3">
|
||||
<a href="#">
|
||||
<i className="mi-message size-16" /> 5 Comments
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Author, Categories, Comments */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<>
|
||||
{/* Section */}
|
||||
<section className="page-section">
|
||||
<div className="container relative">
|
||||
<div className="row">
|
||||
{/* Content */}
|
||||
<div className="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
|
||||
{/* Post */}
|
||||
<div className="blog-item mb-80 mb-xs-40">
|
||||
<div className="blog-item-body">
|
||||
<div className="mb-40 mb-xs-30">
|
||||
<Image
|
||||
src="/assets/images/demo-fancy/blog/post-prev-3-large.jpg"
|
||||
alt="Image Description"
|
||||
width={1350}
|
||||
height={759}
|
||||
className="round"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<blockquote>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Integer posuere erat a ante. Vestibulum
|
||||
pellentesque, purus ut dignissim consectetur, nulla erat
|
||||
ultrices purus.
|
||||
</p>
|
||||
<footer>
|
||||
Someone famous in
|
||||
<cite title="Source Title"> Source Title </cite>
|
||||
</footer>
|
||||
</blockquote>
|
||||
<p>
|
||||
Praesent ultricies ut ipsum non laoreet. Nunc ac
|
||||
<a href="#">ultricies</a> 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.
|
||||
</p>
|
||||
<ul>
|
||||
<li>First item of the list</li>
|
||||
<li>Second item of the list</li>
|
||||
<li>Third item of the list</li>
|
||||
</ul>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Post */}
|
||||
{/* Comments */}
|
||||
<div className="mb-80 mb-xs-40">
|
||||
<h4 className="blog-page-title">
|
||||
Comments <small className="number">(3)</small>
|
||||
</h4>
|
||||
<ul className="media-list comment-list clearlist">
|
||||
<BlogComments />
|
||||
</ul>
|
||||
</div>
|
||||
{/* End Comments */}
|
||||
{/* Add Comment */}
|
||||
<div className="mb-80 mb-xs-40">
|
||||
<h4 className="blog-page-title">Leave a comment</h4>
|
||||
{/* Form */}
|
||||
<CommentForm />
|
||||
{/* End Form */}
|
||||
</div>
|
||||
{/* End Add Comment */}
|
||||
{/* Prev/Next Post */}
|
||||
<div className="clearfix mt-40">
|
||||
<a href="#" className="blog-item-more circle left">
|
||||
<i className="mi-chevron-left" />
|
||||
Prev post
|
||||
</a>
|
||||
<a href="#" className="blog-item-more circle right">
|
||||
Next post
|
||||
<i className="mi-chevron-right" />
|
||||
</a>
|
||||
</div>
|
||||
{/* End Prev/Next Post */}
|
||||
</div>
|
||||
{/* End Content */}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/* End Section */}
|
||||
{/* Divider */}
|
||||
<hr className="mt-0 mb-0" />
|
||||
{/* End Divider */}
|
||||
{/* Section */}
|
||||
<section className="page-section">
|
||||
<div className="container relative">
|
||||
<div className="row mt-n60">
|
||||
<BlogWidget
|
||||
inputClass="newsletter-field form-control input-md circle mb-10"
|
||||
btnClass="btn btn-mod btn-color btn-medium btn-circle btn-hover-anim form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div className="container position-relative">
|
||||
{/* Blog Posts Grid */}
|
||||
<div className="row mt-n50 mb-50">
|
||||
{/* Post Item */}
|
||||
{blogs11.map((elm, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="post-prev-3 col-12 col-lg-10 offset-lg-1 col-xl-6 offset-xl-0 mt-50"
|
||||
>
|
||||
<div className="post-prev-3-container d-block d-sm-flex">
|
||||
<div className="post-prev-3-img">
|
||||
<Link href={`/slick-blog-single/${elm.id}`}>
|
||||
<Image
|
||||
width={400}
|
||||
height={488}
|
||||
src={elm.imgSrc}
|
||||
alt="Add Image Description"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="post-prev-3-intro">
|
||||
<h4 className="post-prev-3-title">
|
||||
<Link href={`/slick-blog-single/${elm.id}`}>{elm.title}</Link>
|
||||
</h4>
|
||||
<div className="post-prev-3-text">{elm.text}</div>
|
||||
<div className="post-prev-3-info clearfix">
|
||||
<div className="float-start">
|
||||
<a href="#">
|
||||
<Image
|
||||
className="post-prev-3-author-img"
|
||||
width={30}
|
||||
height={30}
|
||||
src={elm.authorImg}
|
||||
alt="Image Description"
|
||||
/>
|
||||
</a>
|
||||
<a href="#">{elm.author}</a>
|
||||
</div>
|
||||
<div className="float-end">
|
||||
<a href="#">{elm.date}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* End Post Item */}
|
||||
</div>
|
||||
{/* End Blog Posts Grid */}
|
||||
{/* Pagination */}
|
||||
<Pagination />
|
||||
{/* End Pagination */}
|
||||
</div>
|
||||
);
|
||||
}
|
63
src/components/Blogs/BlogCardItem.tsx
Normal file
63
src/components/Blogs/BlogCardItem.tsx
Normal file
@ -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 (
|
||||
<div className="post-prev-3 col-12 col-lg-10 offset-lg-1 col-xl-6 offset-xl-0 mt-50">
|
||||
<div className="post-prev-3-container d-block d-sm-flex">
|
||||
<div className="post-prev-3-img">
|
||||
<Link href={`/blog/${data.slug}`}>
|
||||
<Image
|
||||
width={400}
|
||||
height={488}
|
||||
src={data?.img?.url ?? ""}
|
||||
alt={data?.img?.alt ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="post-prev-3-intro">
|
||||
<h4 className="post-prev-3-title">
|
||||
<Link href={`/blog/${data.slug}`}>{data.title}</Link>
|
||||
</h4>
|
||||
<div className="post-prev-3-text">{data.contentPreview}</div>
|
||||
<div className="post-prev-3-info clearfix">
|
||||
{!!data?.author?.name && (
|
||||
<div className="float-start">
|
||||
<a href="#">
|
||||
<Image
|
||||
className="post-prev-3-author-img"
|
||||
width={30}
|
||||
height={30}
|
||||
src={data.author?.img ?? "#"}
|
||||
alt="Image Description"
|
||||
/>
|
||||
</a>
|
||||
<a href="#">{data.author.name}</a>
|
||||
</div>
|
||||
)}
|
||||
<div className={!!data?.author?.name ? "float-end" : "float-start"}>
|
||||
<a href="#">{data.date}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -10,7 +10,7 @@ export default function BlogWidget({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="col-sm-6 col-lg-3 mt-60">
|
||||
<div className="col-sm-6 col-lg-4 mt-60">
|
||||
{/* Widget */}
|
||||
<div className="widget mb-0">
|
||||
<h3 className="widget-title">Categories</h3>
|
||||
@ -29,7 +29,7 @@ export default function BlogWidget({
|
||||
</div>
|
||||
{/* End Widget */}
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3 mt-60">
|
||||
<div className="col-sm-6 col-lg-4 mt-60">
|
||||
{/* Widget */}
|
||||
<div className="widget mb-0">
|
||||
<h3 className="widget-title">Tags</h3>
|
||||
@ -45,7 +45,7 @@ export default function BlogWidget({
|
||||
</div>
|
||||
{/* End Widget */}
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3 mt-60">
|
||||
<div className="col-sm-6 col-lg-4 mt-60">
|
||||
{/* Widget */}
|
||||
<div className="widget mb-0">
|
||||
<h3 className="widget-title">Archive</h3>
|
||||
@ -63,52 +63,6 @@ export default function BlogWidget({
|
||||
</div>
|
||||
{/* End Widget */}
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6 col-lg-3 mt-60">
|
||||
<div className="widget mb-0">
|
||||
<h3 className="widget-title">Newsletter</h3>
|
||||
|
||||
<div className="widget-body">
|
||||
<div className="widget-text clearfix">
|
||||
<form
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
className="form"
|
||||
id="mailchimp"
|
||||
>
|
||||
<div className="mb-20">Stay informed with our newsletter.</div>
|
||||
|
||||
<div className="mb-20">
|
||||
<input
|
||||
placeholder="Enter your email"
|
||||
className={inputClass}
|
||||
type="email"
|
||||
pattern=".{5,100}"
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
<button type="submit" className={btnClass}>
|
||||
<span>Subscribe</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="form-tip">
|
||||
<i className="icon-info size-16" aria-hidden="true"></i>
|
||||
By sending the form you agree to the
|
||||
<a href="#">Terms & Conditions</a> and
|
||||
<a href="#">Privacy Policy</a>.
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="subscribe-result"
|
||||
role="region"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
56
src/components/Blogs/Blogs.tsx
Normal file
56
src/components/Blogs/Blogs.tsx
Normal file
@ -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 (
|
||||
<div className="container position-relative">
|
||||
{/* Blog Posts Grid */}
|
||||
<div className="row mt-n50 mb-50">
|
||||
{/* Post Item */}
|
||||
{data.formattedData.map((blog, i) => (
|
||||
<BlogCardItem
|
||||
key={i}
|
||||
data={{
|
||||
slug: blog.slug,
|
||||
title: blog.title,
|
||||
contentPreview: sanitizeBlogContentIntoStringPreview(
|
||||
blog.content
|
||||
),
|
||||
date: blog.createdAtFormatted,
|
||||
img: blog.imgFormatted,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{/* End Post Item */}
|
||||
</div>
|
||||
{/* End Blog Posts Grid */}
|
||||
{/* Pagination */}
|
||||
{data.totalPages > 1 && (
|
||||
<Pagination
|
||||
page={data.page ?? 1}
|
||||
hasNextPage={data.hasNextPage}
|
||||
hasPreviousPage={data.hasPrevPage}
|
||||
totalPages={data.totalPages}
|
||||
onClickPage={handleClickPage}
|
||||
/>
|
||||
)}
|
||||
{/* End Pagination */}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div className="main-nav-sub full-wrapper">
|
||||
{/* Logo (* Add your text or image to the link tag. Use SVG or PNG image format.
|
||||
|
@ -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";
|
||||
|
@ -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 (
|
||||
<div
|
||||
className={className ? className : "pagination justify-content-center"}
|
||||
>
|
||||
<div className={"pagination justify-content-center"}>
|
||||
{/* Previous Page Button */}
|
||||
<a
|
||||
onClick={() => activePage > 1 && handlePageChange(activePage - 1)}
|
||||
className={activePage === 1 ? "disabled" : ""}
|
||||
>
|
||||
<i className="mi-chevron-left" />
|
||||
<span className="visually-hidden">Previous page</span>
|
||||
</a>
|
||||
|
||||
{/* Page Number 1 */}
|
||||
<a
|
||||
onClick={() => handlePageChange(1)}
|
||||
className={activePage === 1 ? "active" : ""}
|
||||
>
|
||||
1
|
||||
</a>
|
||||
|
||||
{/* Page Number 2 */}
|
||||
<a
|
||||
onClick={() => handlePageChange(2)}
|
||||
className={activePage === 2 ? "active" : ""}
|
||||
>
|
||||
2
|
||||
</a>
|
||||
|
||||
{/* Page Number 3 */}
|
||||
<a
|
||||
onClick={() => handlePageChange(3)}
|
||||
className={activePage === 3 ? "active" : ""}
|
||||
>
|
||||
3
|
||||
</a>
|
||||
|
||||
{activePage > 4 && activePage < 8 && (
|
||||
<span className="no-active">...</span>
|
||||
{hasPreviousPage && (
|
||||
<a
|
||||
onClick={() => activePage > 1 && handlePageChange(activePage - 1)}
|
||||
className={activePage === 1 ? "disabled" : ""}
|
||||
>
|
||||
<i className="mi-chevron-left" />
|
||||
<span className="visually-hidden">Previous page</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{activePage > 3 && activePage < 8 && (
|
||||
<a className={"active"}>{activePage}</a>
|
||||
)}
|
||||
|
||||
{/* Ellipsis */}
|
||||
<span className="no-active">...</span>
|
||||
{activePage == 8 && <a className={"active"}>{8}</a>}
|
||||
{/* Page Number 9 */}
|
||||
<a
|
||||
onClick={() => handlePageChange(9)}
|
||||
className={activePage === 9 ? "active" : ""}
|
||||
>
|
||||
9
|
||||
</a>
|
||||
{getPageNumbers().map((page, key) => (
|
||||
<a
|
||||
key={key}
|
||||
onClick={() => handlePageChange(page)}
|
||||
className={activePage === page ? "active" : ""}
|
||||
>
|
||||
{page}
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Next Page Button */}
|
||||
<a
|
||||
onClick={() => activePage < 9 && handlePageChange(activePage + 1)}
|
||||
className={activePage === 9 ? "disabled" : ""}
|
||||
>
|
||||
<i className="mi-chevron-right" />
|
||||
<span className="visually-hidden">Next page</span>
|
||||
</a>
|
||||
{hasNextPage && (
|
||||
<a
|
||||
onClick={() =>
|
||||
activePage < totalPages && handlePageChange(activePage + 1)
|
||||
}
|
||||
className={activePage === totalPages ? "disabled" : ""}
|
||||
>
|
||||
<i className="mi-chevron-right" />
|
||||
<span className="visually-hidden">Next page</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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 = [
|
||||
|
52
src/services/payload/blog.ts
Normal file
52
src/services/payload/blog.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
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}...`;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user