feat: add pagination in tag posts (#201)

* feat: add pagination in tag posts

Implement pagination in tag posts. Refactor and extract pagination logic and pagination component.

Closes: #152

* fix: update breadcrumbs for updated tag pagination
This commit is contained in:
Sat Naing
2023-12-29 10:26:02 +06:30
committed by GitHub
parent b05b8fb842
commit 581826a5af
11 changed files with 262 additions and 140 deletions
+12
View File
@@ -10,6 +10,18 @@ const breadcrumbList = currentUrlPath.split("/").slice(1);
// replace Posts with Posts (page number) // replace Posts with Posts (page number)
breadcrumbList[0] === "posts" && breadcrumbList[0] === "posts" &&
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`); breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
// if breadcrumb is Home > Tags > [tag] > [page] <etc>
// replace [tag] > [page] with [tag] (page number)
breadcrumbList[0] === "tags" &&
!isNaN(Number(breadcrumbList[2])) &&
breadcrumbList.splice(
1,
3,
`${breadcrumbList[1]} ${
Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"
}`
);
--- ---
<nav class="breadcrumb" aria-label="breadcrumb"> <nav class="breadcrumb" aria-label="breadcrumb">
+57
View File
@@ -0,0 +1,57 @@
---
import LinkButton from "./LinkButton.astro";
export interface Props {
currentPage: number;
totalPages: number;
prevUrl: string;
nextUrl: string;
}
const { currentPage, totalPages, prevUrl, nextUrl } = Astro.props;
const prev = currentPage > 1 ? "" : "disabled";
const next = currentPage < totalPages ? "" : "disabled";
---
{
totalPages > 1 && (
<nav class="pagination-wrapper" aria-label="Pagination">
<LinkButton
disabled={prev === "disabled"}
href={prevUrl}
className={`mr-4 select-none ${prev}`}
ariaLabel="Previous"
>
<svg xmlns="http://www.w3.org/2000/svg" class={`${prev}-svg`}>
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
</svg>
Prev
</LinkButton>
{currentPage} / {totalPages}
<LinkButton
disabled={next === "disabled"}
href={nextUrl}
className={`ml-4 select-none ${next}`}
ariaLabel="Next"
>
Next
<svg xmlns="http://www.w3.org/2000/svg" class={`${next}-svg`}>
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
</svg>
</LinkButton>
</nav>
)
}
<style>
.pagination-wrapper {
@apply mb-8 mt-auto flex justify-center;
}
.disabled {
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
}
.disabled-svg {
@apply group-hover:!fill-skin-base;
}
</style>
+14 -50
View File
@@ -1,23 +1,20 @@
--- ---
import { SITE } from "@config"; import type { CollectionEntry } from "astro:content";
import Layout from "@layouts/Layout.astro"; import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro"; import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro"; import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro"; import Footer from "@components/Footer.astro";
import Pagination from "@components/Pagination.astro";
import Card from "@components/Card"; import Card from "@components/Card";
import LinkButton from "@components/LinkButton.astro"; import { SITE } from "@config";
import type { CollectionEntry } from "astro:content";
export interface Props { export interface Props {
pageNum: number; currentPage: number;
totalPages: number; totalPages: number;
posts: CollectionEntry<"blog">[]; paginatedPosts: CollectionEntry<"blog">[];
} }
const { pageNum, totalPages, posts } = Astro.props; const { currentPage, totalPages, paginatedPosts } = Astro.props;
const prev = pageNum > 1 ? "" : "disabled";
const next = pageNum < totalPages ? "" : "disabled";
--- ---
<Layout title={`Posts | ${SITE.title}`}> <Layout title={`Posts | ${SITE.title}`}>
@@ -25,52 +22,19 @@ const next = pageNum < totalPages ? "" : "disabled";
<Main pageTitle="Posts" pageDesc="All the articles I've posted."> <Main pageTitle="Posts" pageDesc="All the articles I've posted.">
<ul> <ul>
{ {
posts.map(({ data, slug }) => ( paginatedPosts.map(({ data, slug }) => (
<Card href={`/posts/${slug}`} frontmatter={data} /> <Card href={`/posts/${slug}`} frontmatter={data} />
)) ))
} }
</ul> </ul>
</Main> </Main>
{ <Pagination
totalPages > 1 && ( {currentPage}
<nav class="pagination-wrapper" aria-label="Pagination"> {totalPages}
<LinkButton prevUrl={`/posts${currentPage - 1 !== 1 ? "/" + (currentPage - 1) : ""}`}
disabled={prev === "disabled"} nextUrl={`/posts/${currentPage + 1}`}
href={`/posts${pageNum - 1 !== 1 ? "/" + (pageNum - 1) : ""}`} />
className={`mr-4 select-none ${prev}`}
ariaLabel="Previous"
>
<svg xmlns="http://www.w3.org/2000/svg" class={`${prev}-svg`}>
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
</svg>
Prev
</LinkButton>
<LinkButton
disabled={next === "disabled"}
href={`/posts/${pageNum + 1}`}
className={`ml-4 select-none ${next}`}
ariaLabel="Next"
>
Next
<svg xmlns="http://www.w3.org/2000/svg" class={`${next}-svg`}>
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
</svg>
</LinkButton>
</nav>
)
}
<Footer noMarginTop={totalPages > 1} /> <Footer noMarginTop={totalPages > 1} />
</Layout> </Layout>
<style>
.pagination-wrapper {
@apply mb-8 mt-auto flex justify-center;
}
.disabled {
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
}
.disabled-svg {
@apply group-hover:!fill-skin-base;
}
</style>
+49
View File
@@ -0,0 +1,49 @@
---
import { type CollectionEntry } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Card from "@components/Card";
import Pagination from "@components/Pagination.astro";
import { SITE } from "@config";
export interface Props {
currentPage: number;
totalPages: number;
paginatedPosts: CollectionEntry<"blog">[];
tag: string;
tagName: string;
}
const { currentPage, totalPages, paginatedPosts, tag, tagName } = Astro.props;
---
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
<Header activeNav="tags" />
<Main
pageTitle={[`Tag:`, `${tagName}`]}
titleTransition={tag}
pageDesc={`All the articles with the tag "${tagName}".`}
>
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
<ul>
{
paginatedPosts.map(({ data, slug }) => (
<Card href={`/posts/${slug}`} frontmatter={data} />
))
}
</ul>
</Main>
<Pagination
{currentPage}
{totalPages}
prevUrl={`/tags/${tag}${
currentPage - 1 !== 1 ? "/" + (currentPage - 1) : ""
}`}
nextUrl={`/tags/${tag}/${currentPage + 1}`}
/>
<Footer noMarginTop={totalPages > 1} />
</Layout>
+6 -22
View File
@@ -4,7 +4,7 @@ import Posts from "@layouts/Posts.astro";
import PostDetails from "@layouts/PostDetails.astro"; import PostDetails from "@layouts/PostDetails.astro";
import getSortedPosts from "@utils/getSortedPosts"; import getSortedPosts from "@utils/getSortedPosts";
import getPageNumbers from "@utils/getPageNumbers"; import getPageNumbers from "@utils/getPageNumbers";
import { SITE } from "@config"; import getPagination from "@utils/getPagination";
export interface Props { export interface Props {
post: CollectionEntry<"blog">; post: CollectionEntry<"blog">;
@@ -32,26 +32,10 @@ const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts); const sortedPosts = getSortedPosts(posts);
const totalPages = getPageNumbers(sortedPosts.length); const pagination = getPagination({
posts: sortedPosts,
const currentPage = page: slug,
slug && !isNaN(Number(slug)) && totalPages.includes(Number(slug)) });
? Number(slug)
: 0;
const lastPost = currentPage * SITE.postPerPage;
const startPost = lastPost - SITE.postPerPage;
const paginatedPosts = sortedPosts.slice(startPost, lastPost);
--- ---
{ {post ? <PostDetails post={post} /> : <Posts {...pagination} />}
post ? (
<PostDetails post={post} />
) : (
<Posts
posts={paginatedPosts}
pageNum={currentPage}
totalPages={totalPages.length}
/>
)
}
+8 -8
View File
@@ -1,18 +1,18 @@
--- ---
import { SITE } from "@config"; import { getCollection } from "astro:content";
import Posts from "@layouts/Posts.astro"; import Posts from "@layouts/Posts.astro";
import getSortedPosts from "@utils/getSortedPosts"; import getSortedPosts from "@utils/getSortedPosts";
import getPageNumbers from "@utils/getPageNumbers"; import getPagination from "@utils/getPagination";
import { getCollection } from "astro:content";
const posts = await getCollection("blog"); const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts); const sortedPosts = getSortedPosts(posts);
const totalPages = getPageNumbers(sortedPosts.length); const pagination = getPagination({
posts: sortedPosts,
const paginatedPosts = sortedPosts.slice(0, SITE.postPerPage); page: 1,
isIndex: true,
});
--- ---
<Posts posts={paginatedPosts} pageNum={1} totalPages={totalPages.length} /> <Posts {...pagination} />
-58
View File
@@ -1,58 +0,0 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Card from "@components/Card";
import getUniqueTags from "@utils/getUniqueTags";
import getPostsByTag from "@utils/getPostsByTag";
import { SITE } from "@config";
import getSortedPosts from "@utils/getSortedPosts";
export interface Props {
post: CollectionEntry<"blog">;
tag: string;
tagName: string;
}
export async function getStaticPaths() {
const posts = await getCollection("blog");
const tags = getUniqueTags(posts);
return tags.map(({ tag, tagName }) => {
return {
params: { tag },
props: { tag, tagName },
};
});
}
const { tag, tagName } = Astro.props;
const posts = await getCollection("blog", ({ data }) => !data.draft);
const tagPosts = getPostsByTag(posts, tag);
const sortTagsPost = getSortedPosts(tagPosts);
---
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
<Header activeNav="tags" />
<Main
pageTitle={[`Tag:`, `${tagName}`]}
titleTransition={tag}
pageDesc={`All the articles with the tag "${tagName}".`}
>
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
<ul>
{
sortTagsPost.map(({ data, slug }) => (
<Card href={`/posts/${slug}`} frontmatter={data} />
))
}
</ul>
</Main>
<Footer />
</Layout>
+44
View File
@@ -0,0 +1,44 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import TagPosts from "@layouts/TagPosts.astro";
import getUniqueTags from "@utils/getUniqueTags";
import getPostsByTag from "@utils/getPostsByTag";
import getPageNumbers from "@utils/getPageNumbers";
import getPagination from "@utils/getPagination";
export interface Props {
post: CollectionEntry<"blog">;
tag: string;
tagName: string;
}
export async function getStaticPaths() {
const posts = await getCollection("blog");
const tags = getUniqueTags(posts);
return tags.flatMap(({ tag, tagName }) => {
const tagPosts = getPostsByTag(posts, tag);
const totalPages = getPageNumbers(tagPosts.length);
return totalPages.map(page => ({
params: { tag, page },
props: { tag, tagName },
}));
});
}
const { page } = Astro.params;
const { tag, tagName } = Astro.props;
const posts = await getCollection("blog", ({ data }) => !data.draft);
const postsByTag = getPostsByTag(posts, tag);
const pagination = getPagination({
posts: postsByTag,
page,
});
---
<TagPosts {...pagination} {tag} {tagName} />
+32
View File
@@ -0,0 +1,32 @@
---
import { getCollection } from "astro:content";
import TagPosts from "@layouts/TagPosts.astro";
import getPostsByTag from "@utils/getPostsByTag";
import getPagination from "@utils/getPagination";
import getUniqueTags from "@utils/getUniqueTags";
export async function getStaticPaths() {
const posts = await getCollection("blog");
const tags = getUniqueTags(posts);
return tags.map(({ tag, tagName }) => {
return {
params: { tag },
props: { tag, tagName, posts },
};
});
}
const { tag, tagName, posts } = Astro.props;
const postsByTag = getPostsByTag(posts, tag);
const pagination = getPagination({
posts: postsByTag,
page: 1,
isIndex: true,
});
---
<TagPosts {...pagination} {tag} {tagName} />
+35
View File
@@ -0,0 +1,35 @@
import { SITE } from "@config";
import getPageNumbers from "./getPageNumbers";
interface GetPaginationProps<T> {
posts: T;
page: string | number;
isIndex?: boolean;
}
const getPagination = <T>({
posts,
page,
isIndex = false,
}: GetPaginationProps<T[]>) => {
const totalPagesArray = getPageNumbers(posts.length);
const totalPages = totalPagesArray.length;
const currentPage = isIndex
? 1
: page && !isNaN(Number(page)) && totalPagesArray.includes(Number(page))
? Number(page)
: 0;
const lastPost = isIndex ? SITE.postPerPage : currentPage * SITE.postPerPage;
const startPost = isIndex ? 0 : lastPost - SITE.postPerPage;
const paginatedPosts = posts.slice(startPost, lastPost);
return {
totalPages,
currentPage,
paginatedPosts,
};
};
export default getPagination;
+5 -2
View File
@@ -1,7 +1,10 @@
import { slugifyAll } from "./slugify";
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
import getSortedPosts from "./getSortedPosts";
import { slugifyAll } from "./slugify";
const getPostsByTag = (posts: CollectionEntry<"blog">[], tag: string) => const getPostsByTag = (posts: CollectionEntry<"blog">[], tag: string) =>
posts.filter(post => slugifyAll(post.data.tags).includes(tag)); getSortedPosts(
posts.filter(post => slugifyAll(post.data.tags).includes(tag))
);
export default getPostsByTag; export default getPostsByTag;