Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
240 changes: 240 additions & 0 deletions src/Components/Pagination/Pagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Pagination } 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) => {
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: links.filter(link => !link.label.includes('&laquo;') && !link.label.includes('&raquo;')),
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',
},
};
129 changes: 129 additions & 0 deletions src/Components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Button } from '../Button';

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

interface Pagination<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;
to: number;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

interface PaginationProps<T = Record<string, unknown>> {
data: Pagination<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 || (!data.next_page_url && !data.prev_page_url)) return null;

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

const pageLinks = links.filter(
link =>
!link.label.includes('&laquo;') &&
!link.label.includes('&raquo;') &&
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 <></>;

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} to {to} 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} />

<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 || '#'}
className={!prev_page_url ? 'pointer-events-none' : ''}
aria-label="Go to previous page"
>
Previous
</Button>

Comment thread
MahmudE14 marked this conversation as resolved.
Outdated
{pageLinks.map((link, index) => (
<Button
key={index}
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 || '#'}
className={!next_page_url ? 'pointer-events-none' : ''}
aria-label="Go to next page"
>
Next
</Button>
</nav>
</div>
);
}
Loading