Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 src/Components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
export interface ModalProps {
isOpen: boolean;
onClose(): void;
maxWidth?: string;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
initialFocus?: React.RefObject<HTMLElement>;
}

Expand Down
245 changes: 245 additions & 0 deletions src/Components/Pagination/Pagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Pagination } from './Pagination';
import type { PaginationData } from './Pagination';

const meta: Meta<typeof Pagination> = {
component: Pagination,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
align: {
control: { type: 'select' },
options: ['start', 'center', 'end'],
},
buttonStyle: {
control: { type: 'select' },
options: ['fill', 'outline', 'link'],
},
paginationButtonAs: {
control: { type: 'select' },
options: ['a', 'button'],
},
},
};

export default meta;

type Story = StoryObj<typeof Pagination>;

// Mock pagination data for different scenarios
const createMockData = (
currentPage: number,
totalPages: number,
showEllipsis = false,
): PaginationData<{ id: number; name: string }> => {
const links = [];

// Previous link
links.push({
label: '&laquo; Previous',
url: currentPage > 1 ? `/page/${currentPage - 1}` : null,
active: false,
page: null,
});

// Page links
if (showEllipsis && totalPages > 7) {
// Complex pagination with ellipsis
if (currentPage <= 4) {
for (let i = 1; i <= 5; i++) {
links.push({
label: i.toString(),
url: `/page/${i}`,
active: i === currentPage,
page: i,
});
}
links.push({ label: '...', url: null, active: false, page: null });
links.push({
label: totalPages.toString(),
url: `/page/${totalPages}`,
active: false,
page: totalPages,
});
} else if (currentPage >= totalPages - 3) {
links.push({
label: '1',
url: '/page/1',
active: false,
page: 1,
});
links.push({ label: '...', url: null, active: false, page: null });
for (let i = totalPages - 4; i <= totalPages; i++) {
links.push({
label: i.toString(),
url: `/page/${i}`,
active: i === currentPage,
page: i,
});
}
} else {
links.push({
label: '1',
url: '/page/1',
active: false,
page: 1,
});
links.push({ label: '...', url: null, active: false, page: null });
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
links.push({
label: i.toString(),
url: `/page/${i}`,
active: i === currentPage,
page: i,
});
}
links.push({ label: '...', url: null, active: false, page: null });
links.push({
label: totalPages.toString(),
url: `/page/${totalPages}`,
active: false,
page: totalPages,
});
}
} else {
// Simple pagination without ellipsis
for (let i = 1; i <= totalPages; i++) {
links.push({
label: i.toString(),
url: `/page/${i}`,
active: i === currentPage,
page: i,
});
}
}

// Next link
links.push({
label: 'Next &raquo;',
url: currentPage < totalPages ? `/page/${currentPage + 1}` : null,
active: false,
page: null,
});

return {
data: Array.from({ length: 10 }, (_, i) => ({
id: (currentPage - 1) * 10 + i + 1,
name: `Item ${(currentPage - 1) * 10 + i + 1}`,
})),
links,
current_page: currentPage,
last_page: totalPages,
first_page_url: '/page/1',
last_page_url: `/page/${totalPages}`,
next_page_url: currentPage < totalPages ? `/page/${currentPage + 1}` : null,
prev_page_url: currentPage > 1 ? `/page/${currentPage - 1}` : null,
path: '/page',
per_page: 10,
total: totalPages * 10,
from: (currentPage - 1) * 10 + 1,
to: Math.min(currentPage * 10, totalPages * 10),
};
};

export const Basic: Story = {
args: {
data: createMockData(1, 5),
align: 'end',
showInfo: false,
buttonStyle: 'fill',
paginationButtonAs: 'a',
},
};

export const WithInfo: Story = {
args: {
data: createMockData(2, 5),
align: 'end',
showInfo: true,
buttonStyle: 'fill',
paginationButtonAs: 'a',
},
};

export const Centered: Story = {
args: {
data: createMockData(5, 8),
align: 'center',
showInfo: false,
buttonStyle: 'fill',
paginationButtonAs: 'a',
},
};

export const OutlineStyle: Story = {
args: {
data: createMockData(2, 6),
align: 'end',
showInfo: false,
buttonStyle: 'outline',
paginationButtonAs: 'a',
},
};

export const LinkStyle: Story = {
args: {
data: createMockData(1, 4),
align: 'end',
showInfo: false,
buttonStyle: 'link',
paginationButtonAs: 'a',
},
};

export const FirstPage: Story = {
args: {
data: createMockData(1, 5),
align: 'end',
showInfo: true,
buttonStyle: 'fill',
paginationButtonAs: 'a',
},
};

export const LastPage: Story = {
args: {
data: createMockData(5, 5),
align: 'end',
showInfo: true,
buttonStyle: 'fill',
paginationButtonAs: 'a',
},
};

export const MiddlePage: Story = {
args: {
data: createMockData(7, 12),
align: 'end',
showInfo: false,
buttonStyle: 'fill',
paginationButtonAs: 'a',
},
};

export const AsButtons: Story = {
args: {
data: createMockData(3, 7),
align: 'center',
showInfo: false,
buttonStyle: 'outline',
paginationButtonAs: 'button',
},
};

export const Minimal: Story = {
args: {
data: createMockData(1, 3),
align: 'start',
showInfo: false,
buttonStyle: 'link',
paginationButtonAs: 'a',
},
};
130 changes: 130 additions & 0 deletions src/Components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Button } from '../Button';

export interface Link {
label: string;
url: string | null;
active: boolean;
page: number | null;
}

export interface PaginationData<T = Record<string, unknown>> {
data: T[];
links: Link[];
current_page: number;
last_page: number;
first_page_url?: string;
last_page_url?: string;
next_page_url?: string | null;
prev_page_url?: string | null;
path?: string;
per_page: number;
total: number;
from: number | null;
to: number | null;
}

interface PaginationProps<T = Record<string, unknown>> {
data: PaginationData<T>;
align?: 'start' | 'center' | 'end';
showInfo?: boolean;
buttonStyle?: 'fill' | 'outline' | 'link';
paginationButtonAs?: 'a' | 'button';
}

export function Pagination<T = Record<string, unknown>>({
data,
align = 'end',
showInfo = false,
buttonStyle = 'fill',
paginationButtonAs = 'a',
}: PaginationProps<T>) {
if (!data) return null;

const {
prev_page_url,
next_page_url,
from,
to,
total,
links,
} = data;

const hasNav =
Boolean(prev_page_url || next_page_url) ||
(Array.isArray(links) && links.length > 0);

const pageLinks = links.filter(link => /^\d+$/.test(link.label));

const getAlignStyle = () => {
if (align === 'end') return 'md:justify-end';
if (align === 'start') return 'md:justify-start';
if (align === 'center') return 'md:justify-center';
return 'md:justify-end';
};

const Info = ({ showInfo }: { showInfo: boolean }) => {
if (!showInfo) return null;

return (
<div className="mb-2 w-full text-center text-sm text-gray-600 sm:mb-0 md:text-left" aria-live="polite">
Showing {from ?? 0} to {to ?? 0} of {total} entries
</div>
);
};

return (
<div className="flex w-full flex-col items-center justify-center md:flex-row md:justify-between">
<Info showInfo={showInfo} />

{hasNav && (
<nav
className={`flex w-full flex-wrap items-center justify-center space-x-2 ${getAlignStyle()}`}
aria-label="Pagination Navigation"
>
<Button
variant="secondary"
style={buttonStyle}
size="medium"
disabled={!prev_page_url}
as={paginationButtonAs}
href={prev_page_url ?? undefined}
className={!prev_page_url ? 'pointer-events-none' : ''}
aria-label="Go to previous page"
>
Previous
</Button>

{pageLinks.map((link) => (
<Button
key={link.url ?? `p-${link.label}`}
variant={link.active ? 'primary' : 'secondary'}
style={buttonStyle}
size="medium"
disabled={!link.url}
as={link.url ? 'a' : paginationButtonAs}
href={link.url || '#'}
className={!link.url ? 'pointer-events-none' : ''}
aria-label={`Go to page ${link.label}`}
aria-current={link.active ? 'page' : undefined}
>
{link.label}
</Button>
))}

<Button
variant="secondary"
style={buttonStyle}
size="medium"
disabled={!next_page_url}
as={paginationButtonAs}
href={next_page_url ?? undefined}
className={!next_page_url ? 'pointer-events-none' : ''}
aria-label="Go to next page"
>
Next
</Button>
</nav>
)}
</div>
);
}
Loading