Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds pagination component #1586

Merged
merged 37 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5f3c0e4
adds all, very inspired by shadcn
Snorre98 Nov 1, 2024
4ac2ae6
removes unused code
Snorre98 Nov 1, 2024
f484882
adds Pagination to outer barrel file
Snorre98 Nov 1, 2024
cd8b516
removed more unused code and comments
Snorre98 Nov 1, 2024
cb2b4d2
improved / simplified story
Snorre98 Nov 1, 2024
5358ef3
adds supress unique key
Snorre98 Nov 1, 2024
c58468e
refactors styling
Snorre98 Nov 1, 2024
7f9008e
adds margin to component
Snorre98 Nov 2, 2024
c9d731f
Update frontend/src/Components/Pagination/components/PaginationEllips…
Snorre98 Nov 6, 2024
18119b6
add classnames
Snorre98 Nov 6, 2024
98bb7c5
added classname to ellipsis
Snorre98 Nov 6, 2024
3a20087
added ellipsis icon, same width as chevrons in pagination control (in…
Snorre98 Nov 6, 2024
26bc968
uses classNames in PaginationContent
Snorre98 Nov 6, 2024
253db6f
adds whitespace in PaginationContent
Snorre98 Nov 6, 2024
506d188
adds classNames in Pagination
Snorre98 Nov 6, 2024
551da47
whitespace in pagination
Snorre98 Nov 6, 2024
993e2dd
style to make button width consistent
Snorre98 Nov 6, 2024
432f7b7
renamed PaginationControl
Snorre98 Nov 6, 2024
58a4d59
renamed controllText to controlSymbol
Snorre98 Nov 6, 2024
a2bd975
removed style modularity from pagination control (removes props)
Snorre98 Nov 6, 2024
59120eb
PaginationControllProps -> PaginationControlProps
Snorre98 Nov 6, 2024
7fd5778
newline between import and comp
Snorre98 Nov 6, 2024
b873652
newline between pagination imte comp and imports
Snorre98 Nov 6, 2024
a151709
barrel x 6
Snorre98 Nov 6, 2024
1e9042b
use useMemo
Snorre98 Nov 6, 2024
f81ad32
fix storybook
Snorre98 Nov 6, 2024
7cf16ee
sibling count and boundry count
Snorre98 Nov 6, 2024
b93b292
klink siblings with boundries
Snorre98 Nov 6, 2024
5e65ad3
changes to good ol regular function
Snorre98 Nov 6, 2024
b98d53b
better styling
Snorre98 Nov 6, 2024
649a91f
renamed to paged pagination
Snorre98 Nov 6, 2024
f127185
renamed PaginationControll
Snorre98 Nov 6, 2024
7be8aad
more renaming
Snorre98 Nov 6, 2024
df3763c
more rename
Snorre98 Nov 6, 2024
6468e98
extend button prop
Snorre98 Nov 6, 2024
bfd4a1f
rename
Snorre98 Nov 6, 2024
50f2b12
Merge branch 'master' into 1585-pagination-component
Snorre98 Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/Components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import styles from './Button.module.scss';
import type { ButtonDisplay, ButtonTheme } from './types';
import { displayToStyleMap, themeToStyleMap } from './utils';

type ButtonProps = {
export type ButtonProps = {
name?: string;
theme?: ButtonTheme;
display?: ButtonDisplay;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/Components/Button/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { Button } from './Button';
export type { ButtonProps } from './Button';
export type { ButtonDisplay, ButtonTheme } from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.container {
display: flex;
justify-content: center;
margin: 1rem;
width: 100%;
}
153 changes: 153 additions & 0 deletions frontend/src/Components/Pagination/PagedPagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { useState } from 'react';
import { PagedPagination } from './PagedPagination';

export default {
title: 'Components/DRFPagination',
component: PagedPagination,
argTypes: {
currentPage: {
control: 'number',
description: 'Current active page',
},
totalItems: {
control: 'number',
description: 'Total number of items to paginate',
},
pageSize: {
control: 'number',
description: 'Number of items per page',
},
siblingCount: {
control: 'number',
description: 'Number of sibling pages around the current page',
defaultValue: 1,
},
boundaryCount: {
control: 'number',
description: 'Number of pages to display at the start and end',
defaultValue: 1,
},
className: {
control: 'text',
description: 'Custom class for the pagination container',
},
itemClassName: {
control: 'text',
description: 'Custom class for individual pagination items',
},
},
parameters: {
docs: {
description: {
component: 'A pagination component designed to work with Django Rest Framework pagination.',
},
},
},
} as ComponentMeta<typeof PagedPagination>;

// Template with state management
const Template: ComponentStory<typeof PagedPagination> = (args) => {
const [currentPage, setCurrentPage] = useState(args.currentPage);

return (
<div>
<PagedPagination {...args} currentPage={currentPage} onPageChange={setCurrentPage} />
<p>Current page: {currentPage}</p>
</div>
);
};

// Basic usage
export const Basic = Template.bind({});
Basic.args = {
currentPage: 1,
totalItems: 100,
pageSize: 10,
};
Basic.parameters = {
docs: {
description: {
story: 'Basic pagination with default styling',
},
},
};

// Many pages example
export const ManyPages = Template.bind({});
ManyPages.args = {
...Basic.args,
totalItems: 2500,
currentPage: 7,
};
ManyPages.parameters = {
docs: {
description: {
story: 'Pagination with many pages showing ellipsis',
},
},
};

// Minimal pages example
export const MinimalPages = Template.bind({});
MinimalPages.args = {
...Basic.args,
totalItems: 30,
pageSize: 10,
};
MinimalPages.parameters = {
docs: {
description: {
story: 'Pagination with only a few pages using text theme',
},
},
};

// Example with increased sibling count
export const SiblingCountTwo = Template.bind({});
SiblingCountTwo.args = {
...Basic.args,
totalItems: 250,
siblingCount: 2,
currentPage: 5,
};
SiblingCountTwo.parameters = {
docs: {
description: {
story: 'Pagination showing two sibling pages around the current page.',
},
},
};

// Example with increased boundary count
export const BoundaryCountTwo = Template.bind({});
BoundaryCountTwo.args = {
...Basic.args,
totalItems: 250,
boundaryCount: 2,
currentPage: 10,
};
BoundaryCountTwo.parameters = {
docs: {
description: {
story: 'Pagination with two boundary pages displayed at the start and end.',
},
},
};

// Combination of increased sibling and boundary count
export const SiblingAndBoundary = Template.bind({});
SiblingAndBoundary.args = {
...Basic.args,
totalItems: 250,
siblingCount: 2,
boundaryCount: 2,
currentPage: 12,
};
SiblingAndBoundary.parameters = {
docs: {
description: {
story: 'Pagination showing two sibling pages and two boundary pages.',
},
},
};
115 changes: 115 additions & 0 deletions frontend/src/Components/Pagination/PagedPagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Icon } from '@iconify/react';
import classNames from 'classnames';
import { useMemo } from 'react';
import styles from './PagedPagination.module.scss';
import { Pagination, PaginationButton, PaginationContent, PaginationEllipsis, PaginationItem } from './components';

type PagedPaginationPaginationItemType = (number | 'ellipsis')[];

interface PagedPaginationnProps {
currentPage: number;
totalItems: number;
pageSize: number;
onPageChange: (page: number) => void;
siblingCount?: number; // Controls the number of sibling pages around the current page
boundaryCount?: number; // Controls the number of boundary pages on each end
paginationClassName?: string;
itemClassName?: string;
}

// Helper function to generate sequential page numbers
const generateSequentialPages = (start: number, end: number): number[] => {
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
};

// Adjusted ellipsis helper functions
const showStartEllipsis = (current: number, boundaryCount: number, siblingCount: number): boolean =>
boundaryCount > 0 && siblingCount > 0 && current > boundaryCount + siblingCount + 1;
const showEndEllipsis = (current: number, total: number, boundaryCount: number, siblingCount: number): boolean =>
boundaryCount > 0 && siblingCount > 0 && current < total - boundaryCount - siblingCount;

export function PagedPagination({
currentPage,
totalItems,
pageSize,
onPageChange,
siblingCount = 1,
boundaryCount = 1,
paginationClassName,
itemClassName,
}: PagedPaginationnProps) {
const totalPages = Math.ceil(totalItems / pageSize);

const paginationItems = useMemo(() => {
const pages: PagedPaginationPaginationItemType = [];
const startPages = generateSequentialPages(1, Math.min(boundaryCount, totalPages));
const endPages = generateSequentialPages(Math.max(totalPages - boundaryCount + 1, boundaryCount + 1), totalPages);

// Early return for simple pagination case
if (totalPages <= 7 + siblingCount * 2 + boundaryCount * 2) {
return generateSequentialPages(1, totalPages);
}

// Add boundary pages at the start
pages.push(...startPages);

// Conditionally add start ellipsis
if (showStartEllipsis(currentPage, boundaryCount, siblingCount)) {
pages.push('ellipsis');
}

// Add sibling pages around the current page
const startSibling = Math.max(boundaryCount + 1, currentPage - siblingCount);
const endSibling = Math.min(totalPages - boundaryCount, currentPage + siblingCount);
pages.push(...generateSequentialPages(startSibling, endSibling));

// Conditionally add end ellipsis
if (showEndEllipsis(currentPage, totalPages, boundaryCount, siblingCount)) {
pages.push('ellipsis');
}

// Add boundary pages at the end
pages.push(...endPages);

return pages;
}, [currentPage, totalPages, siblingCount, boundaryCount]);

return (
<div className={styles.container}>
<Pagination className={classNames(paginationClassName)}>
<PaginationContent>
<PaginationItem className={classNames(itemClassName)}>
<PaginationButton
buttonSymbol={<Icon icon={'mdi:chevron-left'} />}
onClick={() => currentPage > 1 && onPageChange(currentPage - 1)}
disabled={currentPage === 1}
/>
</PaginationItem>

{paginationItems.map((page, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<PaginationItem key={index} className={classNames(itemClassName)}>
{page === 'ellipsis' ? (
<PaginationEllipsis />
) : (
<PaginationButton
isActive={page === currentPage}
buttonSymbol={String(page)}
onClick={() => onPageChange(page)}
/>
)}
</PaginationItem>
))}

<PaginationItem className={classNames(itemClassName)}>
<PaginationButton
buttonSymbol={<Icon icon={'mdi:chevron-right'} />}
onClick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.nav {
display: flex;
align-items: center;
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import classNames from 'classnames';
import React from 'react';
import styles from './Pagination.module.scss';

export const Pagination = React.forwardRef<HTMLElement, React.ComponentProps<'nav'>>(({ className, ...props }, ref) => (
<nav ref={ref} aria-label="pagination" className={classNames(styles.nav, className)} {...props} />
));
Pagination.displayName = 'Pagination';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Pagination } from './Pagination';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.control {
display: flex;
justify-content: center;
align-items: center;
padding: 0.25rem 0.5rem;
width: 100%;
min-width: 2.5rem;
text-align: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import type { ButtonProps } from '~/Components';
import { Button } from '~/Components/Button';
import styles from './PaginationButton.module.scss';

type PaginationButtonProps = Omit<ButtonProps, 'theme' | 'display'> & {
isActive?: boolean;
buttonSymbol: string | ReactNode;
};

export function PaginationButton({
isActive,
className,
buttonSymbol,
disabled,
onClick,
...props
}: PaginationButtonProps) {
return (
<Button
theme={isActive ? 'basic' : 'samf'}
display={'basic'}
rounded={false}
onClick={onClick}
disabled={disabled}
className={classNames(styles.control, className)}
{...props}
>
{buttonSymbol}
</Button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PaginationButton } from './PaginationButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.list {
display: flex;
gap: 0.5rem;
align-items: center;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import classNames from 'classnames';
import React from 'react';
import styles from './PaginationContent.module.scss';

export const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
({ className, ...props }, ref) => <ul ref={ref} className={classNames(styles.list, className)} {...props} />,
);
PaginationContent.displayName = 'PaginationContent';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PaginationContent } from './PaginationContent';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.ellipsis {
robines marked this conversation as resolved.
Show resolved Hide resolved
display: flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Icon } from '@iconify/react';
import classNames from 'classnames';
import React from 'react';
import styles from './PaginationEllipsis.module.scss';

export const PaginationEllipsis = React.forwardRef<HTMLSpanElement, React.ComponentProps<'span'>>(
({ className, ...props }, ref) => (
<span ref={ref} className={classNames(styles.ellipsis, className)} {...props}>
<Icon icon={'lucide:ellipsis'} />
</span>
),
);
PaginationEllipsis.displayName = 'PaginationEllipsis';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PaginationEllipsis } from './PaginationEllipsis';
Loading
Loading