aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-03-09 00:38:02 +0100
committerGitHub <noreply@github.com>2022-03-09 00:38:02 +0100
commit5b6639a3cf9b6c63045cb82e6ef1a43b0742c367 (patch)
tree4e7cebf9f6b094d405e96febe743fea514cfca9f /src
parentb0d9d8cb1c8c4a4d2b9234bbfdc7195fb563b21a (diff)
feat: provide pagination for users with js disabled (#13)
* chore: add a Pagination component * chore: add blog pages * chore: fallback to page number based navigation if JS disabled * chore: update translation
Diffstat (limited to 'src')
-rw-r--r--src/components/Pagination/Pagination.module.scss92
-rw-r--r--src/components/Pagination/Pagination.tsx131
-rw-r--r--src/i18n/en.json24
-rw-r--r--src/i18n/fr.json24
-rw-r--r--src/pages/blog/index.tsx59
-rw-r--r--src/pages/blog/page/[id].tsx195
-rw-r--r--src/services/graphql/queries.ts29
-rw-r--r--src/ts/types/app.ts9
-rw-r--r--src/ts/types/blog.ts10
-rw-r--r--src/utils/helpers/format.ts17
10 files changed, 550 insertions, 40 deletions
diff --git a/src/components/Pagination/Pagination.module.scss b/src/components/Pagination/Pagination.module.scss
new file mode 100644
index 0000000..4d74d1b
--- /dev/null
+++ b/src/components/Pagination/Pagination.module.scss
@@ -0,0 +1,92 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.list {
+ @extend %flex-list;
+ justify-content: center;
+
+ row-gap: var(--spacing-sm);
+}
+
+.link {
+ display: block;
+ padding: var(--spacing-xs) var(--spacing-sm);
+ background: var(--color-bg);
+ border: fun.convert-px(2) solid var(--color-primary);
+ box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0
+ var(--color-primary-darker);
+ font-weight: 600;
+ text-decoration: none;
+
+ @include mix.pointer("fine") {
+ padding: var(--spacing-2xs) var(--spacing-xs);
+ }
+
+ &--current {
+ padding: calc(var(--spacing-xs) / 1.5) var(--spacing-sm);
+ border-color: var(--color-primary-darker);
+ box-shadow: none;
+ color: var(--color-primary-darker);
+ transform: translateY(#{fun.convert-px(10)});
+
+ @include mix.pointer("fine") {
+ padding: calc(var(--spacing-2xs) / 1.5) var(--spacing-xs);
+ transform: translateY(#{fun.convert-px(7)});
+ }
+ }
+
+ &:not(.link--current) {
+ &:hover,
+ &:focus {
+ border-color: var(--color-primary-light);
+ box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0
+ var(--color-primary-darker),
+ 0 fun.convert-px(2) fun.convert-px(2) fun.convert-px(1)
+ var(--color-shadow-dark),
+ 0 fun.convert-px(7) fun.convert-px(7) fun.convert-px(2)
+ var(--color-shadow-light);
+ color: var(--color-primary-light);
+ transform: translateY(#{fun.convert-px(-5)});
+ }
+
+ &:active {
+ padding: calc(var(--spacing-xs) / 1.5) var(--spacing-sm);
+ border-color: var(--color-primary-dark);
+ box-shadow: none;
+ color: var(--color-primary-dark);
+ transform: translateY(#{fun.convert-px(10)});
+
+ @include mix.pointer("fine") {
+ padding: calc(var(--spacing-2xs) / 1.5) var(--spacing-xs);
+ transform: translateY(#{fun.convert-px(7)});
+ }
+ }
+ }
+}
+
+.item {
+ position: relative;
+
+ &:first-child {
+ .link {
+ border-top-left-radius: fun.convert-px(4);
+ border-bottom-left-radius: fun.convert-px(4);
+ }
+ }
+
+ &:last-child {
+ .link {
+ border-top-right-radius: fun.convert-px(4);
+ border-bottom-right-radius: fun.convert-px(4);
+ }
+ }
+
+ &:not(:first-child) {
+ margin-left: fun.convert-px(-1);
+ }
+
+ &:not(:last-child) {
+ margin-right: fun.convert-px(-1);
+ }
+}
diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx
new file mode 100644
index 0000000..2c24a8c
--- /dev/null
+++ b/src/components/Pagination/Pagination.tsx
@@ -0,0 +1,131 @@
+import { settings } from '@utils/config';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useIntl } from 'react-intl';
+import styles from './Pagination.module.scss';
+
+const Pagination = ({ baseUrl, total }: { baseUrl: string; total: number }) => {
+ const intl = useIntl();
+ const { asPath } = useRouter();
+ const totalPages = Math.floor(total / settings.postsPerPage);
+ const currentPage = asPath.includes('/page/')
+ ? Number(asPath.split(`${baseUrl}/page/`)[1])
+ : 1;
+ const hasPreviousPage = currentPage !== 1;
+ const hasNextPage = currentPage !== totalPages;
+
+ const getPreviousPageItem = () => {
+ return (
+ <li className={styles.item}>
+ <Link href={`${baseUrl}/page/${currentPage - 1}`}>
+ <a className={styles.link}>
+ {intl.formatMessage(
+ {
+ defaultMessage: '{icon} Previous page',
+ description: 'Pagination: previous page link',
+ },
+ { icon: '←' }
+ )}
+ </a>
+ </Link>
+ </li>
+ );
+ };
+
+ const getNextPageItem = () => {
+ return (
+ <li className={styles.item}>
+ <Link href={`${baseUrl}/page/${currentPage + 1}`}>
+ <a className={styles.link}>
+ {intl.formatMessage(
+ {
+ defaultMessage: 'Next page {icon}',
+ description: 'Pagination: Next page link',
+ },
+ { icon: '→' }
+ )}
+ </a>
+ </Link>
+ </li>
+ );
+ };
+
+ const getPages = () => {
+ const pages = [];
+ for (let i = 1; i <= totalPages; i++) {
+ if (i === currentPage) {
+ pages.push({
+ id: `page-${i}`,
+ link: (
+ <span className={`${styles.link} ${styles['link--current']}`}>
+ {intl.formatMessage(
+ {
+ defaultMessage: '<a11y>Page </a11y>{number}',
+ description: 'Pagination: page number',
+ },
+ {
+ number: i,
+ a11y: (chunks: string) => (
+ <span className="screen-reader-text">{chunks}</span>
+ ),
+ }
+ )}
+ </span>
+ ),
+ });
+ } else {
+ pages.push({
+ id: `page-${i}`,
+ link: (
+ <Link href={`${baseUrl}/page/${i}`}>
+ <a className={styles.link}>
+ {intl.formatMessage(
+ {
+ defaultMessage: '<a11y>Page </a11y>{number}',
+ description: 'Pagination: page number',
+ },
+ {
+ number: i,
+ a11y: (chunks: string) => (
+ <span className="screen-reader-text">{chunks}</span>
+ ),
+ }
+ )}
+ </a>
+ </Link>
+ ),
+ });
+ }
+ }
+
+ return pages;
+ };
+
+ const getItems = () => {
+ const pages = getPages();
+
+ return pages.map((page) => (
+ <li key={page.id} className={styles.item}>
+ {page.link}
+ </li>
+ ));
+ };
+
+ return (
+ <nav className={styles.wrapper} aria-labelledby="pagination-title">
+ <h2 id="pagination-title" className="screen-reader-text">
+ {intl.formatMessage({
+ defaultMessage: 'Pagination',
+ description: 'Pagination: pagination title',
+ })}
+ </h2>
+ <ul className={styles.list}>
+ {hasPreviousPage && getPreviousPageItem()}
+ {getItems()}
+ {hasNextPage && getNextPageItem()}
+ </ul>
+ </nav>
+ );
+};
+
+export default Pagination;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4928516..f6e48ae 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -107,6 +107,10 @@
"defaultMessage": "Please fill the form to contact me.",
"description": "ContactPage: page introduction"
},
+ "8w+jnD": {
+ "defaultMessage": "Blog - Page {number} - {websiteName}",
+ "description": "BlogPage: SEO - Page title"
+ },
"9kx83j": {
"defaultMessage": "Close help",
"description": "Tooltip: button title"
@@ -139,6 +143,10 @@
"defaultMessage": "Others formats",
"description": "CVPage: cv preview widget title"
},
+ "BAkq7J": {
+ "defaultMessage": "Pagination",
+ "description": "Pagination: pagination title"
+ },
"C+r/LF": {
"defaultMessage": "Updated on:",
"description": "Dates: update date meta label"
@@ -223,10 +231,6 @@
"defaultMessage": "Comment",
"description": "CommentForm: Comment field label"
},
- "JPh168": {
- "defaultMessage": "Javascript is required to load more posts.",
- "description": "BlogPage: noscript tag"
- },
"JeYOeA": {
"defaultMessage": "Sidebar",
"description": "ArticlePage: right sidebar aria-label"
@@ -331,6 +335,10 @@
"defaultMessage": "Blog",
"description": "BlogPage: breadcrumb item"
},
+ "R4yaW6": {
+ "defaultMessage": "Next page {icon}",
+ "description": "Pagination: Next page link"
+ },
"RZzx/4": {
"defaultMessage": "Javascript is required to use the table of contents.",
"description": "ToC: noscript tag"
@@ -355,6 +363,10 @@
"defaultMessage": "Subscribe",
"description": "HomePage: RSS feed subscription text"
},
+ "TSXPzr": {
+ "defaultMessage": "<a11y>Page </a11y>{number}",
+ "description": "Pagination: page number"
+ },
"TfU6Qm": {
"defaultMessage": "Search",
"description": "SearchPage: breadcrumb item"
@@ -455,6 +467,10 @@
"defaultMessage": "{starsCount, plural, =0 {0 stars on Github} one {# star on Github} other {# stars on Github}}",
"description": "ProjectSummary: technologies list label"
},
+ "aMFqPH": {
+ "defaultMessage": "{icon} Previous page",
+ "description": "Pagination: previous page link"
+ },
"akSutM": {
"defaultMessage": "Projects",
"description": "MainNav: projects link"
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 3411667..6f8ce41 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -107,6 +107,10 @@
"defaultMessage": "Veuillez remplir le formulaire pour me contacter.",
"description": "ContactPage: page introduction"
},
+ "8w+jnD": {
+ "defaultMessage": "Blog - Page {number} - {websiteName}",
+ "description": "BlogPage: SEO - Page title"
+ },
"9kx83j": {
"defaultMessage": "Fermer l'aide",
"description": "Tooltip: button title"
@@ -139,6 +143,10 @@
"defaultMessage": "Autres formats",
"description": "CVPage: cv preview widget title"
},
+ "BAkq7J": {
+ "defaultMessage": "Pagination",
+ "description": "Pagination: pagination title"
+ },
"C+r/LF": {
"defaultMessage": "Mis à jour le :",
"description": "Dates: update date meta label"
@@ -223,10 +231,6 @@
"defaultMessage": "Commentaire",
"description": "CommentForm: Comment field label"
},
- "JPh168": {
- "defaultMessage": "Javascript est nécessaire pour charger plus d'articles.",
- "description": "BlogPage: noscript tag"
- },
"JeYOeA": {
"defaultMessage": "Barre latérale",
"description": "ArticlePage: right sidebar aria-label"
@@ -331,6 +335,10 @@
"defaultMessage": "Blog",
"description": "BlogPage: breadcrumb item"
},
+ "R4yaW6": {
+ "defaultMessage": "Page suivante {icon}",
+ "description": "Pagination: Next page link"
+ },
"RZzx/4": {
"defaultMessage": "Javascript est nécessaire pour utiliser la table des matières.",
"description": "ToC: noscript tag"
@@ -355,6 +363,10 @@
"defaultMessage": "Vous abonner",
"description": "HomePage: RSS feed subscription text"
},
+ "TSXPzr": {
+ "defaultMessage": "<a11y>Page </a11y>{number}",
+ "description": "Pagination: page number"
+ },
"TfU6Qm": {
"defaultMessage": "Recherche",
"description": "SearchPage: breadcrumb item"
@@ -455,6 +467,10 @@
"defaultMessage": "{starsCount, plural, =0 {0 étoile sur Github} one {# étoile sur Github} other {# étoiles sur Github}}",
"description": "ProjectSummary: technologies list label"
},
+ "aMFqPH": {
+ "defaultMessage": "{icon} Page précédente",
+ "description": "Pagination: previous page link"
+ },
"akSutM": {
"defaultMessage": "Projets",
"description": "MainNav: projects link"
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index 543fad9..366fc28 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -1,5 +1,6 @@
import { Button } from '@components/Buttons';
import { getLayout } from '@components/Layouts/Layout';
+import Pagination from '@components/Pagination/Pagination';
import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
import PostHeader from '@components/PostHeader/PostHeader';
import PostsList from '@components/PostsList/PostsList';
@@ -29,12 +30,17 @@ import useSWRInfinite from 'swr/infinite';
const Blog: NextPageWithLayout<BlogPageProps> = ({
allThematics,
allTopics,
- firstPosts,
+ posts,
totalPosts,
}) => {
const intl = useIntl();
const lastPostRef = useRef<HTMLSpanElement>(null);
const router = useRouter();
+ const [isMounted, setIsMounted] = useState<boolean>(false);
+
+ useEffect(() => {
+ if (typeof window !== undefined) setIsMounted(true);
+ }, []);
const getKey = (pageIndex: number, previousData: PostsListData) => {
if (previousData && !previousData.posts) return null;
@@ -50,7 +56,7 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({
const { data, error, size, setSize } = useSWRInfinite(
getKey,
getPublishedPosts,
- { fallbackData: [firstPosts] }
+ { fallbackData: [posts] }
);
const [totalPostsCount, setTotalPostsCount] = useState<number>(totalPosts);
@@ -171,31 +177,28 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({
<PostHeader title={title} meta={{ results: totalPostsCount }} />
<div className={styles.body}>
{getPostsList()}
- {hasNextPage && (
- <>
- <PaginationCursor
- current={loadedPostsCount}
- total={totalPostsCount}
- />
- <Button
- isDisabled={isLoadingMore}
- clickHandler={loadMorePosts}
- position="center"
- spacing={true}
- >
- {intl.formatMessage({
- defaultMessage: 'Load more?',
- description: 'BlogPage: load more text',
- })}
- </Button>
- <noscript>
- {intl.formatMessage({
- defaultMessage: 'Javascript is required to load more posts.',
- description: 'BlogPage: noscript tag',
- })}
- </noscript>
- </>
- )}
+ {hasNextPage &&
+ (isMounted ? (
+ <>
+ <PaginationCursor
+ current={loadedPostsCount}
+ total={totalPostsCount}
+ />
+ <Button
+ isDisabled={isLoadingMore}
+ clickHandler={loadMorePosts}
+ position="center"
+ spacing={true}
+ >
+ {intl.formatMessage({
+ defaultMessage: 'Load more?',
+ description: 'BlogPage: load more text',
+ })}
+ </Button>
+ </>
+ ) : (
+ <Pagination baseUrl="/blog" total={totalPostsCount} />
+ ))}
</div>
<Sidebar
position="right"
@@ -246,8 +249,8 @@ export const getStaticProps: GetStaticProps = async (
allThematics,
allTopics,
breadcrumbTitle,
- firstPosts,
locale,
+ posts: firstPosts,
totalPosts,
translation,
},
diff --git a/src/pages/blog/page/[id].tsx b/src/pages/blog/page/[id].tsx
new file mode 100644
index 0000000..3be058b
--- /dev/null
+++ b/src/pages/blog/page/[id].tsx
@@ -0,0 +1,195 @@
+import { getLayout } from '@components/Layouts/Layout';
+import Pagination from '@components/Pagination/Pagination';
+import PostHeader from '@components/PostHeader/PostHeader';
+import PostsList from '@components/PostsList/PostsList';
+import Sidebar from '@components/Sidebar/Sidebar';
+import { ThematicsList, TopicsList } from '@components/Widgets';
+import {
+ getAllThematics,
+ getAllTopics,
+ getEndCursor,
+ getPostsTotal,
+ getPublishedPosts,
+} from '@services/graphql/queries';
+import { NextPageWithLayout } from '@ts/types/app';
+import { BlogPageProps } from '@ts/types/blog';
+import { settings } from '@utils/config';
+import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
+import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { useIntl } from 'react-intl';
+import { Blog, Graph, WebPage } from 'schema-dts';
+import styles from '@styles/pages/Page.module.scss';
+import { getFormattedPageNumbers } from '@utils/helpers/format';
+import { useEffect } from 'react';
+
+const BlogPage: NextPageWithLayout<BlogPageProps> = ({
+ allThematics,
+ allTopics,
+ posts,
+ totalPosts,
+}) => {
+ const intl = useIntl();
+ const router = useRouter();
+ const pageNumber = Number(router.query.id);
+
+ useEffect(() => {
+ if (router.query.id === '1') router.push('/blog');
+ }, [router]);
+
+ const pageTitle = intl.formatMessage(
+ {
+ defaultMessage: `Blog - Page {number} - {websiteName}`,
+ description: 'BlogPage: SEO - Page title',
+ },
+ { number: pageNumber, websiteName: settings.name }
+ );
+ const pageDescription = intl.formatMessage(
+ {
+ defaultMessage:
+ "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.",
+ description: 'BlogPage: SEO - Meta description',
+ },
+ { websiteName: settings.name }
+ );
+ const pageUrl = `${settings.url}${router.asPath}`;
+
+ const webpageSchema: WebPage = {
+ '@id': `${pageUrl}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
+ name: pageTitle,
+ description: pageDescription,
+ inLanguage: settings.locales.defaultLocale,
+ reviewedBy: { '@id': `${settings.url}/#branding` },
+ url: `${settings.url}`,
+ isPartOf: {
+ '@id': `${settings.url}`,
+ },
+ };
+
+ const blogSchema: Blog = {
+ '@id': `${settings.url}/#blog`,
+ '@type': 'Blog',
+ author: { '@id': `${settings.url}/#branding` },
+ creator: { '@id': `${settings.url}/#branding` },
+ editor: { '@id': `${settings.url}/#branding` },
+ inLanguage: settings.locales.defaultLocale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${pageUrl}` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, blogSchema],
+ };
+
+ const title = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'BlogPage: page title',
+ });
+
+ return (
+ <>
+ <Head>
+ <title>{pageTitle}</title>
+ <meta name="description" content={pageDescription} />
+ <meta property="og:url" content={`${pageUrl}`} />
+ <meta property="og:type" content="website" />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={pageDescription} />
+ </Head>
+ <Script
+ id="schema-blog"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <article
+ id="blog"
+ className={`${styles.article} ${styles['article--no-comments']}`}
+ >
+ <PostHeader title={title} meta={{ results: totalPosts }} />
+ <div className={styles.body}>
+ <PostsList data={[posts]} showYears={true} />
+ <Pagination baseUrl="/blog" total={totalPosts} />
+ </div>
+ <Sidebar
+ position="right"
+ title={intl.formatMessage({
+ defaultMessage: 'Filter by:',
+ description: 'BlogPage: sidebar title',
+ })}
+ >
+ <ThematicsList
+ initialData={allThematics}
+ title={intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'BlogPage: thematics list widget title',
+ })}
+ />
+ <TopicsList
+ initialData={allTopics}
+ title={intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'BlogPage: topics list widget title',
+ })}
+ />
+ </Sidebar>
+ </article>
+ </>
+ );
+};
+
+BlogPage.getLayout = getLayout;
+
+export const getStaticProps: GetStaticProps = async (
+ context: GetStaticPropsContext
+) => {
+ const intl = await getIntlInstance();
+ const breadcrumbTitle = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'BlogPage: breadcrumb item',
+ });
+ const { locale, params } = context;
+ const queriedPageNumber = params ? Number(params.id) : 1;
+ const queriedPostsNumber = settings.postsPerPage * queriedPageNumber;
+ const endCursor =
+ queriedPostsNumber === 1
+ ? undefined
+ : await getEndCursor({ first: queriedPostsNumber });
+ const posts = await getPublishedPosts({
+ first: settings.postsPerPage,
+ after: endCursor,
+ });
+ const totalPosts = await getPostsTotal();
+ const allThematics = await getAllThematics();
+ const allTopics = await getAllTopics();
+ const translation = await loadTranslation(locale);
+
+ return {
+ props: {
+ allThematics,
+ allTopics,
+ breadcrumbTitle,
+ locale,
+ posts,
+ totalPosts,
+ translation,
+ },
+ };
+};
+
+export default BlogPage;
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const totalPosts = await getPostsTotal();
+ const totalPages = Math.floor(totalPosts / settings.postsPerPage);
+ const paths = getFormattedPageNumbers(totalPages);
+
+ return {
+ paths,
+ fallback: true,
+ };
+};
diff --git a/src/services/graphql/queries.ts b/src/services/graphql/queries.ts
index e56590f..8dd8563 100644
--- a/src/services/graphql/queries.ts
+++ b/src/services/graphql/queries.ts
@@ -1,6 +1,11 @@
import { Slug } from '@ts/types/app';
import { Article, PostBy, TotalArticles } from '@ts/types/articles';
-import { AllPostsSlug, PostsList, RawPostsList } from '@ts/types/blog';
+import {
+ AllPostsSlug,
+ LastPostCursor,
+ PostsList,
+ RawPostsList,
+} from '@ts/types/blog';
import { Comment, CommentsByPostId } from '@ts/types/comments';
import {
AllTopics,
@@ -510,3 +515,25 @@ export const getAllThematics = async (): Promise<ThematicPreview[]> => {
const response = await fetchApi<AllThematics>(query, null);
return response.thematics.nodes;
};
+
+export const getEndCursor = async ({
+ first = 10,
+}: {
+ first: number;
+}): Promise<string> => {
+ const query = gql`
+ query EndCursorAfter($first: Int) {
+ posts(first: $first) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+ }
+ `;
+
+ const variables = { first };
+ const response = await fetchApi<LastPostCursor>(query, variables);
+
+ return response.posts.pageInfo.endCursor;
+};
diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts
index 444733c..4243762 100644
--- a/src/ts/types/app.ts
+++ b/src/ts/types/app.ts
@@ -3,7 +3,7 @@ import { AppProps } from 'next/app';
import { ImageProps } from 'next/image';
import { ReactElement, ReactNode } from 'react';
import { PostBy, TotalArticles } from './articles';
-import { AllPostsSlug, RawPostsList } from './blog';
+import { AllPostsSlug, LastPostCursor, RawPostsList } from './blog';
import { CommentData, CommentsByPostId, CreateComment } from './comments';
import { ContactData, SendEmail } from './contact';
import {
@@ -39,6 +39,8 @@ export type VariablesType<T> = T extends PostBy | TopicBy | ThematicBy
? { id: number }
: T extends CreateComment
? CommentData
+ : T extends LastPostCursor
+ ? { first: number }
: T extends SendEmail
? ContactData
: null;
@@ -51,6 +53,7 @@ export type RequestType =
| AllThematicsSlug
| CommentsByPostId
| CreateComment
+ | LastPostCursor
| PostBy
| RawPostsList
| SendEmail
@@ -109,6 +112,10 @@ export type PageInfo = {
total: number;
};
+export type ParamsIds = {
+ params: { id: string };
+};
+
export type ParamsSlug = {
params: { slug: string };
};
diff --git a/src/ts/types/blog.ts b/src/ts/types/blog.ts
index 8b48264..05bdd1f 100644
--- a/src/ts/types/blog.ts
+++ b/src/ts/types/blog.ts
@@ -19,6 +19,14 @@ export type RawPostsList = {
};
};
+export type LastPostCursor = {
+ posts: {
+ pageInfo: {
+ endCursor: string;
+ };
+ };
+};
+
export type AllPostsSlug = {
posts: {
nodes: Slug[];
@@ -28,6 +36,6 @@ export type AllPostsSlug = {
export type BlogPageProps = {
allThematics: ThematicPreview[];
allTopics: TopicPreview[];
- firstPosts: PostsList;
+ posts: PostsList;
totalPosts: number;
};
diff --git a/src/utils/helpers/format.ts b/src/utils/helpers/format.ts
index 9c6f266..71455b6 100644
--- a/src/utils/helpers/format.ts
+++ b/src/utils/helpers/format.ts
@@ -1,4 +1,4 @@
-import { ParamsSlug, Slug } from '@ts/types/app';
+import { ParamsIds, ParamsSlug, Slug } from '@ts/types/app';
import {
Article,
ArticlePreview,
@@ -293,3 +293,18 @@ export const getFormattedPaths = (array: Slug[]): ParamsSlug[] => {
return { params: { slug: object.slug } };
});
};
+
+/**
+ * Convert a number of pages to an array of params with ids.
+ * @param {number} totalPages - The total pages.
+ * @returns {ParamsIds} An array of params with ids.
+ */
+export const getFormattedPageNumbers = (totalPages: number): ParamsIds[] => {
+ const paths = [];
+
+ for (let i = 1; i <= totalPages; i++) {
+ paths.push({ params: { id: `${i}` } });
+ }
+
+ return paths;
+};