TypeScript · 5096 bytes Raw Blame History
1 import React from 'react';
2
3 interface PaginationProps {
4 currentPage: number;
5 totalItems: number;
6 itemsPerPage: number;
7 onPageChange: (page: number) => void;
8 onItemsPerPageChange: (itemsPerPage: number) => void;
9 itemsPerPageOptions?: number[];
10 }
11
12 export default function Pagination({
13 currentPage,
14 totalItems,
15 itemsPerPage,
16 onPageChange,
17 onItemsPerPageChange,
18 itemsPerPageOptions = [30, 40, 50],
19 }: PaginationProps) {
20 const totalPages = Math.ceil(totalItems / itemsPerPage);
21
22 // Don't show pagination if there's only one page or less
23 if (totalPages <= 1) {
24 return null;
25 }
26
27 const startItem = (currentPage - 1) * itemsPerPage + 1;
28 const endItem = Math.min(currentPage * itemsPerPage, totalItems);
29
30 // Generate page numbers to display
31 const getPageNumbers = () => {
32 const pages: (number | string)[] = [];
33 const maxVisiblePages = 7;
34
35 if (totalPages <= maxVisiblePages) {
36 // Show all pages if total is small
37 for (let i = 1; i <= totalPages; i++) {
38 pages.push(i);
39 }
40 } else {
41 // Always show first page
42 pages.push(1);
43
44 if (currentPage <= 4) {
45 // Near the beginning
46 for (let i = 2; i <= 5; i++) {
47 pages.push(i);
48 }
49 pages.push('...');
50 pages.push(totalPages);
51 } else if (currentPage >= totalPages - 3) {
52 // Near the end
53 pages.push('...');
54 for (let i = totalPages - 4; i <= totalPages; i++) {
55 pages.push(i);
56 }
57 } else {
58 // In the middle
59 pages.push('...');
60 for (let i = currentPage - 1; i <= currentPage + 1; i++) {
61 pages.push(i);
62 }
63 pages.push('...');
64 pages.push(totalPages);
65 }
66 }
67
68 return pages;
69 };
70
71 const pageNumbers = getPageNumbers();
72
73 return (
74 <div className="flex flex-col sm:flex-row items-center justify-between gap-4 py-6">
75 {/* Items per page selector */}
76 <div className="flex items-center gap-3">
77 <label htmlFor="items-per-page" className="text-gray-700 font-semibold">
78 Per page:
79 </label>
80 <select
81 id="items-per-page"
82 value={itemsPerPage}
83 onChange={(e) => onItemsPerPageChange(Number(e.target.value))}
84 className="px-3 py-2 border-2 border-gray-300 rounded bg-white text-gray-800 font-semibold hover:border-vmi-gold focus:border-vmi-gold focus:outline-none focus:ring-2 focus:ring-vmi-light-gold transition-colors"
85 >
86 {itemsPerPageOptions.map((option) => (
87 <option key={option} value={option}>
88 {option}
89 </option>
90 ))}
91 <option value={totalItems}>All ({totalItems})</option>
92 </select>
93 <span className="text-gray-600 text-sm">
94 Showing {startItem}-{endItem} of {totalItems}
95 </span>
96 </div>
97
98 {/* Page navigation */}
99 <div className="flex items-center gap-2">
100 {/* Previous button */}
101 <button
102 onClick={() => onPageChange(currentPage - 1)}
103 disabled={currentPage === 1}
104 className="px-4 py-2 border-2 border-gray-300 rounded font-semibold text-gray-700 hover:border-vmi-gold hover:bg-vmi-light-gold disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-gray-300 disabled:hover:bg-transparent transition-all"
105 aria-label="Previous page"
106 >
107 Prev
108 </button>
109
110 {/* Page numbers */}
111 <div className="flex gap-1">
112 {pageNumbers.map((page, index) => {
113 if (page === '...') {
114 return (
115 <span
116 key={`ellipsis-${index}`}
117 className="px-3 py-2 text-gray-500"
118 >
119 ...
120 </span>
121 );
122 }
123
124 const pageNum = page as number;
125 const isCurrentPage = pageNum === currentPage;
126
127 return (
128 <button
129 key={pageNum}
130 onClick={() => onPageChange(pageNum)}
131 className={`px-4 py-2 border-2 rounded font-semibold transition-all ${
132 isCurrentPage
133 ? 'bg-vmi-red text-white border-vmi-red'
134 : 'border-gray-300 text-gray-700 hover:border-vmi-gold hover:bg-vmi-light-gold'
135 }`}
136 aria-label={`Page ${pageNum}`}
137 aria-current={isCurrentPage ? 'page' : undefined}
138 >
139 {pageNum}
140 </button>
141 );
142 })}
143 </div>
144
145 {/* Next button */}
146 <button
147 onClick={() => onPageChange(currentPage + 1)}
148 disabled={currentPage === totalPages}
149 className="px-4 py-2 border-2 border-gray-300 rounded font-semibold text-gray-700 hover:border-vmi-gold hover:bg-vmi-light-gold disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-gray-300 disabled:hover:bg-transparent transition-all"
150 aria-label="Next page"
151 >
152 Next
153 </button>
154 </div>
155 </div>
156 );
157 }