diff --git a/package-lock.json b/package-lock.json
index cba706a1..e9a2fa1e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26488,9 +26488,9 @@
}
},
"node_modules/telejson": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.1.0.tgz",
- "integrity": "sha512-jFJO4P5gPebZAERPkJsqMAQ0IMA1Hi0AoSfxpnUaV6j6R2SZqlpkbS20U6dEUtA3RUYt2Ak/mTlkQzHH9Rv/hA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz",
+ "integrity": "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==",
"dev": true,
"dependencies": {
"memoizerific": "^1.11.3"
@@ -28227,6 +28227,7 @@
"@storybook/addon-links": "^7.1.0",
"@storybook/blocks": "^7.1.0",
"@storybook/jest": "^0.1.0",
+ "@storybook/preview-api": "^7.4.0",
"@storybook/react": "^7.1.0",
"@storybook/react-vite": "^7.1.0",
"@storybook/test-runner": "^0.11.0",
@@ -28248,6 +28249,104 @@
"wait-on": "^7.0.1"
}
},
+ "packages/react-front-kit/node_modules/@storybook/channels": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.4.0.tgz",
+ "integrity": "sha512-/1CU0s3npFumzVHLGeubSyPs21O3jNqtSppOjSB9iDTyV2GtQrjh5ntVwebfKpCkUSitx3x7TkCb9dylpEZ8+w==",
+ "dev": true,
+ "dependencies": {
+ "@storybook/client-logger": "7.4.0",
+ "@storybook/core-events": "7.4.0",
+ "@storybook/global": "^5.0.0",
+ "qs": "^6.10.0",
+ "telejson": "^7.2.0",
+ "tiny-invariant": "^1.3.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ }
+ },
+ "packages/react-front-kit/node_modules/@storybook/client-logger": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.4.0.tgz",
+ "integrity": "sha512-4pBnf7+df1wXEVcF1civqxbrtccGGHQkfWQkJo49s53RXvF7SRTcif6XTx0V3cQV0v7I1C5mmLm0LNlmjPRP1Q==",
+ "dev": true,
+ "dependencies": {
+ "@storybook/global": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ }
+ },
+ "packages/react-front-kit/node_modules/@storybook/core-events": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.4.0.tgz",
+ "integrity": "sha512-JavEo4dw7TQdF5pSKjk4RtqLgsG2R/eWRI8vZ3ANKa0ploGAnQR/eMTfSxf6TUH3ElBWLJhi+lvUCkKXPQD+dw==",
+ "dev": true,
+ "dependencies": {
+ "ts-dedent": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ }
+ },
+ "packages/react-front-kit/node_modules/@storybook/preview-api": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.4.0.tgz",
+ "integrity": "sha512-ndXO0Nx+eE7ktVE4EqHpQZ0guX7yYBdruDdJ7B739C0+OoPWsJN7jAzUqq0NXaBcYrdaU5gTy+KnWJUt8R+OyA==",
+ "dev": true,
+ "dependencies": {
+ "@storybook/channels": "7.4.0",
+ "@storybook/client-logger": "7.4.0",
+ "@storybook/core-events": "7.4.0",
+ "@storybook/csf": "^0.1.0",
+ "@storybook/global": "^5.0.0",
+ "@storybook/types": "7.4.0",
+ "@types/qs": "^6.9.5",
+ "dequal": "^2.0.2",
+ "lodash": "^4.17.21",
+ "memoizerific": "^1.11.3",
+ "qs": "^6.10.0",
+ "synchronous-promise": "^2.0.15",
+ "ts-dedent": "^2.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ }
+ },
+ "packages/react-front-kit/node_modules/@storybook/types": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.0.tgz",
+ "integrity": "sha512-XyzYkmeklywxvElPrIWLczi/PWtEdgTL6ToT3++FVxptsC2LZKS3Ue+sBcQ9xRZhkRemw4HQHwed5EW3dO8yUg==",
+ "dev": true,
+ "dependencies": {
+ "@storybook/channels": "7.4.0",
+ "@types/babel__core": "^7.0.0",
+ "@types/express": "^4.7.0",
+ "@types/react": "^16.14.34",
+ "file-system-cache": "2.3.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ }
+ },
+ "packages/react-front-kit/node_modules/@storybook/types/node_modules/@types/react": {
+ "version": "16.14.46",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.46.tgz",
+ "integrity": "sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw==",
+ "dev": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
"packages/react-front-kit/node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
@@ -46036,6 +46135,7 @@
"@storybook/addon-links": "^7.1.0",
"@storybook/blocks": "^7.1.0",
"@storybook/jest": "^0.1.0",
+ "@storybook/preview-api": "^7.4.0",
"@storybook/react": "^7.1.0",
"@storybook/react-vite": "^7.1.0",
"@storybook/test-runner": "^0.11.0",
@@ -46059,6 +46159,86 @@
"wait-on": "^7.0.1"
},
"dependencies": {
+ "@storybook/channels": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.4.0.tgz",
+ "integrity": "sha512-/1CU0s3npFumzVHLGeubSyPs21O3jNqtSppOjSB9iDTyV2GtQrjh5ntVwebfKpCkUSitx3x7TkCb9dylpEZ8+w==",
+ "dev": true,
+ "requires": {
+ "@storybook/client-logger": "7.4.0",
+ "@storybook/core-events": "7.4.0",
+ "@storybook/global": "^5.0.0",
+ "qs": "^6.10.0",
+ "telejson": "^7.2.0",
+ "tiny-invariant": "^1.3.1"
+ }
+ },
+ "@storybook/client-logger": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.4.0.tgz",
+ "integrity": "sha512-4pBnf7+df1wXEVcF1civqxbrtccGGHQkfWQkJo49s53RXvF7SRTcif6XTx0V3cQV0v7I1C5mmLm0LNlmjPRP1Q==",
+ "dev": true,
+ "requires": {
+ "@storybook/global": "^5.0.0"
+ }
+ },
+ "@storybook/core-events": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.4.0.tgz",
+ "integrity": "sha512-JavEo4dw7TQdF5pSKjk4RtqLgsG2R/eWRI8vZ3ANKa0ploGAnQR/eMTfSxf6TUH3ElBWLJhi+lvUCkKXPQD+dw==",
+ "dev": true,
+ "requires": {
+ "ts-dedent": "^2.0.0"
+ }
+ },
+ "@storybook/preview-api": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.4.0.tgz",
+ "integrity": "sha512-ndXO0Nx+eE7ktVE4EqHpQZ0guX7yYBdruDdJ7B739C0+OoPWsJN7jAzUqq0NXaBcYrdaU5gTy+KnWJUt8R+OyA==",
+ "dev": true,
+ "requires": {
+ "@storybook/channels": "7.4.0",
+ "@storybook/client-logger": "7.4.0",
+ "@storybook/core-events": "7.4.0",
+ "@storybook/csf": "^0.1.0",
+ "@storybook/global": "^5.0.0",
+ "@storybook/types": "7.4.0",
+ "@types/qs": "^6.9.5",
+ "dequal": "^2.0.2",
+ "lodash": "^4.17.21",
+ "memoizerific": "^1.11.3",
+ "qs": "^6.10.0",
+ "synchronous-promise": "^2.0.15",
+ "ts-dedent": "^2.0.0",
+ "util-deprecate": "^1.0.2"
+ }
+ },
+ "@storybook/types": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.0.tgz",
+ "integrity": "sha512-XyzYkmeklywxvElPrIWLczi/PWtEdgTL6ToT3++FVxptsC2LZKS3Ue+sBcQ9xRZhkRemw4HQHwed5EW3dO8yUg==",
+ "dev": true,
+ "requires": {
+ "@storybook/channels": "7.4.0",
+ "@types/babel__core": "^7.0.0",
+ "@types/express": "^4.7.0",
+ "@types/react": "^16.14.34",
+ "file-system-cache": "2.3.0"
+ },
+ "dependencies": {
+ "@types/react": {
+ "version": "16.14.46",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.46.tgz",
+ "integrity": "sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw==",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ }
+ }
+ },
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
@@ -47511,9 +47691,9 @@
}
},
"telejson": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.1.0.tgz",
- "integrity": "sha512-jFJO4P5gPebZAERPkJsqMAQ0IMA1Hi0AoSfxpnUaV6j6R2SZqlpkbS20U6dEUtA3RUYt2Ak/mTlkQzHH9Rv/hA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz",
+ "integrity": "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==",
"dev": true,
"requires": {
"memoizerific": "^1.11.3"
diff --git a/packages/react-front-kit/package.json b/packages/react-front-kit/package.json
index 3c86dcc0..7b98e0c7 100644
--- a/packages/react-front-kit/package.json
+++ b/packages/react-front-kit/package.json
@@ -63,6 +63,7 @@
"@storybook/addon-links": "^7.1.0",
"@storybook/blocks": "^7.1.0",
"@storybook/jest": "^0.1.0",
+ "@storybook/preview-api": "^7.4.0",
"@storybook/react": "^7.1.0",
"@storybook/react-vite": "^7.1.0",
"@storybook/test-runner": "^0.11.0",
diff --git a/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.stories.tsx b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.stories.tsx
new file mode 100644
index 00000000..bfc09253
--- /dev/null
+++ b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.stories.tsx
@@ -0,0 +1,38 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { useStorybookArgsConnect } from '../../../hooks/useStorybookArgsConnect';
+
+import { Pagination as Cmp } from './Pagination';
+
+const meta = {
+ component: Cmp,
+ decorators: [
+ function Component(Story, ctx) {
+ const args = useStorybookArgsConnect(ctx.args, {
+ onPageChange: 'page',
+ onRowsPerPageChange: 'rowsPerPage',
+ });
+ return ;
+ },
+ ],
+ tags: ['autodocs'],
+ title: '3-custom/Components/Pagination',
+} satisfies Meta;
+
+export default meta;
+type IStory = StoryObj;
+
+export const Pagination: IStory = {
+ args: {
+ page: 2,
+ rowsPerPage: 15,
+ rowsPerPageLabel: 'Number of results per page',
+ rowsPerPageOptions: [
+ { label: 'Display 1 result', value: 1 },
+ { label: 'Display 5 results', value: 5 },
+ { label: 'Display 10 results', value: 10 },
+ { label: 'Display 15 results', value: 15 },
+ ],
+ totalPages: 10,
+ },
+};
diff --git a/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.test.tsx b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.test.tsx
new file mode 100644
index 00000000..087fbd70
--- /dev/null
+++ b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.test.tsx
@@ -0,0 +1,62 @@
+import { expect } from '@storybook/jest';
+import { within } from '@storybook/testing-library';
+
+import { renderWithProviders } from '../../../utils/tests';
+
+import { Pagination } from './Pagination';
+
+describe('Pagination', () => {
+ beforeEach(() => {
+ // Prevent mantine random ID
+ Math.random = () => 0.42;
+ });
+
+ it('matches snapshot', () => {
+ const { container } = renderWithProviders(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders with minimal props', () => {
+ const { container } = renderWithProviders(
+
+ );
+ const canvas = within(container);
+ expect(canvas.queryByTestId('pagination')).toBeVisible();
+ expect(canvas.queryByTestId('pagination-rowsPerPage')).toBeNull();
+ expect(canvas.queryByTestId('pagination-page')).toBeVisible();
+ });
+
+ it('renders with full props', () => {
+ const { container } = renderWithProviders(
+
+ );
+ const canvas = within(container);
+ expect(canvas.queryByTestId('pagination')).toBeVisible();
+ expect(canvas.queryByTestId('pagination-rowsPerPage')).toBeVisible();
+ expect(canvas.queryByTestId('pagination-page')).toBeVisible();
+ });
+});
diff --git a/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.tsx b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.tsx
new file mode 100644
index 00000000..26e90d03
--- /dev/null
+++ b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.tsx
@@ -0,0 +1,93 @@
+'use client';
+import type { PaginationProps } from '@mantine/core';
+import type { FlexProps } from '@mantine/core/lib/Flex/Flex';
+import type { SelectProps } from '@mantine/core/lib/Select/Select';
+import type { ReactElement } from 'react';
+
+import {
+ Flex,
+ Pagination as MantinePagination,
+ Select,
+ createStyles,
+} from '@mantine/core';
+
+const useStyles = createStyles(() => ({
+ container: {
+ justifyContent: 'space-between',
+ },
+}));
+
+export interface IRowsPerPageOption {
+ label?: string;
+ value: number;
+}
+
+interface IPaginationProps extends FlexProps {
+ onPageChange?: (value: number) => void;
+ onRowsPerPageChange?: (value: number) => void;
+ page: number;
+ paginationProps?: PaginationProps;
+ rowsPerPage: number;
+ rowsPerPageLabel?: string;
+ rowsPerPageOptions?: IRowsPerPageOption[];
+ selectProps?: SelectProps;
+ totalPages: number;
+}
+
+export function Pagination(props: IPaginationProps): ReactElement {
+ const {
+ onPageChange,
+ onRowsPerPageChange,
+ page,
+ paginationProps,
+ rowsPerPage,
+ rowsPerPageLabel,
+ rowsPerPageOptions = [],
+ selectProps,
+ totalPages,
+ ...flexProps
+ } = props;
+ const { classes } = useStyles();
+
+ /* methods */
+ function handleChangeRowsPerPage(value: string): void {
+ if (value) {
+ onRowsPerPageChange?.(Number(value));
+ }
+ }
+
+ function handleChangePage(value: number): void {
+ onPageChange?.(value);
+ }
+
+ return (
+
+ {rowsPerPageOptions.length > 0 && rowsPerPageLabel ? (
+
+ );
+}
diff --git a/packages/react-front-kit/src/3-custom/Components/Pagination/__snapshots__/Pagination.test.tsx.snap b/packages/react-front-kit/src/3-custom/Components/Pagination/__snapshots__/Pagination.test.tsx.snap
new file mode 100644
index 00000000..a676acce
--- /dev/null
+++ b/packages/react-front-kit/src/3-custom/Components/Pagination/__snapshots__/Pagination.test.tsx.snap
@@ -0,0 +1,123 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Pagination matches snapshot 1`] = `
+
+`;
diff --git a/packages/react-front-kit/src/hooks/useStorybookArgsConnect.ts b/packages/react-front-kit/src/hooks/useStorybookArgsConnect.ts
new file mode 100644
index 00000000..5f822a74
--- /dev/null
+++ b/packages/react-front-kit/src/hooks/useStorybookArgsConnect.ts
@@ -0,0 +1,23 @@
+import { useArgs } from '@storybook/preview-api';
+
+import { isCallback } from '../utils/utilities';
+
+export function useStorybookArgsConnect>(
+ args: T,
+ propsToConnect: Record
+): T {
+ const [, setArgs] = useArgs();
+ const connectedProps = Object.entries(propsToConnect)
+ .filter(([action]) => isCallback(args[action]))
+ .map(([action, prop]) => [
+ [action],
+ (...params: unknown[]): void => {
+ (args[action] as (...params: unknown[]) => void)?.(...params);
+ if (args[prop] !== undefined) {
+ // We suppose the value is passed as the first argument in the callback
+ setArgs({ [prop]: params[0] } as Partial);
+ }
+ },
+ ]);
+ return { ...args, ...Object.fromEntries(connectedProps) };
+}
diff --git a/packages/react-front-kit/src/index.tsx b/packages/react-front-kit/src/index.tsx
index cdebd8cf..ce96b15c 100644
--- a/packages/react-front-kit/src/index.tsx
+++ b/packages/react-front-kit/src/index.tsx
@@ -4,6 +4,7 @@ export * from './3-custom/Components/SidebarMenu/SidebarMenu';
export * from './3-custom/Components/HeaderSearch/HeaderSearch';
export * from './3-custom/Components/DropdownButton/DropdownButton';
export * from './3-custom/Components/Header/Header';
+export * from './3-custom/Components/Pagination/Pagination';
export * from './3-custom/Pages/TestPage/TestPage';
export * from './3-custom/Provider/Provider';
export * from './theme';
diff --git a/packages/react-front-kit/src/utils/utilities.ts b/packages/react-front-kit/src/utils/utilities.ts
new file mode 100644
index 00000000..53edc747
--- /dev/null
+++ b/packages/react-front-kit/src/utils/utilities.ts
@@ -0,0 +1,9 @@
+export function isNotNullNotEmpty(
+ value: S | undefined | Record
+): value is Exclude {
+ return value != null && Object.keys(value).length !== 0;
+}
+
+export function isCallback(maybeFunc: T | unknown): maybeFunc is T {
+ return typeof maybeFunc === 'function';
+}