summaryrefslogtreecommitdiffstats
path: root/src/components
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/components
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/components')
-rw-r--r--src/components/Pagination/Pagination.module.scss92
-rw-r--r--src/components/Pagination/Pagination.tsx131
2 files changed, 223 insertions, 0 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;