Skip to content

Commit

Permalink
Merge pull request #973 from logto-io/charles-log-10713-tutorial-list…
Browse files Browse the repository at this point in the history
…-view-pagination

feat: add pagination to tutorial list page
  • Loading branch information
charIeszhao authored Jan 20, 2025
2 parents b6d698b + 9b65817 commit 49e747d
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 22 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-modal": "^3.16.1",
"react-paginate": "^8.2.0",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"sass": "^1.83.0",
Expand Down
31 changes: 22 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/assets/arrow-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/arrow-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/FlipOnRtl/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.flip {
transform: scaleX(-1);
}
39 changes: 39 additions & 0 deletions src/components/FlipOnRtl/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import clsx from 'clsx';
import { cloneElement, isValidElement, type ReactElement } from 'react';

import styles from './index.module.scss';

type Props = {
readonly children: ReactElement<HTMLElement>;
};

/**
* This component flips its child element horizontally if the browser's text direction is RTL (right-to-left).
*
* @component
* @example
* ```tsx
* <FlipOnRtl>
* <SVG />
* </FlipOnRtl>
* ```
*
* @param {React.ReactNode} children - The SVG or other HTML content to render and flip if RTL.
* @returns {JSX.Element} The flipped content.
*/
function FlipOnRtl({ children }: Props) {
const { i18n } = useDocusaurusContext();

const isRtl = i18n.localeConfigs[i18n.currentLocale]?.direction === 'rtl';

if (!isValidElement(children)) {
return children;
}

return cloneElement(children, {
className: clsx(children.props.className, isRtl && styles.flip),
});
}

export default FlipOnRtl;
83 changes: 83 additions & 0 deletions src/components/Pagination/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@use '@scss/underscore' as _;

.container {
display: flex;
justify-content: flex-end;
align-items: center;

.positionInfo {
font: var(--font-body-2);
color: var(--logto-color-text-secondary);
}

.pagination {
display: flex;
justify-content: right;
margin: 0;
height: 28px;
padding-inline-start: _.unit(4);

li {
list-style: none;

&:not(:first-child) {
margin-inline-start: _.unit(2);
}

.button {
display: block;
font: var(--font-label-2);
background: transparent;
border-radius: 6px;
min-width: 28px;
padding: 3px 6px;
height: 28px;
border: 1px solid var(--logto-border);
color: var(--logto-color-text);
user-select: none;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: 0.3s ease-in-out;
transition-property: border-color, opacity;
opacity: 100%;
outline-offset: 0;

svg {
width: 20px;
height: 20px;
}

&.active {
border-color: var(--ifm-link-color);
color: var(--ifm-link-color);
}
}
}
}

li.disabled {
cursor: not-allowed;

.button {
background: var(logto-container-neutral-bg);
}
}

&.pico {
.pagination {
height: 20px;

li {
.button {
border-radius: 4px;
height: 20px;
min-width: unset;
border: unset;
background: unset;
padding: 0;
}
}
}
}
}
104 changes: 104 additions & 0 deletions src/components/Pagination/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import BrowserOnly from '@docusaurus/BrowserOnly';
import Translate from '@docusaurus/Translate';
import clsx from 'clsx';
import { useEffect, useRef } from 'react';
import ReactPaginate from 'react-paginate';

import Previous from '@site/src/assets/arrow-left.svg';
import Next from '@site/src/assets/arrow-right.svg';

import FlipOnRtl from '../FlipOnRtl';

import styles from './index.module.scss';

export type Props = {
readonly page: number;
readonly totalCount: number;
readonly pageSize: number;
readonly className?: string;
readonly mode?: 'normal' | 'pico';
readonly onChange?: (pageIndex: number) => void;
};

const Pagination = ({
page,
totalCount,
pageSize,
className,
mode = 'normal',
onChange,
}: Props) => {
const ref = useRef<HTMLDivElement>(null);
const pageCount = Math.ceil(totalCount / pageSize);

useEffect(() => {
if (ref.current) {
const ul = ref.current.querySelector('ul[role="navigation"]');
// This role is not properly set by react-paginate
// See https://dequeuniversity.com/rules/axe/4.10/aria-allowed-role
ul?.removeAttribute('role');
}
}, []);

if (pageCount <= 1) {
return null;
}

const min = (page - 1) * pageSize + 1;
const max = Math.min(page * pageSize, totalCount);
const isPicoMode = mode === 'pico';

return (
<div ref={ref} className={clsx(styles.container, isPicoMode && styles.pico, className)}>
<div className={styles.positionInfo}>
<Translate id="theme.common.pagination.info" values={{ min, max, total: totalCount }}>
{'{min}-{max} of {total}'}
</Translate>
</div>
<BrowserOnly>
{() => (
<ReactPaginate
className={styles.pagination}
pageCount={pageCount}
forcePage={page - 1}
pageLabelBuilder={(pageNumber: number) => (
<button
className={clsx(styles.button, pageNumber === page && styles.active)}
aria-label={`Page ${pageNumber}`}
>
{String(pageNumber)}
</button>
)}
previousLabel={
<FlipOnRtl>
<button className={styles.button} disabled={page === 1} aria-label="Previous page">
<Previous />
</button>
</FlipOnRtl>
}
nextLabel={
<FlipOnRtl>
<button
className={styles.button}
disabled={page === pageCount}
aria-label="Next page"
>
<Next />
</button>
</FlipOnRtl>
}
breakLabel={<button className={styles.button}>...</button>}
disabledClassName={styles.disabled}
pageRangeDisplayed={isPicoMode ? -1 : undefined}
marginPagesDisplayed={isPicoMode ? 0 : undefined}
onPageChange={({ selected }) => {
onChange?.(selected + 1);
}}
/>
)}
</BrowserOnly>
</div>
);
};

export default Pagination;
Loading

0 comments on commit 49e747d

Please sign in to comment.