aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-05-20 15:37:08 +0200
committerArmand Philippot <git@armandphilippot.com>2022-05-20 15:37:08 +0200
commitf4c7ab4e306d2f04324853e67032d370abd65d0c (patch)
tree2c7d1ad467d6c52bc134202f0d33f7524f9056fa
parentbbd63400f94b43fde04449e0c71d14763d893e6a (diff)
chore: handle blog pagination when JS is disabled
-rw-r--r--src/components/molecules/nav/pagination.module.scss2
-rw-r--r--src/components/molecules/nav/pagination.tsx10
-rw-r--r--src/components/organisms/layout/posts-list.tsx95
-rw-r--r--src/pages/blog/index.tsx1
-rw-r--r--src/pages/blog/page/[number].tsx311
-rw-r--r--src/pages/recherche/index.tsx1
-rw-r--r--src/pages/sujet/[slug].tsx1
-rw-r--r--src/pages/thematique/[slug].tsx1
-rw-r--r--src/services/graphql/api.ts9
-rw-r--r--src/services/graphql/articles.query.ts12
-rw-r--r--src/services/graphql/articles.ts28
-rw-r--r--src/utils/hooks/use-breadcrumb.tsx3
-rw-r--r--src/utils/hooks/use-redirection.tsx33
13 files changed, 472 insertions, 35 deletions
diff --git a/src/components/molecules/nav/pagination.module.scss b/src/components/molecules/nav/pagination.module.scss
index a8cef47..56c5bfc 100644
--- a/src/components/molecules/nav/pagination.module.scss
+++ b/src/components/molecules/nav/pagination.module.scss
@@ -13,7 +13,7 @@
&--pages {
column-gap: var(--spacing-2xs);
- margin-top: var(--spacing-sm);
+ margin-bottom: var(--spacing-sm);
}
}
diff --git a/src/components/molecules/nav/pagination.tsx b/src/components/molecules/nav/pagination.tsx
index 38f6841..934b50a 100644
--- a/src/components/molecules/nav/pagination.tsx
+++ b/src/components/molecules/nav/pagination.tsx
@@ -44,12 +44,12 @@ const Pagination: FC<PaginationProps> = ({
className = '',
current,
perPage,
- siblings = 1,
+ siblings = 2,
total,
...props
}) => {
const intl = useIntl();
- const totalPages = Math.ceil(total / perPage);
+ const totalPages = Math.round(total / perPage);
const hasPreviousPage = current > 1;
const previousPageName = intl.formatMessage(
{
@@ -205,14 +205,14 @@ const Pagination: FC<PaginationProps> = ({
return (
<nav className={`${styles.wrapper} ${className}`} {...props}>
+ <ul className={`${styles.list} ${styles['list--pages']}`}>
+ {getPages(current, totalPages)}
+ </ul>
<ul className={styles.list}>
{hasPreviousPage &&
getItem('previous', previousPageName, previousPageUrl)}
{hasNextPage && getItem('next', nextPageName, nextPageUrl)}
</ul>
- <ul className={`${styles.list} ${styles['list--pages']}`}>
- {getPages(current, totalPages)}
- </ul>
</nav>
);
};
diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx
index 9dfe254..50192dd 100644
--- a/src/components/organisms/layout/posts-list.tsx
+++ b/src/components/organisms/layout/posts-list.tsx
@@ -2,6 +2,11 @@ import Button from '@components/atoms/buttons/button';
import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
import ProgressBar from '@components/atoms/loaders/progress-bar';
import Spinner from '@components/atoms/loaders/spinner';
+import Pagination, {
+ PaginationProps,
+} from '@components/molecules/nav/pagination';
+import useIsMounted from '@utils/hooks/use-is-mounted';
+import useSettings from '@utils/hooks/use-settings';
import { FC, Fragment, useRef } from 'react';
import { useIntl } from 'react-intl';
import styles from './posts-list.module.scss';
@@ -18,7 +23,7 @@ export type YearCollection = {
[key: string]: Post[];
};
-export type PostsListProps = {
+export type PostsListProps = Pick<PaginationProps, 'baseUrl' | 'siblings'> & {
/**
* True to display the posts by year. Default: false.
*/
@@ -32,6 +37,10 @@ export type PostsListProps = {
*/
loadMore?: () => void;
/**
+ * The current page number. Default: 1.
+ */
+ pageNumber?: number;
+ /**
* The posts data.
*/
posts: Post[];
@@ -74,16 +83,22 @@ const sortPostsByYear = (data: Post[]): YearCollection => {
* Render a list of post summaries.
*/
const PostsList: FC<PostsListProps> = ({
+ baseUrl,
byYear = false,
isLoading = false,
loadMore,
+ pageNumber = 1,
posts,
showLoadMoreBtn = false,
+ siblings,
titleLevel,
total,
}) => {
const intl = useIntl();
+ const listRef = useRef<HTMLOListElement>(null);
const lastPostRef = useRef<HTMLSpanElement>(null);
+ const isMounted = useIsMounted(listRef);
+ const { blog } = useSettings();
/**
* Retrieve the list of posts.
@@ -99,7 +114,7 @@ const PostsList: FC<PostsListProps> = ({
const lastPostId = allPosts[allPosts.length - 1].id;
return (
- <ol className={styles.list}>
+ <ol className={styles.list} ref={listRef}>
{allPosts.map(({ id, ...post }) => (
<Fragment key={id}>
<li className={styles.item}>
@@ -168,34 +183,60 @@ const PostsList: FC<PostsListProps> = ({
loadMore && loadMore();
};
- return posts.length === 0 ? (
- <p>
- {intl.formatMessage({
- defaultMessage: 'No results found.',
- description: 'PostsList: no results',
- id: 'vK7Sxv',
- })}
- </p>
- ) : (
+ const getProgressBar = () => {
+ return (
+ <>
+ <ProgressBar
+ min={1}
+ max={total}
+ current={posts.length}
+ info={progressInfo}
+ />
+ {showLoadMoreBtn && (
+ <Button
+ kind="tertiary"
+ onClick={loadMorePosts}
+ disabled={isLoading}
+ className={styles.btn}
+ >
+ {loadMoreBody}
+ </Button>
+ )}
+ </>
+ );
+ };
+
+ const getPagination = () => {
+ return posts.length <= blog.postsPerPage ? (
+ <Pagination
+ baseUrl={baseUrl}
+ current={pageNumber}
+ perPage={blog.postsPerPage}
+ siblings={siblings}
+ total={total}
+ />
+ ) : (
+ <></>
+ );
+ };
+
+ if (posts.length === 0) {
+ return (
+ <p>
+ {intl.formatMessage({
+ defaultMessage: 'No results found.',
+ description: 'PostsList: no results',
+ id: 'vK7Sxv',
+ })}
+ </p>
+ );
+ }
+
+ return (
<>
{getPosts()}
{isLoading && <Spinner />}
- <ProgressBar
- min={1}
- max={total}
- current={posts.length}
- info={progressInfo}
- />
- {showLoadMoreBtn && (
- <Button
- kind="tertiary"
- onClick={loadMorePosts}
- disabled={isLoading}
- className={styles.btn}
- >
- {loadMoreBody}
- </Button>
- )}
+ {isMounted ? getProgressBar() : getPagination()}
</>
);
};
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index 2676305..90f56be 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -250,6 +250,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
>
{data && (
<PostsList
+ baseUrl="/blog/page/"
byYear={true}
isLoading={isLoadingMore || isLoadingInitialData}
loadMore={loadMore}
diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx
new file mode 100644
index 0000000..e8c93f7
--- /dev/null
+++ b/src/pages/blog/page/[number].tsx
@@ -0,0 +1,311 @@
+import PostsList, { type Post } from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { type EdgesResponse } from '@services/graphql/api';
+import {
+ getArticleFromRawData,
+ getArticles,
+ getArticlesEndCursor,
+ getTotalArticles,
+} from '@services/graphql/articles';
+import {
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import {
+ type Article,
+ type Meta,
+ type NextPageWithLayout,
+} from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { settings } from '@utils/config';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+} from '@utils/helpers/pages';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useRedirection from '@utils/hooks/use-redirection';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { ParsedUrlQuery } from 'querystring';
+import { useIntl } from 'react-intl';
+import { Blog, Graph, WebPage } from 'schema-dts';
+
+type BlogPageProps = {
+ articles: EdgesResponse<RawArticle>;
+ pageNumber: number;
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ totalArticles: number;
+ translation: Messages;
+};
+
+/**
+ * Blog index page.
+ */
+const BlogPage: NextPageWithLayout<BlogPageProps> = ({
+ articles,
+ pageNumber,
+ thematicsList,
+ topicsList,
+ totalArticles,
+}) => {
+ useRedirection({
+ query: { param: 'number', value: '1' },
+ redirectTo: '/blog',
+ });
+
+ const intl = useIntl();
+ const title = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'BlogPage: page title',
+ id: '7TbbIk',
+ });
+ const pageNumberTitle = intl.formatMessage(
+ {
+ defaultMessage: 'Page {number}',
+ id: 'zbzlb1',
+ description: 'BlogPage: page number',
+ },
+ {
+ number: pageNumber,
+ }
+ );
+ const pageTitleWithPageNumber = `${title} - ${pageNumberTitle}`;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title: pageNumberTitle,
+ url: `/blog/page/${pageNumber}`,
+ });
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const pageTitle = `${pageTitleWithPageNumber} - ${website.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',
+ id: '18h/t0',
+ },
+ { websiteName: website.name }
+ );
+ const pageUrl = `${website.url}${asPath}`;
+
+ const webpageSchema: WebPage = {
+ '@id': `${pageUrl}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${website.url}/#breadcrumb` },
+ name: pageTitle,
+ description: pageDescription,
+ inLanguage: website.locales.default,
+ reviewedBy: { '@id': `${website.url}/#branding` },
+ url: `${website.url}`,
+ isPartOf: {
+ '@id': `${website.url}`,
+ },
+ };
+
+ const blogSchema: Blog = {
+ '@id': `${website.url}/#blog`,
+ '@type': 'Blog',
+ author: { '@id': `${website.url}/#branding` },
+ creator: { '@id': `${website.url}/#branding` },
+ editor: { '@id': `${website.url}/#branding` },
+ inLanguage: website.locales.default,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${pageUrl}` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, blogSchema],
+ };
+
+ /**
+ * Retrieve the formatted meta.
+ *
+ * @param {Meta<'article'>} meta - The article meta.
+ * @returns {Post['meta']} The formatted meta.
+ */
+ const getPostMeta = (meta: Meta<'article'>): Post['meta'] => {
+ const { commentsCount, dates, thematics, wordsCount } = meta;
+
+ return {
+ commentsCount,
+ dates,
+ readingTime: { wordsCount: wordsCount || 0, onlyMinutes: true },
+ thematics: thematics?.map((thematic) => {
+ return { ...thematic, url: `/thematique/${thematic.slug}` };
+ }),
+ };
+ };
+
+ /**
+ * Retrieve the formatted posts.
+ *
+ * @param {Article[]} posts - An array of articles.
+ * @returns {Post[]} An array of formatted posts.
+ */
+ const getPosts = (posts: Article[]): Post[] => {
+ return posts.map((post) => {
+ return {
+ ...post,
+ cover: post.meta.cover,
+ excerpt: post.intro,
+ meta: getPostMeta(post.meta),
+ url: `/article/${post.slug}`,
+ };
+ });
+ };
+
+ /**
+ * Retrieve the posts list from raw data.
+ *
+ * @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
+ * @returns {Post[]} An array of posts.
+ */
+ const getPostsList = (rawData: EdgesResponse<RawArticle>[]): Post[] => {
+ const articlesList: RawArticle[] = [];
+ rawData.forEach((articleData) =>
+ articleData.edges.forEach((edge) => {
+ articlesList.push(edge.node);
+ })
+ );
+
+ return getPosts(
+ articlesList.map((article) => getArticleFromRawData(article))
+ );
+ };
+
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'BlogPage: thematics list widget title',
+ id: 'HriY57',
+ });
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'BlogPage: topics list widget title',
+ id: '2D9tB5',
+ });
+
+ 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={pageTitleWithPageNumber} />
+ <meta property="og:description" content={pageDescription} />
+ </Head>
+ <Script
+ id="schema-blog"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={pageTitleWithPageNumber}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={{ total: totalArticles }}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map(getPageLinkFromRawData),
+ 'thematic'
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map(getPageLinkFromRawData),
+ 'topic'
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
+ >
+ <PostsList
+ baseUrl="/blog/page/"
+ byYear={true}
+ pageNumber={pageNumber}
+ posts={getPostsList([articles])}
+ total={totalArticles}
+ />
+ </PageLayout>
+ </>
+ );
+};
+
+BlogPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
+
+interface BlogPageParams extends ParsedUrlQuery {
+ number: string;
+}
+
+export const getStaticProps: GetStaticProps<BlogPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const pageNumber = Number(params!.number as BlogPageParams['number']);
+ const queriedPostsNumber = settings.postsPerPage * pageNumber;
+ const lastCursor = await getArticlesEndCursor({
+ first: queriedPostsNumber,
+ });
+ const articles = await getArticles({
+ first: settings.postsPerPage,
+ after: lastCursor,
+ });
+ const totalArticles = await getTotalArticles();
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
+ const translation = await loadTranslation(locale);
+
+ return {
+ props: {
+ articles: JSON.parse(JSON.stringify(articles)),
+ pageNumber,
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
+ totalArticles,
+ translation,
+ },
+ };
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const totalArticles = await getTotalArticles();
+ const totalPages = Math.ceil(totalArticles / settings.postsPerPage);
+ const pagesArray = Array.from(
+ { length: totalPages },
+ (_, index) => index + 1
+ );
+ const paths = pagesArray.map((number) => {
+ return { params: { number: `${number}` } };
+ });
+
+ return {
+ paths,
+ fallback: false,
+ };
+};
+
+export default BlogPage;
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index 09091c8..8895015 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -263,6 +263,7 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({
>
{data && data.length > 0 ? (
<PostsList
+ baseUrl="/recherche/page/"
byYear={true}
isLoading={isLoadingMore || isLoadingInitialData}
loadMore={loadMore}
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 6277293..271f4ec 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -205,6 +205,7 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
)}
</Heading>
<PostsList
+ baseUrl="/sujet/page/"
posts={getPosts(articles)}
total={articles.length}
titleLevel={3}
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index fbe50fc..ce4eccf 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -186,6 +186,7 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
)}
</Heading>
<PostsList
+ baseUrl="/thematique/page/"
posts={getPosts(articles)}
total={articles.length}
titleLevel={3}
diff --git a/src/services/graphql/api.ts b/src/services/graphql/api.ts
index 9f68ddc..009aea4 100644
--- a/src/services/graphql/api.ts
+++ b/src/services/graphql/api.ts
@@ -2,6 +2,7 @@ import { settings } from '@utils/config';
import {
articleBySlugQuery,
articlesCardQuery,
+ articlesEndCursor,
articlesQuery,
articlesSlugQuery,
totalArticlesQuery,
@@ -28,6 +29,7 @@ export type Queries =
| typeof articlesQuery
| typeof articleBySlugQuery
| typeof articlesCardQuery
+ | typeof articlesEndCursor
| typeof articlesSlugQuery
| typeof commentsQuery
| typeof thematicBySlugQuery
@@ -100,9 +102,15 @@ export type NodesResponse<T> = {
nodes: T[];
};
+export type EndCursor = Pick<
+ EdgesResponse<Pick<PageInfo, 'endCursor'>>,
+ 'pageInfo'
+>;
+
export type ResponseMap<T> = {
[articleBySlugQuery]: ArticleResponse<T>;
[articlesCardQuery]: ArticlesResponse<NodesResponse<T>>;
+ [articlesEndCursor]: ArticlesResponse<EndCursor>;
[articlesQuery]: ArticlesResponse<EdgesResponse<T>>;
[articlesSlugQuery]: ArticlesResponse<EdgesResponse<T>>;
[commentsQuery]: CommentsResponse<NodesResponse<T>>;
@@ -213,6 +221,7 @@ export type SendMailVars = {
export type VariablesMap = {
[articleBySlugQuery]: BySlugVar;
[articlesCardQuery]: EdgesVars;
+ [articlesEndCursor]: EdgesVars;
[articlesQuery]: EdgesVars;
[articlesSlugQuery]: EdgesVars;
[commentsQuery]: ByContentIdVar;
diff --git a/src/services/graphql/articles.query.ts b/src/services/graphql/articles.query.ts
index e02ca8e..7bd0901 100644
--- a/src/services/graphql/articles.query.ts
+++ b/src/services/graphql/articles.query.ts
@@ -184,3 +184,15 @@ export const totalArticlesQuery = `query PostsTotal($search: String = "") {
}
}
}`;
+
+/**
+ * Query the end cursor based on the queried posts number.
+ */
+export const articlesEndCursor = `query EndCursorAfter($first: Int) {
+ posts(first: $first) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+}`;
diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts
index 41052c4..f130872 100644
--- a/src/services/graphql/articles.ts
+++ b/src/services/graphql/articles.ts
@@ -7,10 +7,18 @@ import {
import { getAuthorFromRawData } from '@utils/helpers/author';
import { getImageFromRawData } from '@utils/helpers/images';
import { getPageLinkFromRawData } from '@utils/helpers/pages';
-import { EdgesResponse, EdgesVars, fetchAPI, getAPIUrl, PageInfo } from './api';
+import {
+ EdgesResponse,
+ EdgesVars,
+ EndCursor,
+ fetchAPI,
+ getAPIUrl,
+ PageInfo,
+} from './api';
import {
articleBySlugQuery,
articlesCardQuery,
+ articlesEndCursor,
articlesQuery,
articlesSlugQuery,
totalArticlesQuery,
@@ -173,3 +181,21 @@ export const getAllArticlesSlugs = async (): Promise<string[]> => {
return response.posts.edges.map((edge) => edge.node.slug);
};
+
+/**
+ * Retrieve the last cursor.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<string>} - The end cursor.
+ */
+export const getArticlesEndCursor = async (
+ props: EdgesVars
+): Promise<string> => {
+ const response = await fetchAPI<EndCursor, typeof articlesEndCursor>({
+ api: getAPIUrl(),
+ query: articlesEndCursor,
+ variables: { ...props },
+ });
+
+ return response.posts.pageInfo.endCursor;
+};
diff --git a/src/utils/hooks/use-breadcrumb.tsx b/src/utils/hooks/use-breadcrumb.tsx
index 087d400..130ebf1 100644
--- a/src/utils/hooks/use-breadcrumb.tsx
+++ b/src/utils/hooks/use-breadcrumb.tsx
@@ -40,6 +40,7 @@ const useBreadcrumb = ({
const { website } = useSettings();
const isArticle = url.startsWith('/article/');
const isHome = url === '/';
+ const isPageNumber = url.includes('/page/');
const isProject = url.startsWith('/projets/');
const isSearch = url.startsWith('/recherche');
const isThematic = url.startsWith('/thematique/');
@@ -62,7 +63,7 @@ const useBreadcrumb = ({
if (isHome) return { items, schema };
- if (isArticle || isSearch || isThematic || isTopic) {
+ if (isArticle || isPageNumber || isSearch || isThematic || isTopic) {
const blogLabel = intl.formatMessage({
defaultMessage: 'Blog',
description: 'Breadcrumb: blog label',
diff --git a/src/utils/hooks/use-redirection.tsx b/src/utils/hooks/use-redirection.tsx
new file mode 100644
index 0000000..9eb26c2
--- /dev/null
+++ b/src/utils/hooks/use-redirection.tsx
@@ -0,0 +1,33 @@
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+export type RouterQuery = {
+ param: string;
+ value: string;
+};
+
+export type UseRedirectionProps = {
+ /**
+ * The router query.
+ */
+ query: RouterQuery;
+ /**
+ * The redirection url.
+ */
+ redirectTo: string;
+};
+
+/**
+ * Redirect to another url when router query match the given parameters.
+ *
+ * @param {UseRedirectionProps} props - The redirection parameters.
+ */
+const useRedirection = ({ query, redirectTo }: UseRedirectionProps) => {
+ const router = useRouter();
+
+ useEffect(() => {
+ if (router.query[query.param] === query.value) router.push(redirectTo);
+ }, [query, redirectTo, router]);
+};
+
+export default useRedirection;