diff --git a/src/Components/Pagination/Pagination.stories.tsx b/src/Components/Pagination/Pagination.stories.tsx new file mode 100644 index 0000000..6b80a79 --- /dev/null +++ b/src/Components/Pagination/Pagination.stories.tsx @@ -0,0 +1,240 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Pagination } from './Pagination'; + +const meta: Meta = { + 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; + +// Mock pagination data for different scenarios +const createMockData = (currentPage: number, totalPages: number, showEllipsis = false) => { + const links = []; + + // Previous link + links.push({ + label: '« 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 »', + 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('«') && !link.label.includes('»')), + 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', + }, +}; diff --git a/src/Components/Pagination/Pagination.tsx b/src/Components/Pagination/Pagination.tsx new file mode 100644 index 0000000..3b87729 --- /dev/null +++ b/src/Components/Pagination/Pagination.tsx @@ -0,0 +1,129 @@ +import { Button } from '../Button'; + +interface Link { + label: string; + url: string | null; + active: boolean; + page: number | null; +} + +interface Pagination> { + 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; +} + +interface PaginationProps> { + data: Pagination; + align?: 'start' | 'center' | 'end'; + showInfo?: boolean; + buttonStyle?: 'fill' | 'outline' | 'link'; + paginationButtonAs?: 'a' | 'button'; +} + +export function Pagination>({ + data, + align = 'end', + showInfo = false, + buttonStyle = 'fill', + paginationButtonAs = 'a', +}: PaginationProps) { + 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('«') && + !link.label.includes('»') && + 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 ( +
+ Showing {from} to {to} of {total} entries +
+ ); + }; + + return ( +
+ + + +
+ ); +} diff --git a/src/Components/Pagination/README.md b/src/Components/Pagination/README.md new file mode 100644 index 0000000..cf9d342 --- /dev/null +++ b/src/Components/Pagination/README.md @@ -0,0 +1,238 @@ +# Pagination Component Documentation + +The `Pagination` component is a flexible pagination control built using React and Tailwind CSS. It displays page navigation links and provides an intuitive interface for users to navigate through paginated data. + +## Usage + +To use the `Pagination` component, import it into your React component and pass the required props with pagination data. + +### Props + +The `Pagination` component accepts the following props: + +- `data` (Pagination, required): The pagination data object containing links, current page info, and metadata. + +- `align` ('start' | 'center' | 'end', optional, default: 'end'): The alignment of the pagination controls. Options are 'start', 'center', or 'end'. + +- `showInfo` (boolean, optional, default: false): If true, displays information about the current page range and total entries. + +- `buttonStyle` ('fill' | 'outline' | 'link', optional, default: 'fill'): The visual style type of the buttons. Options are 'fill' (solid background), 'outline' (border only), or 'link' (minimal styling). + +- `paginationButtonAs` ('a' | 'button', optional, default: 'a'): The HTML element type to use for pagination buttons. 'a' for anchor links, 'button' for form submissions. + +### Pagination Data Structure + +The `data` prop expects an object with the following structure: + +```typescript +interface Pagination> { + data: T[]; // Array of items for the current page + links: Link[]; // Array of pagination links + current_page: number; // Current page number + last_page: number; // Total number of pages + first_page_url?: string; // URL for the first page + last_page_url?: string; // URL for the last page + next_page_url?: string | null; // URL for the next page + prev_page_url?: string | null; // URL for the previous page + path?: string; // Base path for pagination URLs + per_page: number; // Number of items per page + total: number; // Total number of items + from: number; // First item number on current page + to: number; // Last item number on current page +} + +interface Link { + label: string; // Display label for the link + url: string | null; // URL for the link (null if disabled) + active: boolean; // Whether this is the current page + page: number | null; // Page number (null for Previous/Next) +} +``` + +### Examples + +```jsx +import React from 'react'; +import { Pagination } from './Pagination'; + +const MyComponent = () => { + // Example pagination data (typically from an API) + const paginationData = { + data: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + // ... more items + ], + links: [ + { label: '1', url: '/api/items?page=1', active: true, page: 1 }, + { label: '2', url: '/api/items?page=2', active: false, page: 2 }, + { label: '3', url: '/api/items?page=3', active: false, page: 3 }, + ], + current_page: 1, + last_page: 3, + first_page_url: '/api/items?page=1', + last_page_url: '/api/items?page=3', + next_page_url: '/api/items?page=2', + prev_page_url: null, + path: '/api/items', + per_page: 10, + total: 25, + from: 1, + to: 10, + }; + + return ( +
+ {/* Basic Pagination */} + + + {/* Pagination with Info */} + + + {/* Centered Pagination with Outline Buttons */} + + + {/* Pagination using button elements instead of links */} + +
+ ); +}; + +export default MyComponent; +``` + +## Features + +### Responsive Design + +The pagination component is fully responsive and adapts to different screen sizes: + +- On mobile devices, elements stack vertically +- On larger screens, pagination controls are displayed horizontally +- Alignment options work across different breakpoints + +### Accessibility + +The component follows accessibility best practices: + +- Uses semantic `