1- import React , { useState , useEffect } from 'react' ;
1+ import React , { useMemo } from 'react' ;
2+ import PropTypes from 'prop-types' ;
23
34import './Pager.css' ;
45
6+ // Constants
7+ const PAGE_WINDOW = 2 ; // Pages to show before and after the current page
8+
9+ /**
10+ * Pagination component for search results - fully controlled by parent
11+ * @param {Object } props
12+ * @param {number } props.currentPage - Current active page number (1-based)
13+ * @param {number } props.resultCount - Total number of results across all pages
14+ * @param {number } props.resultsPerPage - Number of results displayed per page
15+ * @param {function } props.onPageChange - Callback function when page changes
16+ */
517export default function Pager ( props ) {
6-
7- let [ selectedPage , setSelectedPage ] = useState ( props . currentPage ) ;
8- let totalPages = Math . ceil ( props . resultCount / props . resultsPerPage ) ;
9-
10- useEffect ( _ => {
11- props . setCurrentPage ( selectedPage ) ;
12- } , [ selectedPage , props ] ) ;
13-
14- function goToNextPage ( ) {
15- setSelectedPage ( selectedPage + 1 ) ;
16- }
17-
18- function goToPreviousPage ( ) {
19- setSelectedPage ( selectedPage - 1 ) ;
18+ // Destructure props for cleaner code and proper dependency tracking
19+ const { currentPage, resultCount, resultsPerPage, onPageChange } = props ;
20+
21+ // Ensure currentPage is always an integer
22+ const page = parseInt ( currentPage ) || 1 ;
23+ const totalPages = Math . max ( 1 , Math . ceil ( resultCount / resultsPerPage ) ) ;
24+
25+ // Handler for changing the current page
26+ function handlePageChange ( pageNumber ) {
27+ // Convert to integer and clamp within valid range
28+ const newPage = Math . max ( 1 , Math . min ( totalPages , parseInt ( pageNumber ) || 1 ) ) ;
29+
30+ // Only update if actually changing page
31+ if ( newPage !== page ) {
32+ onPageChange ( newPage ) ;
33+ }
2034 }
2135
22- var i = 0 ;
23- var page_links = [ ] ;
24-
25- var minPage = 1 ;
26- var maxPage = totalPages ;
27-
28- if ( selectedPage - minPage > 2 ) {
29- minPage = selectedPage - 2 ;
36+ // Handler for next page button click
37+ function handleNextPage ( ) {
38+ if ( page < totalPages ) {
39+ handlePageChange ( page + 1 ) ;
40+ }
3041 }
3142
32- if ( maxPage - selectedPage > 2 ) {
33- maxPage = parseInt ( selectedPage ) + 2 ;
43+ // Handler for previous page button click
44+ function handlePreviousPage ( ) {
45+ if ( page > 1 ) {
46+ handlePageChange ( page - 1 ) ;
47+ }
3448 }
35-
36-
37- for ( i = minPage ; i <= maxPage ; i ++ ) {
38- if ( i === parseInt ( selectedPage ) ) {
39- page_links . push (
40- < li className = "page-item active" key = { i } >
41- < span className = "page-link" >
42- { i }
43- </ span >
44- </ li >
45- ) ;
46- } else {
47- page_links . push (
48- < li className = "page-item" key = { i } >
49- < button className = "page-link" id = { i } onClick = { ( e ) => setSelectedPage ( e . currentTarget . id ) } > { i } </ button >
50- </ li >
51- ) ;
49+
50+ // Calculate page range and memoize to avoid recalculation on every render
51+ const { minPage, maxPage } = useMemo ( ( ) => {
52+ let minPage = Math . max ( 1 , page - PAGE_WINDOW ) ;
53+ let maxPage = Math . min ( totalPages , page + PAGE_WINDOW ) ;
54+
55+ // Adjust range if we're near the start or end
56+ // This ensures we always show 5 pages if available
57+ if ( maxPage - minPage < PAGE_WINDOW * 2 ) {
58+ if ( page < totalPages / 2 ) {
59+ // Near start, expand end
60+ maxPage = Math . min ( totalPages , minPage + PAGE_WINDOW * 2 ) ;
61+ } else {
62+ // Near end, expand start
63+ minPage = Math . max ( 1 , maxPage - PAGE_WINDOW * 2 ) ;
64+ }
5265 }
66+
67+ return { minPage, maxPage } ;
68+ } , [ page , totalPages ] ) ;
69+
70+ // Generate page links array
71+ function renderPageLinks ( ) {
72+ const links = [ ] ;
73+
74+ for ( let i = minPage ; i <= maxPage ; i ++ ) {
75+ if ( i === page ) {
76+ links . push (
77+ < li className = "page-item active" key = { i } >
78+ < span className = "page-link" aria-current = "page" >
79+ { i }
80+ </ span >
81+ </ li >
82+ ) ;
83+ } else {
84+ links . push (
85+ < li className = "page-item" key = { i } >
86+ < button
87+ className = "page-link"
88+ onClick = { ( ) => handlePageChange ( i ) }
89+ aria-label = { `Go to page ${ i } ` } >
90+ { i }
91+ </ button >
92+ </ li >
93+ ) ;
94+ }
95+ }
96+ return links ;
5397 }
5498
55- var previousButton ;
56- if ( parseInt ( selectedPage ) === 1 ) {
57- previousButton = ( < li className = "page-item disabled" key = "prev" >
58- < span className = "page-link" > Previous</ span >
59- </ li > ) ;
60- } else {
61- previousButton = ( < li className = "page-item" key = "prev" onClick = { goToPreviousPage } >
62- < button className = "page-link" > Previous</ button >
63- </ li > ) ;
99+ // Create previous button component
100+ function renderPreviousButton ( ) {
101+ const isFirstPage = page === 1 ;
102+ return (
103+ < li className = { `page-item ${ isFirstPage ? 'disabled' : '' } ` } key = "prev" >
104+ { isFirstPage ? (
105+ < span className = "page-link" > Previous</ span >
106+ ) : (
107+ < button
108+ className = "page-link"
109+ onClick = { handlePreviousPage }
110+ aria-label = "Go to previous page" >
111+ Previous
112+ </ button >
113+ ) }
114+ </ li >
115+ ) ;
64116 }
65117
66- var nextButton ;
67- if ( parseInt ( selectedPage ) === totalPages ) {
68- nextButton = ( < li className = "page-item disabled" key = "next" >
69- < span className = "page-link" > Next</ span >
70- </ li > ) ;
71- } else {
72- nextButton = ( < li className = "page-item" key = "next" >
73- < button className = "page-link" onClick = { goToNextPage } > Next</ button >
74- </ li > ) ;
118+ // Create next button component
119+ function renderNextButton ( ) {
120+ const isLastPage = page === totalPages ;
121+ return (
122+ < li className = { `page-item ${ isLastPage ? 'disabled' : '' } ` } key = "next" >
123+ { isLastPage ? (
124+ < span className = "page-link" > Next</ span >
125+ ) : (
126+ < button
127+ className = "page-link"
128+ onClick = { handleNextPage }
129+ aria-label = "Go to next page" >
130+ Next
131+ </ button >
132+ ) }
133+ </ li >
134+ ) ;
75135 }
76136
77-
137+ // Handle case with no results
138+ if ( totalPages <= 0 ) {
139+ return null ; // No pagination needed when there are no results
140+ }
78141
79142 return (
80- < nav aria-label = "..." className = "pager" >
143+ < nav aria-label = "Search results pagination" className = "pager" >
81144 < ul className = "pagination item" >
82- { previousButton }
83- { page_links }
84- { nextButton }
145+ { renderPreviousButton ( ) }
146+ { renderPageLinks ( ) }
147+ { renderNextButton ( ) }
85148 </ ul >
86149 </ nav >
87150 ) ;
88-
89- }
151+ }
152+
153+ // PropTypes for better documentation and runtime type checking
154+ Pager . propTypes = {
155+ currentPage : PropTypes . number ,
156+ resultCount : PropTypes . number . isRequired ,
157+ resultsPerPage : PropTypes . number . isRequired ,
158+ onPageChange : PropTypes . func . isRequired
159+ } ;
160+
161+ // Default props
162+ Pager . defaultProps = {
163+ currentPage : 1
164+ } ;
0 commit comments