├── .babelrc ├── .flowconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── dependabot.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ └── index.js.snap ├── index.js └── utils.js ├── dependabot.yml ├── docs └── examples.md ├── package.json ├── src ├── components.js ├── index.js └── utils.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": [ 4 | "babel-plugin-transform-react-jsx", 5 | "transform-flow-strip-types", 6 | "add-module-exports" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectError 11 | 12 | [strict] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support request 4 | url: https://stackoverflow.com/questions/tagged/gatsby 5 | about: Please ask and answer support requests on stack overflow 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | vrsion: 2 2 | allow: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | allow: 6 | - dependency-type: "production" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog][keep a changelog] and this project adheres to [Semantic Versioning][semantic versioning]. 6 | 7 | ## [Unreleased] 8 | 9 | --- 10 | 11 | ## [0.3.8] - 2021-04-01 12 | 13 | - Bump lodash to 4.17.21 for security fixes 14 | 15 | ## [0.3.7] - 2021-04-01 16 | 17 | ### Fixed 18 | 19 | - Change `peerDependencies` to `>=` for Gatsby 3 support 20 | 21 | --- 22 | 23 | 24 | 25 | [keep a changelog]: https://keepachangelog.com/ 26 | [semantic versioning]: https://semver.org/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gatsby Central 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Awesome Pagination for Gatsby 2 | --- 3 | 4 | A sensible approach to pagination for Gatsby sites. 5 | 6 | Please post questions on StackOverflow, only bug reports are accepted via GitHub. 7 | 8 | ## Contents 9 | 10 | * [QuickStart](#quick-start) 11 | * [Introduction](#introduction) 12 | * [Philosophy](#philosophy) 13 | * [Notes](#notes) 14 | 15 | ## Quick start 16 | 17 | ``` 18 | yarn add gatsby-awesome-pagination 19 | ``` 20 | 21 | Open `gatsby-node.js` and import: 22 | 23 | ```javascript 24 | import { paginate } from 'gatsby-awesome-pagination'; 25 | ``` 26 | 27 | Then, use `paginate()` like so: 28 | 29 | ```javascript 30 | exports.createPages = ({ actions, graphql }) => { 31 | const { createPage } = actions; 32 | 33 | // Fetch your items (blog posts, categories, etc). 34 | const blogPosts = doSomeMagic(); 35 | 36 | // Create your paginated pages 37 | paginate({ 38 | createPage, // The Gatsby `createPage` function 39 | items: blogPosts, // An array of objects 40 | itemsPerPage: 10, // How many items you want per page 41 | pathPrefix: '/blog', // Creates pages like `/blog`, `/blog/2`, etc 42 | component: path.resolve('...'), // Just like `createPage()` 43 | }) 44 | } 45 | ``` 46 | 47 | Now in your page query you can use the pagination context like so: 48 | 49 | ```javascript 50 | export const pageQuery = graphql` 51 | query ($skip: Int!, $limit: Int!) { 52 | allMarkdownRemark( 53 | sort: { fields: [frontmatter___date], order: DESC } 54 | skip: $skip // This was added by the plugin 55 | limit: $limit // This was added by the plugin 56 | ) { 57 | ... 58 | } 59 | } 60 | ` 61 | ``` 62 | 63 | Then inside your component, you can link to next / previous pages, and so on: 64 | 65 | ```javascript 66 | const BlogIndex = (props) => { 67 | return ( 68 |
69 | {data.allMarkdownRemark.edges.map(edge => )} 70 |
71 | {/* previousPageLink and nextPageLink were added by the plugin */ } 72 | Previous 73 | Next 74 |
75 |
76 | ) 77 | } 78 | ``` 79 | 80 | For a more detailed example, see [docs/examples.md](https://github.com/GatsbyCentral/gatsby-awesome-pagination/blob/master/docs/examples.md) 81 | 82 | ## Introduction 83 | 84 | Love Gatsby, wanna paginate. Sweet, that's exactly what this package is for. 85 | 86 | We differ from other pagination options as follows: 87 | 88 | * Don't abuse `context` to pass data into components 89 | * Pass only pagination context via `context` 90 | * Provide helpers for next / previous links 91 | 92 | There are 2 types of pagination. You have 80 blog posts and you want to show 93 | them 15 at a time on pages like `/blog`, `/blog/2`, `/blog/3`, etc. You do this 94 | with `paginate()`. Then on each blog post, you want to link to the previous and 95 | next blog posts. You do this with `createPagePerItem()`. 96 | 97 | ## Philosophy 98 | 99 | Why did we create this plugin? We felt that the other Gatsby pagination plugins 100 | were using an approach that goes against the principles of GraphQL. One of the 101 | advantages of GraphQL is to be able to decide what data you need right where you 102 | use that data. That's how Gatsby works with page queries. 103 | 104 | By putting all the data into `context`, the other pagination plugins break this. 105 | Now you need to decide what data you require for each page inside 106 | `gatsby-node.js` and not inside your page query. 107 | 108 | We also felt that there were some helpers missing. Generating links to the next 109 | and previous pages. 110 | 111 | This plugin aims to make it easy to paginate in Gatsby **properly**. No 112 | compromises. 113 | 114 | ## API 115 | 116 | Both `paginate()` and `createPagePerItem()` take a single argument, an object. 117 | They share the following keys (* = required): 118 | 119 | * `createPage`* - The `createPage` function from `exports.createPages` 120 | * `component`* - The value you would pass to `createPage()` as `component` [Gatsby docs here](https://www.gatsbyjs.org/docs/bound-action-creators/#createPage) 121 | * `items`* - An array of objects, the items you want to paginate over 122 | 123 | ### `paginate()` 124 | 125 | In addition to the arguments above, `paginate()` also supports: 126 | 127 | * `itemsPerPage`* - An integer, how many items should be displayed on each page 128 | * `itemsPerFirstPage` - An integer, how many items should be displayed on the **first** page 129 | * `pathPrefix`* - A (nonempty) string or string returning function, the path (eg `/blog`) to which `/2`, `/3`, etc will be added 130 | * `context` - A base context object which is extended with the pagination context values 131 | 132 | Example: 133 | 134 | ```javascript 135 | paginate({ 136 | createPage: boundActionCreators.createPage, 137 | component: path.resolve('./src/templates/blog-index.js'), 138 | items: blogPosts, 139 | itemsPerPage: 15, 140 | itemsPerFirstPage: 3, 141 | pathPrefix: '/blog' 142 | }) 143 | ``` 144 | 145 | Each page's `context` automatically receives the following values: 146 | 147 | * `pageNumber` - The page number (starting from 0) 148 | * `humanPageNumber` - The page number (starting from 1) for human consumption 149 | * `skip` - The $skip you can use in a GraphQL query 150 | * `limit` - The $limit you can use in a GraphQL query 151 | * `numberOfPages` - The total number of pages 152 | * `previousPagePath` - The path to the previous page or `undefined` 153 | * `nextPagePath` - The path to the next page or `undefined` 154 | 155 | #### pathPrefix() 156 | 157 | For more advanced use cases, you can supply a function to `pathPrefix`. This 158 | function will receive a single object as its only argument, that object will 159 | contain `pageNumber` and `numberOfPages`, both integers. 160 | 161 | A simple example implementation could be: 162 | 163 | ```javascript 164 | const pathPrefix = ({ pageNumber, numberOfPages }) => 165 | pageNumber === 0 ? '/blog' : '/blog/page' 166 | ``` 167 | 168 | This example produces pages like `/blog`, `/blog/page/2`, `/blog/page/3`, etc. 169 | 170 | ### `createPagePerItem()` 171 | 172 | WARNING: This API is under active development and will probably change. USE WITH 173 | CAUTION. 174 | 175 | In addition to the arguments above, `createPagePerItem()` also accepts: 176 | 177 | * `itemToPath`* - A function that takes one object from `items` and returns the 178 | `path` for this `item` 179 | * `itemToId`* - A function that takes one object from `items` and returns the 180 | item's ID 181 | 182 | **NOTE**: Both `itemToPath` and `itemToId` also accept a string with the path to 183 | the value, for example `node.frontmatter.permalink` or `node.id`. 184 | 185 | **NOTE**: If an individual `item` has a property called `context`, and that 186 | property is an object, then it's own properties will be added to the page's 187 | `context` for that item. 188 | 189 | Example: 190 | 191 | ```javascript 192 | createPagePerItem({ 193 | createPage: boundActionCreators.createPage, 194 | component: path.resolve('./src/templates/blog-post.js'), 195 | items: blogPosts, 196 | itemToPath: 'node.frontmatter.permalink', 197 | itemToId: 'node.id' 198 | }) 199 | ``` 200 | 201 | Each page's `context` automatically receives the following values: 202 | 203 | * `previousPagePath` - The path to the previous page or `undefined` 204 | * `previousItem` - A copy of the previous element from `items` 205 | * `previousPageId` - The ID of the previous page 206 | * `nextPagePath` - The path to the next page or `undefined` 207 | * `nextItem` - A copy of the next element from `items` 208 | * `nextPageId` - The ID of the next page 209 | 210 | ## Notes 211 | 212 | ### Flow 213 | 214 | This plugin is written using [flow](https://flow.org/). There are some 215 | limitations when using flow and [lodash](https://lodash.com/). Specifically 216 | [this issue](https://github.com/facebook/flow/issues/34). In many cases we use 217 | `$FlowExpectError` and explicitly define the type of something to workaround. A 218 | more elegant solution does not currently seem to exist. Any input on improving 219 | the typing is greatly appreciated in the plugin's issues. 220 | 221 | ### Contributors 222 | 223 | Thanks to the following for their contributions: 224 | 225 | * https://github.com/Pyrax 226 | * https://github.com/JesseSingleton 227 | * https://github.com/silvenon 228 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createPagePerItem() Calls createPage() with the correct params 1`] = ` 4 | [MockFunction] { 5 | "calls": Array [ 6 | Array [ 7 | Object { 8 | "component": "/path/to/some/template.js", 9 | "context": Object { 10 | "nextItem": Object { 11 | "id": 1, 12 | "path": "/blog/post-2", 13 | "title": "Post number 1", 14 | }, 15 | "nextPageId": 1, 16 | "nextPagePath": "/blog/post-2", 17 | "pageId": 0, 18 | "previousItem": undefined, 19 | "previousPageId": "", 20 | "previousPagePath": "", 21 | }, 22 | "path": "/blog/post-1", 23 | }, 24 | ], 25 | Array [ 26 | Object { 27 | "component": "/path/to/some/template.js", 28 | "context": Object { 29 | "nextItem": Object { 30 | "id": 2, 31 | "path": "/blog/post-3", 32 | "title": "Post number 2", 33 | }, 34 | "nextPageId": 2, 35 | "nextPagePath": "/blog/post-3", 36 | "pageId": 1, 37 | "previousItem": Object { 38 | "id": 0, 39 | "path": "/blog/post-1", 40 | "title": "Post number 0", 41 | }, 42 | "previousPageId": "", 43 | "previousPagePath": "/blog/post-1", 44 | }, 45 | "path": "/blog/post-2", 46 | }, 47 | ], 48 | Array [ 49 | Object { 50 | "component": "/path/to/some/template.js", 51 | "context": Object { 52 | "nextItem": Object { 53 | "id": 3, 54 | "path": "/blog/post-4", 55 | "title": "Post number 3", 56 | }, 57 | "nextPageId": 3, 58 | "nextPagePath": "/blog/post-4", 59 | "pageId": 2, 60 | "previousItem": Object { 61 | "id": 1, 62 | "path": "/blog/post-2", 63 | "title": "Post number 1", 64 | }, 65 | "previousPageId": 1, 66 | "previousPagePath": "/blog/post-2", 67 | }, 68 | "path": "/blog/post-3", 69 | }, 70 | ], 71 | Array [ 72 | Object { 73 | "component": "/path/to/some/template.js", 74 | "context": Object { 75 | "nextItem": Object { 76 | "id": 4, 77 | "path": "/blog/post-5", 78 | "title": "Post number 4", 79 | }, 80 | "nextPageId": 4, 81 | "nextPagePath": "/blog/post-5", 82 | "pageId": 3, 83 | "previousItem": Object { 84 | "id": 2, 85 | "path": "/blog/post-3", 86 | "title": "Post number 2", 87 | }, 88 | "previousPageId": 2, 89 | "previousPagePath": "/blog/post-3", 90 | }, 91 | "path": "/blog/post-4", 92 | }, 93 | ], 94 | Array [ 95 | Object { 96 | "component": "/path/to/some/template.js", 97 | "context": Object { 98 | "nextItem": Object { 99 | "id": 5, 100 | "path": "/blog/post-6", 101 | "title": "Post number 5", 102 | }, 103 | "nextPageId": 5, 104 | "nextPagePath": "/blog/post-6", 105 | "pageId": 4, 106 | "previousItem": Object { 107 | "id": 3, 108 | "path": "/blog/post-4", 109 | "title": "Post number 3", 110 | }, 111 | "previousPageId": 3, 112 | "previousPagePath": "/blog/post-4", 113 | }, 114 | "path": "/blog/post-5", 115 | }, 116 | ], 117 | Array [ 118 | Object { 119 | "component": "/path/to/some/template.js", 120 | "context": Object { 121 | "nextItem": Object { 122 | "id": 6, 123 | "path": "/blog/post-7", 124 | "title": "Post number 6", 125 | }, 126 | "nextPageId": 6, 127 | "nextPagePath": "/blog/post-7", 128 | "pageId": 5, 129 | "previousItem": Object { 130 | "id": 4, 131 | "path": "/blog/post-5", 132 | "title": "Post number 4", 133 | }, 134 | "previousPageId": 4, 135 | "previousPagePath": "/blog/post-5", 136 | }, 137 | "path": "/blog/post-6", 138 | }, 139 | ], 140 | Array [ 141 | Object { 142 | "component": "/path/to/some/template.js", 143 | "context": Object { 144 | "nextItem": Object { 145 | "id": 7, 146 | "path": "/blog/post-8", 147 | "title": "Post number 7", 148 | }, 149 | "nextPageId": 7, 150 | "nextPagePath": "/blog/post-8", 151 | "pageId": 6, 152 | "previousItem": Object { 153 | "id": 5, 154 | "path": "/blog/post-6", 155 | "title": "Post number 5", 156 | }, 157 | "previousPageId": 5, 158 | "previousPagePath": "/blog/post-6", 159 | }, 160 | "path": "/blog/post-7", 161 | }, 162 | ], 163 | Array [ 164 | Object { 165 | "component": "/path/to/some/template.js", 166 | "context": Object { 167 | "nextItem": Object { 168 | "id": 8, 169 | "path": "/blog/post-9", 170 | "title": "Post number 8", 171 | }, 172 | "nextPageId": 8, 173 | "nextPagePath": "/blog/post-9", 174 | "pageId": 7, 175 | "previousItem": Object { 176 | "id": 6, 177 | "path": "/blog/post-7", 178 | "title": "Post number 6", 179 | }, 180 | "previousPageId": 6, 181 | "previousPagePath": "/blog/post-7", 182 | }, 183 | "path": "/blog/post-8", 184 | }, 185 | ], 186 | Array [ 187 | Object { 188 | "component": "/path/to/some/template.js", 189 | "context": Object { 190 | "nextItem": Object { 191 | "id": 9, 192 | "path": "/blog/post-10", 193 | "title": "Post number 9", 194 | }, 195 | "nextPageId": 9, 196 | "nextPagePath": "/blog/post-10", 197 | "pageId": 8, 198 | "previousItem": Object { 199 | "id": 7, 200 | "path": "/blog/post-8", 201 | "title": "Post number 7", 202 | }, 203 | "previousPageId": 7, 204 | "previousPagePath": "/blog/post-8", 205 | }, 206 | "path": "/blog/post-9", 207 | }, 208 | ], 209 | Array [ 210 | Object { 211 | "component": "/path/to/some/template.js", 212 | "context": Object { 213 | "nextItem": Object { 214 | "id": 10, 215 | "path": "/blog/post-11", 216 | "title": "Post number 10", 217 | }, 218 | "nextPageId": 10, 219 | "nextPagePath": "/blog/post-11", 220 | "pageId": 9, 221 | "previousItem": Object { 222 | "id": 8, 223 | "path": "/blog/post-9", 224 | "title": "Post number 8", 225 | }, 226 | "previousPageId": 8, 227 | "previousPagePath": "/blog/post-9", 228 | }, 229 | "path": "/blog/post-10", 230 | }, 231 | ], 232 | Array [ 233 | Object { 234 | "component": "/path/to/some/template.js", 235 | "context": Object { 236 | "nextItem": Object { 237 | "id": 11, 238 | "path": "/blog/post-12", 239 | "title": "Post number 11", 240 | }, 241 | "nextPageId": 11, 242 | "nextPagePath": "/blog/post-12", 243 | "pageId": 10, 244 | "previousItem": Object { 245 | "id": 9, 246 | "path": "/blog/post-10", 247 | "title": "Post number 9", 248 | }, 249 | "previousPageId": 9, 250 | "previousPagePath": "/blog/post-10", 251 | }, 252 | "path": "/blog/post-11", 253 | }, 254 | ], 255 | Array [ 256 | Object { 257 | "component": "/path/to/some/template.js", 258 | "context": Object { 259 | "nextItem": Object { 260 | "id": 12, 261 | "path": "/blog/post-13", 262 | "title": "Post number 12", 263 | }, 264 | "nextPageId": 12, 265 | "nextPagePath": "/blog/post-13", 266 | "pageId": 11, 267 | "previousItem": Object { 268 | "id": 10, 269 | "path": "/blog/post-11", 270 | "title": "Post number 10", 271 | }, 272 | "previousPageId": 10, 273 | "previousPagePath": "/blog/post-11", 274 | }, 275 | "path": "/blog/post-12", 276 | }, 277 | ], 278 | Array [ 279 | Object { 280 | "component": "/path/to/some/template.js", 281 | "context": Object { 282 | "nextItem": Object { 283 | "id": 13, 284 | "path": "/blog/post-14", 285 | "title": "Post number 13", 286 | }, 287 | "nextPageId": 13, 288 | "nextPagePath": "/blog/post-14", 289 | "pageId": 12, 290 | "previousItem": Object { 291 | "id": 11, 292 | "path": "/blog/post-12", 293 | "title": "Post number 11", 294 | }, 295 | "previousPageId": 11, 296 | "previousPagePath": "/blog/post-12", 297 | }, 298 | "path": "/blog/post-13", 299 | }, 300 | ], 301 | Array [ 302 | Object { 303 | "component": "/path/to/some/template.js", 304 | "context": Object { 305 | "nextItem": Object { 306 | "id": 14, 307 | "path": "/blog/post-15", 308 | "title": "Post number 14", 309 | }, 310 | "nextPageId": 14, 311 | "nextPagePath": "/blog/post-15", 312 | "pageId": 13, 313 | "previousItem": Object { 314 | "id": 12, 315 | "path": "/blog/post-13", 316 | "title": "Post number 12", 317 | }, 318 | "previousPageId": 12, 319 | "previousPagePath": "/blog/post-13", 320 | }, 321 | "path": "/blog/post-14", 322 | }, 323 | ], 324 | Array [ 325 | Object { 326 | "component": "/path/to/some/template.js", 327 | "context": Object { 328 | "nextItem": Object { 329 | "id": 15, 330 | "path": "/blog/post-16", 331 | "title": "Post number 15", 332 | }, 333 | "nextPageId": 15, 334 | "nextPagePath": "/blog/post-16", 335 | "pageId": 14, 336 | "previousItem": Object { 337 | "id": 13, 338 | "path": "/blog/post-14", 339 | "title": "Post number 13", 340 | }, 341 | "previousPageId": 13, 342 | "previousPagePath": "/blog/post-14", 343 | }, 344 | "path": "/blog/post-15", 345 | }, 346 | ], 347 | Array [ 348 | Object { 349 | "component": "/path/to/some/template.js", 350 | "context": Object { 351 | "nextItem": Object { 352 | "id": 16, 353 | "path": "/blog/post-17", 354 | "title": "Post number 16", 355 | }, 356 | "nextPageId": 16, 357 | "nextPagePath": "/blog/post-17", 358 | "pageId": 15, 359 | "previousItem": Object { 360 | "id": 14, 361 | "path": "/blog/post-15", 362 | "title": "Post number 14", 363 | }, 364 | "previousPageId": 14, 365 | "previousPagePath": "/blog/post-15", 366 | }, 367 | "path": "/blog/post-16", 368 | }, 369 | ], 370 | Array [ 371 | Object { 372 | "component": "/path/to/some/template.js", 373 | "context": Object { 374 | "nextItem": Object { 375 | "id": 17, 376 | "path": "/blog/post-18", 377 | "title": "Post number 17", 378 | }, 379 | "nextPageId": 17, 380 | "nextPagePath": "/blog/post-18", 381 | "pageId": 16, 382 | "previousItem": Object { 383 | "id": 15, 384 | "path": "/blog/post-16", 385 | "title": "Post number 15", 386 | }, 387 | "previousPageId": 15, 388 | "previousPagePath": "/blog/post-16", 389 | }, 390 | "path": "/blog/post-17", 391 | }, 392 | ], 393 | Array [ 394 | Object { 395 | "component": "/path/to/some/template.js", 396 | "context": Object { 397 | "nextItem": Object { 398 | "id": 18, 399 | "path": "/blog/post-19", 400 | "title": "Post number 18", 401 | }, 402 | "nextPageId": 18, 403 | "nextPagePath": "/blog/post-19", 404 | "pageId": 17, 405 | "previousItem": Object { 406 | "id": 16, 407 | "path": "/blog/post-17", 408 | "title": "Post number 16", 409 | }, 410 | "previousPageId": 16, 411 | "previousPagePath": "/blog/post-17", 412 | }, 413 | "path": "/blog/post-18", 414 | }, 415 | ], 416 | Array [ 417 | Object { 418 | "component": "/path/to/some/template.js", 419 | "context": Object { 420 | "nextItem": Object { 421 | "id": 19, 422 | "path": "/blog/post-20", 423 | "title": "Post number 19", 424 | }, 425 | "nextPageId": 19, 426 | "nextPagePath": "/blog/post-20", 427 | "pageId": 18, 428 | "previousItem": Object { 429 | "id": 17, 430 | "path": "/blog/post-18", 431 | "title": "Post number 17", 432 | }, 433 | "previousPageId": 17, 434 | "previousPagePath": "/blog/post-18", 435 | }, 436 | "path": "/blog/post-19", 437 | }, 438 | ], 439 | Array [ 440 | Object { 441 | "component": "/path/to/some/template.js", 442 | "context": Object { 443 | "nextItem": Object { 444 | "id": 20, 445 | "path": "/blog/post-21", 446 | "title": "Post number 20", 447 | }, 448 | "nextPageId": 20, 449 | "nextPagePath": "/blog/post-21", 450 | "pageId": 19, 451 | "previousItem": Object { 452 | "id": 18, 453 | "path": "/blog/post-19", 454 | "title": "Post number 18", 455 | }, 456 | "previousPageId": 18, 457 | "previousPagePath": "/blog/post-19", 458 | }, 459 | "path": "/blog/post-20", 460 | }, 461 | ], 462 | Array [ 463 | Object { 464 | "component": "/path/to/some/template.js", 465 | "context": Object { 466 | "nextItem": Object { 467 | "id": 21, 468 | "path": "/blog/post-22", 469 | "title": "Post number 21", 470 | }, 471 | "nextPageId": 21, 472 | "nextPagePath": "/blog/post-22", 473 | "pageId": 20, 474 | "previousItem": Object { 475 | "id": 19, 476 | "path": "/blog/post-20", 477 | "title": "Post number 19", 478 | }, 479 | "previousPageId": 19, 480 | "previousPagePath": "/blog/post-20", 481 | }, 482 | "path": "/blog/post-21", 483 | }, 484 | ], 485 | Array [ 486 | Object { 487 | "component": "/path/to/some/template.js", 488 | "context": Object { 489 | "nextItem": Object { 490 | "id": 22, 491 | "path": "/blog/post-23", 492 | "title": "Post number 22", 493 | }, 494 | "nextPageId": 22, 495 | "nextPagePath": "/blog/post-23", 496 | "pageId": 21, 497 | "previousItem": Object { 498 | "id": 20, 499 | "path": "/blog/post-21", 500 | "title": "Post number 20", 501 | }, 502 | "previousPageId": 20, 503 | "previousPagePath": "/blog/post-21", 504 | }, 505 | "path": "/blog/post-22", 506 | }, 507 | ], 508 | Array [ 509 | Object { 510 | "component": "/path/to/some/template.js", 511 | "context": Object { 512 | "nextItem": Object { 513 | "id": 23, 514 | "path": "/blog/post-24", 515 | "title": "Post number 23", 516 | }, 517 | "nextPageId": 23, 518 | "nextPagePath": "/blog/post-24", 519 | "pageId": 22, 520 | "previousItem": Object { 521 | "id": 21, 522 | "path": "/blog/post-22", 523 | "title": "Post number 21", 524 | }, 525 | "previousPageId": 21, 526 | "previousPagePath": "/blog/post-22", 527 | }, 528 | "path": "/blog/post-23", 529 | }, 530 | ], 531 | Array [ 532 | Object { 533 | "component": "/path/to/some/template.js", 534 | "context": Object { 535 | "nextItem": Object { 536 | "id": 24, 537 | "path": "/blog/post-25", 538 | "title": "Post number 24", 539 | }, 540 | "nextPageId": 24, 541 | "nextPagePath": "/blog/post-25", 542 | "pageId": 23, 543 | "previousItem": Object { 544 | "id": 22, 545 | "path": "/blog/post-23", 546 | "title": "Post number 22", 547 | }, 548 | "previousPageId": 22, 549 | "previousPagePath": "/blog/post-23", 550 | }, 551 | "path": "/blog/post-24", 552 | }, 553 | ], 554 | Array [ 555 | Object { 556 | "component": "/path/to/some/template.js", 557 | "context": Object { 558 | "nextItem": Object { 559 | "id": 25, 560 | "path": "/blog/post-26", 561 | "title": "Post number 25", 562 | }, 563 | "nextPageId": 25, 564 | "nextPagePath": "/blog/post-26", 565 | "pageId": 24, 566 | "previousItem": Object { 567 | "id": 23, 568 | "path": "/blog/post-24", 569 | "title": "Post number 23", 570 | }, 571 | "previousPageId": 23, 572 | "previousPagePath": "/blog/post-24", 573 | }, 574 | "path": "/blog/post-25", 575 | }, 576 | ], 577 | Array [ 578 | Object { 579 | "component": "/path/to/some/template.js", 580 | "context": Object { 581 | "nextItem": Object { 582 | "id": 26, 583 | "path": "/blog/post-27", 584 | "title": "Post number 26", 585 | }, 586 | "nextPageId": 26, 587 | "nextPagePath": "/blog/post-27", 588 | "pageId": 25, 589 | "previousItem": Object { 590 | "id": 24, 591 | "path": "/blog/post-25", 592 | "title": "Post number 24", 593 | }, 594 | "previousPageId": 24, 595 | "previousPagePath": "/blog/post-25", 596 | }, 597 | "path": "/blog/post-26", 598 | }, 599 | ], 600 | Array [ 601 | Object { 602 | "component": "/path/to/some/template.js", 603 | "context": Object { 604 | "nextItem": Object { 605 | "id": 27, 606 | "path": "/blog/post-28", 607 | "title": "Post number 27", 608 | }, 609 | "nextPageId": 27, 610 | "nextPagePath": "/blog/post-28", 611 | "pageId": 26, 612 | "previousItem": Object { 613 | "id": 25, 614 | "path": "/blog/post-26", 615 | "title": "Post number 25", 616 | }, 617 | "previousPageId": 25, 618 | "previousPagePath": "/blog/post-26", 619 | }, 620 | "path": "/blog/post-27", 621 | }, 622 | ], 623 | Array [ 624 | Object { 625 | "component": "/path/to/some/template.js", 626 | "context": Object { 627 | "nextItem": Object { 628 | "id": 28, 629 | "path": "/blog/post-29", 630 | "title": "Post number 28", 631 | }, 632 | "nextPageId": 28, 633 | "nextPagePath": "/blog/post-29", 634 | "pageId": 27, 635 | "previousItem": Object { 636 | "id": 26, 637 | "path": "/blog/post-27", 638 | "title": "Post number 26", 639 | }, 640 | "previousPageId": 26, 641 | "previousPagePath": "/blog/post-27", 642 | }, 643 | "path": "/blog/post-28", 644 | }, 645 | ], 646 | Array [ 647 | Object { 648 | "component": "/path/to/some/template.js", 649 | "context": Object { 650 | "nextItem": Object { 651 | "id": 29, 652 | "path": "/blog/post-30", 653 | "title": "Post number 29", 654 | }, 655 | "nextPageId": 29, 656 | "nextPagePath": "/blog/post-30", 657 | "pageId": 28, 658 | "previousItem": Object { 659 | "id": 27, 660 | "path": "/blog/post-28", 661 | "title": "Post number 27", 662 | }, 663 | "previousPageId": 27, 664 | "previousPagePath": "/blog/post-28", 665 | }, 666 | "path": "/blog/post-29", 667 | }, 668 | ], 669 | Array [ 670 | Object { 671 | "component": "/path/to/some/template.js", 672 | "context": Object { 673 | "nextItem": Object { 674 | "id": 30, 675 | "path": "/blog/post-31", 676 | "title": "Post number 30", 677 | }, 678 | "nextPageId": 30, 679 | "nextPagePath": "/blog/post-31", 680 | "pageId": 29, 681 | "previousItem": Object { 682 | "id": 28, 683 | "path": "/blog/post-29", 684 | "title": "Post number 28", 685 | }, 686 | "previousPageId": 28, 687 | "previousPagePath": "/blog/post-29", 688 | }, 689 | "path": "/blog/post-30", 690 | }, 691 | ], 692 | Array [ 693 | Object { 694 | "component": "/path/to/some/template.js", 695 | "context": Object { 696 | "nextItem": Object { 697 | "id": 31, 698 | "path": "/blog/post-32", 699 | "title": "Post number 31", 700 | }, 701 | "nextPageId": 31, 702 | "nextPagePath": "/blog/post-32", 703 | "pageId": 30, 704 | "previousItem": Object { 705 | "id": 29, 706 | "path": "/blog/post-30", 707 | "title": "Post number 29", 708 | }, 709 | "previousPageId": 29, 710 | "previousPagePath": "/blog/post-30", 711 | }, 712 | "path": "/blog/post-31", 713 | }, 714 | ], 715 | Array [ 716 | Object { 717 | "component": "/path/to/some/template.js", 718 | "context": Object { 719 | "nextItem": Object { 720 | "id": 32, 721 | "path": "/blog/post-33", 722 | "title": "Post number 32", 723 | }, 724 | "nextPageId": 32, 725 | "nextPagePath": "/blog/post-33", 726 | "pageId": 31, 727 | "previousItem": Object { 728 | "id": 30, 729 | "path": "/blog/post-31", 730 | "title": "Post number 30", 731 | }, 732 | "previousPageId": 30, 733 | "previousPagePath": "/blog/post-31", 734 | }, 735 | "path": "/blog/post-32", 736 | }, 737 | ], 738 | Array [ 739 | Object { 740 | "component": "/path/to/some/template.js", 741 | "context": Object { 742 | "nextItem": Object { 743 | "id": 33, 744 | "path": "/blog/post-34", 745 | "title": "Post number 33", 746 | }, 747 | "nextPageId": 33, 748 | "nextPagePath": "/blog/post-34", 749 | "pageId": 32, 750 | "previousItem": Object { 751 | "id": 31, 752 | "path": "/blog/post-32", 753 | "title": "Post number 31", 754 | }, 755 | "previousPageId": 31, 756 | "previousPagePath": "/blog/post-32", 757 | }, 758 | "path": "/blog/post-33", 759 | }, 760 | ], 761 | Array [ 762 | Object { 763 | "component": "/path/to/some/template.js", 764 | "context": Object { 765 | "nextItem": Object { 766 | "id": 34, 767 | "path": "/blog/post-35", 768 | "title": "Post number 34", 769 | }, 770 | "nextPageId": 34, 771 | "nextPagePath": "/blog/post-35", 772 | "pageId": 33, 773 | "previousItem": Object { 774 | "id": 32, 775 | "path": "/blog/post-33", 776 | "title": "Post number 32", 777 | }, 778 | "previousPageId": 32, 779 | "previousPagePath": "/blog/post-33", 780 | }, 781 | "path": "/blog/post-34", 782 | }, 783 | ], 784 | Array [ 785 | Object { 786 | "component": "/path/to/some/template.js", 787 | "context": Object { 788 | "nextItem": Object { 789 | "id": 35, 790 | "path": "/blog/post-36", 791 | "title": "Post number 35", 792 | }, 793 | "nextPageId": 35, 794 | "nextPagePath": "/blog/post-36", 795 | "pageId": 34, 796 | "previousItem": Object { 797 | "id": 33, 798 | "path": "/blog/post-34", 799 | "title": "Post number 33", 800 | }, 801 | "previousPageId": 33, 802 | "previousPagePath": "/blog/post-34", 803 | }, 804 | "path": "/blog/post-35", 805 | }, 806 | ], 807 | Array [ 808 | Object { 809 | "component": "/path/to/some/template.js", 810 | "context": Object { 811 | "nextItem": Object { 812 | "id": 36, 813 | "path": "/blog/post-37", 814 | "title": "Post number 36", 815 | }, 816 | "nextPageId": 36, 817 | "nextPagePath": "/blog/post-37", 818 | "pageId": 35, 819 | "previousItem": Object { 820 | "id": 34, 821 | "path": "/blog/post-35", 822 | "title": "Post number 34", 823 | }, 824 | "previousPageId": 34, 825 | "previousPagePath": "/blog/post-35", 826 | }, 827 | "path": "/blog/post-36", 828 | }, 829 | ], 830 | Array [ 831 | Object { 832 | "component": "/path/to/some/template.js", 833 | "context": Object { 834 | "nextItem": Object { 835 | "id": 37, 836 | "path": "/blog/post-38", 837 | "title": "Post number 37", 838 | }, 839 | "nextPageId": 37, 840 | "nextPagePath": "/blog/post-38", 841 | "pageId": 36, 842 | "previousItem": Object { 843 | "id": 35, 844 | "path": "/blog/post-36", 845 | "title": "Post number 35", 846 | }, 847 | "previousPageId": 35, 848 | "previousPagePath": "/blog/post-36", 849 | }, 850 | "path": "/blog/post-37", 851 | }, 852 | ], 853 | Array [ 854 | Object { 855 | "component": "/path/to/some/template.js", 856 | "context": Object { 857 | "nextItem": Object { 858 | "id": 38, 859 | "path": "/blog/post-39", 860 | "title": "Post number 38", 861 | }, 862 | "nextPageId": 38, 863 | "nextPagePath": "/blog/post-39", 864 | "pageId": 37, 865 | "previousItem": Object { 866 | "id": 36, 867 | "path": "/blog/post-37", 868 | "title": "Post number 36", 869 | }, 870 | "previousPageId": 36, 871 | "previousPagePath": "/blog/post-37", 872 | }, 873 | "path": "/blog/post-38", 874 | }, 875 | ], 876 | Array [ 877 | Object { 878 | "component": "/path/to/some/template.js", 879 | "context": Object { 880 | "nextItem": Object { 881 | "id": 39, 882 | "path": "/blog/post-40", 883 | "title": "Post number 39", 884 | }, 885 | "nextPageId": 39, 886 | "nextPagePath": "/blog/post-40", 887 | "pageId": 38, 888 | "previousItem": Object { 889 | "id": 37, 890 | "path": "/blog/post-38", 891 | "title": "Post number 37", 892 | }, 893 | "previousPageId": 37, 894 | "previousPagePath": "/blog/post-38", 895 | }, 896 | "path": "/blog/post-39", 897 | }, 898 | ], 899 | Array [ 900 | Object { 901 | "component": "/path/to/some/template.js", 902 | "context": Object { 903 | "nextItem": Object { 904 | "id": 40, 905 | "path": "/blog/post-41", 906 | "title": "Post number 40", 907 | }, 908 | "nextPageId": 40, 909 | "nextPagePath": "/blog/post-41", 910 | "pageId": 39, 911 | "previousItem": Object { 912 | "id": 38, 913 | "path": "/blog/post-39", 914 | "title": "Post number 38", 915 | }, 916 | "previousPageId": 38, 917 | "previousPagePath": "/blog/post-39", 918 | }, 919 | "path": "/blog/post-40", 920 | }, 921 | ], 922 | Array [ 923 | Object { 924 | "component": "/path/to/some/template.js", 925 | "context": Object { 926 | "nextItem": Object { 927 | "id": 41, 928 | "path": "/blog/post-42", 929 | "title": "Post number 41", 930 | }, 931 | "nextPageId": 41, 932 | "nextPagePath": "/blog/post-42", 933 | "pageId": 40, 934 | "previousItem": Object { 935 | "id": 39, 936 | "path": "/blog/post-40", 937 | "title": "Post number 39", 938 | }, 939 | "previousPageId": 39, 940 | "previousPagePath": "/blog/post-40", 941 | }, 942 | "path": "/blog/post-41", 943 | }, 944 | ], 945 | Array [ 946 | Object { 947 | "component": "/path/to/some/template.js", 948 | "context": Object { 949 | "nextItem": Object { 950 | "id": 42, 951 | "path": "/blog/post-43", 952 | "title": "Post number 42", 953 | }, 954 | "nextPageId": 42, 955 | "nextPagePath": "/blog/post-43", 956 | "pageId": 41, 957 | "previousItem": Object { 958 | "id": 40, 959 | "path": "/blog/post-41", 960 | "title": "Post number 40", 961 | }, 962 | "previousPageId": 40, 963 | "previousPagePath": "/blog/post-41", 964 | }, 965 | "path": "/blog/post-42", 966 | }, 967 | ], 968 | Array [ 969 | Object { 970 | "component": "/path/to/some/template.js", 971 | "context": Object { 972 | "nextItem": undefined, 973 | "nextPageId": "", 974 | "nextPagePath": "", 975 | "pageId": 42, 976 | "previousItem": Object { 977 | "id": 41, 978 | "path": "/blog/post-42", 979 | "title": "Post number 41", 980 | }, 981 | "previousPageId": 41, 982 | "previousPagePath": "/blog/post-42", 983 | }, 984 | "path": "/blog/post-43", 985 | }, 986 | ], 987 | ], 988 | "results": Array [ 989 | Object { 990 | "isThrow": false, 991 | "value": undefined, 992 | }, 993 | Object { 994 | "isThrow": false, 995 | "value": undefined, 996 | }, 997 | Object { 998 | "isThrow": false, 999 | "value": undefined, 1000 | }, 1001 | Object { 1002 | "isThrow": false, 1003 | "value": undefined, 1004 | }, 1005 | Object { 1006 | "isThrow": false, 1007 | "value": undefined, 1008 | }, 1009 | Object { 1010 | "isThrow": false, 1011 | "value": undefined, 1012 | }, 1013 | Object { 1014 | "isThrow": false, 1015 | "value": undefined, 1016 | }, 1017 | Object { 1018 | "isThrow": false, 1019 | "value": undefined, 1020 | }, 1021 | Object { 1022 | "isThrow": false, 1023 | "value": undefined, 1024 | }, 1025 | Object { 1026 | "isThrow": false, 1027 | "value": undefined, 1028 | }, 1029 | Object { 1030 | "isThrow": false, 1031 | "value": undefined, 1032 | }, 1033 | Object { 1034 | "isThrow": false, 1035 | "value": undefined, 1036 | }, 1037 | Object { 1038 | "isThrow": false, 1039 | "value": undefined, 1040 | }, 1041 | Object { 1042 | "isThrow": false, 1043 | "value": undefined, 1044 | }, 1045 | Object { 1046 | "isThrow": false, 1047 | "value": undefined, 1048 | }, 1049 | Object { 1050 | "isThrow": false, 1051 | "value": undefined, 1052 | }, 1053 | Object { 1054 | "isThrow": false, 1055 | "value": undefined, 1056 | }, 1057 | Object { 1058 | "isThrow": false, 1059 | "value": undefined, 1060 | }, 1061 | Object { 1062 | "isThrow": false, 1063 | "value": undefined, 1064 | }, 1065 | Object { 1066 | "isThrow": false, 1067 | "value": undefined, 1068 | }, 1069 | Object { 1070 | "isThrow": false, 1071 | "value": undefined, 1072 | }, 1073 | Object { 1074 | "isThrow": false, 1075 | "value": undefined, 1076 | }, 1077 | Object { 1078 | "isThrow": false, 1079 | "value": undefined, 1080 | }, 1081 | Object { 1082 | "isThrow": false, 1083 | "value": undefined, 1084 | }, 1085 | Object { 1086 | "isThrow": false, 1087 | "value": undefined, 1088 | }, 1089 | Object { 1090 | "isThrow": false, 1091 | "value": undefined, 1092 | }, 1093 | Object { 1094 | "isThrow": false, 1095 | "value": undefined, 1096 | }, 1097 | Object { 1098 | "isThrow": false, 1099 | "value": undefined, 1100 | }, 1101 | Object { 1102 | "isThrow": false, 1103 | "value": undefined, 1104 | }, 1105 | Object { 1106 | "isThrow": false, 1107 | "value": undefined, 1108 | }, 1109 | Object { 1110 | "isThrow": false, 1111 | "value": undefined, 1112 | }, 1113 | Object { 1114 | "isThrow": false, 1115 | "value": undefined, 1116 | }, 1117 | Object { 1118 | "isThrow": false, 1119 | "value": undefined, 1120 | }, 1121 | Object { 1122 | "isThrow": false, 1123 | "value": undefined, 1124 | }, 1125 | Object { 1126 | "isThrow": false, 1127 | "value": undefined, 1128 | }, 1129 | Object { 1130 | "isThrow": false, 1131 | "value": undefined, 1132 | }, 1133 | Object { 1134 | "isThrow": false, 1135 | "value": undefined, 1136 | }, 1137 | Object { 1138 | "isThrow": false, 1139 | "value": undefined, 1140 | }, 1141 | Object { 1142 | "isThrow": false, 1143 | "value": undefined, 1144 | }, 1145 | Object { 1146 | "isThrow": false, 1147 | "value": undefined, 1148 | }, 1149 | Object { 1150 | "isThrow": false, 1151 | "value": undefined, 1152 | }, 1153 | Object { 1154 | "isThrow": false, 1155 | "value": undefined, 1156 | }, 1157 | Object { 1158 | "isThrow": false, 1159 | "value": undefined, 1160 | }, 1161 | ], 1162 | } 1163 | `; 1164 | 1165 | exports[`paginate() allows fine-tuning pathPrefix with a function 1`] = ` 1166 | [MockFunction] { 1167 | "calls": Array [ 1168 | Array [ 1169 | Object { 1170 | "component": "path/to/blog", 1171 | "context": Object { 1172 | "humanPageNumber": 1, 1173 | "limit": 3, 1174 | "nextPagePath": "/blog/page/2", 1175 | "numberOfPages": 4, 1176 | "pageNumber": 0, 1177 | "previousPagePath": "", 1178 | "skip": 0, 1179 | }, 1180 | "path": "/blog", 1181 | }, 1182 | ], 1183 | Array [ 1184 | Object { 1185 | "component": "path/to/blog", 1186 | "context": Object { 1187 | "humanPageNumber": 2, 1188 | "limit": 3, 1189 | "nextPagePath": "/blog/page/3", 1190 | "numberOfPages": 4, 1191 | "pageNumber": 1, 1192 | "previousPagePath": "/blog", 1193 | "skip": 3, 1194 | }, 1195 | "path": "/blog/page/2", 1196 | }, 1197 | ], 1198 | Array [ 1199 | Object { 1200 | "component": "path/to/blog", 1201 | "context": Object { 1202 | "humanPageNumber": 3, 1203 | "limit": 3, 1204 | "nextPagePath": "/blog/page/4", 1205 | "numberOfPages": 4, 1206 | "pageNumber": 2, 1207 | "previousPagePath": "/blog/page/2", 1208 | "skip": 6, 1209 | }, 1210 | "path": "/blog/page/3", 1211 | }, 1212 | ], 1213 | Array [ 1214 | Object { 1215 | "component": "path/to/blog", 1216 | "context": Object { 1217 | "humanPageNumber": 4, 1218 | "limit": 3, 1219 | "nextPagePath": "", 1220 | "numberOfPages": 4, 1221 | "pageNumber": 3, 1222 | "previousPagePath": "/blog/page/3", 1223 | "skip": 9, 1224 | }, 1225 | "path": "/blog/page/4", 1226 | }, 1227 | ], 1228 | ], 1229 | "results": Array [ 1230 | Object { 1231 | "isThrow": false, 1232 | "value": undefined, 1233 | }, 1234 | Object { 1235 | "isThrow": false, 1236 | "value": undefined, 1237 | }, 1238 | Object { 1239 | "isThrow": false, 1240 | "value": undefined, 1241 | }, 1242 | Object { 1243 | "isThrow": false, 1244 | "value": undefined, 1245 | }, 1246 | ], 1247 | } 1248 | `; 1249 | 1250 | exports[`paginate() creates pages and forwards context 1`] = ` 1251 | [MockFunction] { 1252 | "calls": Array [ 1253 | Array [ 1254 | Object { 1255 | "component": "path/to/blog", 1256 | "context": Object { 1257 | "additionalContext": true, 1258 | "humanPageNumber": 1, 1259 | "limit": 5, 1260 | "nextPagePath": "/blog/2", 1261 | "numberOfPages": 4, 1262 | "pageNumber": 0, 1263 | "previousPagePath": "", 1264 | "skip": 0, 1265 | }, 1266 | "path": "/blog", 1267 | }, 1268 | ], 1269 | Array [ 1270 | Object { 1271 | "component": "path/to/blog", 1272 | "context": Object { 1273 | "additionalContext": true, 1274 | "humanPageNumber": 2, 1275 | "limit": 3, 1276 | "nextPagePath": "/blog/3", 1277 | "numberOfPages": 4, 1278 | "pageNumber": 1, 1279 | "previousPagePath": "/blog", 1280 | "skip": 5, 1281 | }, 1282 | "path": "/blog/2", 1283 | }, 1284 | ], 1285 | Array [ 1286 | Object { 1287 | "component": "path/to/blog", 1288 | "context": Object { 1289 | "additionalContext": true, 1290 | "humanPageNumber": 3, 1291 | "limit": 3, 1292 | "nextPagePath": "/blog/4", 1293 | "numberOfPages": 4, 1294 | "pageNumber": 2, 1295 | "previousPagePath": "/blog/2", 1296 | "skip": 8, 1297 | }, 1298 | "path": "/blog/3", 1299 | }, 1300 | ], 1301 | Array [ 1302 | Object { 1303 | "component": "path/to/blog", 1304 | "context": Object { 1305 | "additionalContext": true, 1306 | "humanPageNumber": 4, 1307 | "limit": 3, 1308 | "nextPagePath": "", 1309 | "numberOfPages": 4, 1310 | "pageNumber": 3, 1311 | "previousPagePath": "/blog/3", 1312 | "skip": 11, 1313 | }, 1314 | "path": "/blog/4", 1315 | }, 1316 | ], 1317 | ], 1318 | "results": Array [ 1319 | Object { 1320 | "isThrow": false, 1321 | "value": undefined, 1322 | }, 1323 | Object { 1324 | "isThrow": false, 1325 | "value": undefined, 1326 | }, 1327 | Object { 1328 | "isThrow": false, 1329 | "value": undefined, 1330 | }, 1331 | Object { 1332 | "isThrow": false, 1333 | "value": undefined, 1334 | }, 1335 | ], 1336 | } 1337 | `; 1338 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import fp from "lodash/fp"; 3 | import { paginate, createPagePerItem } from "../src"; 4 | 5 | describe("paginate()", () => { 6 | it("creates pages and forwards context", () => { 7 | const createPage = jest.fn(); 8 | paginate({ 9 | createPage, 10 | items: Array(12).fill(), 11 | itemsPerPage: 3, 12 | itemsPerFirstPage: 5, 13 | pathPrefix: "/blog", 14 | component: "path/to/blog", 15 | context: { additionalContext: true } 16 | }); 17 | expect(createPage).toMatchSnapshot(); 18 | }); 19 | 20 | it("allows fine-tuning pathPrefix with a function", () => { 21 | const createPage = jest.fn(); 22 | paginate({ 23 | createPage, 24 | items: Array(12).fill(), 25 | itemsPerPage: 3, 26 | pathPrefix: ({ pageNumber }) => 27 | pageNumber === 0 ? "/blog" : "/blog/page", 28 | component: "path/to/blog" 29 | }); 30 | expect(createPage).toMatchSnapshot(); 31 | }); 32 | }); 33 | 34 | const blogPosts = _.map(Array(43), (item, index) => ({ 35 | id: index, 36 | title: `Post number ${index.toString()}`, 37 | path: `/blog/post-${index + 1}` 38 | })); 39 | 40 | describe("createPagePerItem()", () => { 41 | it("Calls createPage() once per item", () => { 42 | const createPage = jest.fn(); 43 | 44 | createPagePerItem({ 45 | createPage, 46 | items: fp.cloneDeep(blogPosts), 47 | itemToPath: "path", 48 | itemToId: "id", 49 | component: "/path/to/some/template.js" 50 | }); 51 | 52 | expect(createPage).toHaveBeenCalledTimes(blogPosts.length); 53 | }); 54 | 55 | it("Calls createPage() with the correct params", () => { 56 | const createPage = jest.fn(); 57 | 58 | createPagePerItem({ 59 | createPage, 60 | items: fp.cloneDeep(blogPosts), 61 | itemToPath: "path", 62 | itemToId: "id", 63 | component: "/path/to/some/template.js" 64 | }); 65 | 66 | expect(createPage).toMatchSnapshot(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /__tests__/utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | getPreviousItem, 3 | getNextItem, 4 | paginatedPath, 5 | calculateSkip 6 | } from "../src/utils"; 7 | 8 | describe("utils", () => { 9 | describe("getPreviousItem()", () => { 10 | it("Returns the second item", () => { 11 | const items = [{ name: "one" }, { name: "two" }, { name: "three" }]; 12 | expect(getPreviousItem(items, 2)).toEqual(items[1]); 13 | }); 14 | 15 | it("Returns undefined for index 0", () => { 16 | const items = [{ name: "one" }, { name: "two" }, { name: "three" }]; 17 | expect(getPreviousItem(items, 0)).toBeUndefined(); 18 | }); 19 | }); 20 | 21 | describe("getNextItem()", () => { 22 | it("Returns the third item", () => { 23 | const items = [{ name: "one" }, { name: "two" }, { name: "three" }]; 24 | expect(getNextItem(items, 1)).toEqual(items[2]); 25 | }); 26 | 27 | it("Returns undefined for index 2", () => { 28 | const items = [{ name: "one" }, { name: "two" }, { name: "three" }]; 29 | expect(getNextItem(items, 2)).toBeUndefined(); 30 | }); 31 | }); 32 | 33 | describe("paginatedPath()", () => { 34 | it("Returns only one slash for the index page", () => { 35 | expect(paginatedPath("/", 0, 3)).toEqual("/"); 36 | }); 37 | 38 | it("Returns /2 for the second index page", () => { 39 | expect(paginatedPath("/", 1, 3)).toEqual("/2"); 40 | }); 41 | 42 | it("Passes pageNumber and numberOfPages to pathPrefix()", () => { 43 | const pathPrefix = jest.fn(() => "/foo"); 44 | expect(paginatedPath(pathPrefix, 0, 3)).toEqual("/foo"); 45 | expect(pathPrefix).toHaveBeenCalledWith({ 46 | pageNumber: 0, 47 | numberOfPages: 3 48 | }); 49 | }); 50 | 51 | it("Returns empty string for the last + 1 page", () => { 52 | expect(paginatedPath("/foo", 2, 2)).toEqual(""); 53 | }); 54 | 55 | it("Returns empty string for page -1", () => { 56 | expect(paginatedPath("/foo", -1, 2)).toEqual(""); 57 | }); 58 | }); 59 | 60 | describe("calculateSkip()", () => { 61 | it("Returns zero for the first page", () => { 62 | expect(calculateSkip(0, 3, 10)).toEqual(0); 63 | }); 64 | 65 | it("Returns firstPageCount for the second page", () => { 66 | expect(calculateSkip(1, 3, 10)).toEqual(3); 67 | }); 68 | 69 | it("Returns firstPageCount + itemsPerPage for the third page", () => { 70 | expect(calculateSkip(2, 3, 10)).toEqual(13); 71 | }); 72 | 73 | it("Returns firstPageCount + (3 x itemsPerPage) for the fifth page", () => { 74 | expect(calculateSkip(4, 3, 10)).toEqual(33); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | allow: 8 | - dependency-type: "production" 9 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | ## `gatsby-node.js` 4 | 5 | ```javascript 6 | import { paginate, createPagePerItem } from "gatsby-awesome-pagination"; 7 | ``` 8 | 9 | ```javascript 10 | exports.createPages = ({ graphql, boundActionCreators }) => { 11 | const { createPage } = boundActionCreators; 12 | 13 | // We return a promise immediately 14 | return new Promise((resolve, reject) => { 15 | // Start by creating all the blog pages 16 | const blogPost = path.resolve("./src/templates/blog-post.js"); 17 | const blogIndex = path.resolve("./src/templates/blog-index.js"); 18 | resolve( 19 | graphql( 20 | ` 21 | { 22 | allMarkdownRemark( 23 | sort: { fields: [frontmatter___date], order: DESC } 24 | ) { 25 | edges { 26 | node { 27 | id 28 | frontmatter { 29 | permalink 30 | } 31 | } 32 | } 33 | } 34 | } 35 | ` 36 | ).then(result => { 37 | if (result.errors) { 38 | console.log(result.errors); 39 | reject(result.errors); 40 | } 41 | 42 | // Get an array of posts from the query result 43 | const blogPosts = _.get(result, "data.allMarkdownRemark.edges"); 44 | 45 | // Create the blog index pages like `/blog`, `/blog/2`, `/blog/3`, etc. 46 | // The first page will have 3 items and each following page will have 10 47 | // blog posts and a link to the next and previous pages. 48 | paginate({ 49 | createPage, 50 | items: blogPosts, 51 | component: blogIndex, 52 | itemsPerPage: 10, 53 | itemsPerFirstPage: 3, 54 | pathPrefix: "/blog" 55 | }); 56 | 57 | // Create one page per blog post, with a link to the previous and next 58 | // blog posts. 59 | createPagePerItem({ 60 | createPage, 61 | items: blogPosts, 62 | component: blogPost, 63 | itemToPath: "node.frontmatter.permalink", 64 | itemToId: "node.id" 65 | }); 66 | }) 67 | ); 68 | }); 69 | }; 70 | ``` 71 | 72 | ## Blog index component 73 | 74 | Retrieve posts via the `$skip` and `$limit` parameters like so: 75 | 76 | ```javascript 77 | export const pageQuery = graphql` 78 | query($skip: Int!, $limit: Int!) { 79 | posts: allMarkdownRemark( 80 | sort: { fields: [frontmatter___date], order: DESC } 81 | skip: $skip 82 | limit: $limit 83 | ) { 84 | edges { 85 | node { 86 | excerpt(pruneLength: 280) 87 | fields { 88 | permalink 89 | } 90 | frontmatter { 91 | date(formatString: "DD MMMM, YYYY") 92 | title 93 | } 94 | } 95 | } 96 | } 97 | } 98 | `; 99 | ``` 100 | 101 | Render posts like so: 102 | 103 | ```javascript 104 | const BlogIndex = props => { 105 | const { pageContext } = props; 106 | const { previousPagePath, nextPagePath } = pageContext; 107 | 108 | return ( 109 |
110 | {props.data.posts.edges.map(edge => )} 111 |
112 | {previousPagePath ? Previous : null} 113 | {nextPagePath ? Next : null} 114 |
115 |
116 | ); 117 | }; 118 | ``` 119 | 120 | ## Blog post component 121 | 122 | First, you can retrieve any data you need like so: 123 | 124 | ```javascript 125 | export const pageQuery = graphql` 126 | query($pageId: String!) { 127 | post: markdownRemark(id: { eq: $pageId }) { 128 | html 129 | frontmatter { 130 | title 131 | } 132 | } 133 | } 134 | `; 135 | ``` 136 | 137 | Then inside your component you can render links to the previous and next posts. 138 | 139 | ```javascript 140 | const BlogPost = props => { 141 | const { pageContext, data } = props; 142 | const { previousPagePath, nextPagePath, previousItem, nextItem } = pageContext; 143 | const { post } = data; 144 | 145 | return ( 146 |
147 |

{post.frontmatter.title}

148 |
149 |
150 | {previousPagePath ? ( 151 | 152 | {previousItem.node.frontmatter.title} 153 | 154 | ) : null} 155 | {nextPagePath ? ( 156 | 157 | {nextItem.node.frontmatter.title} 158 | 159 | ) : null} 160 |
161 |
162 | ); 163 | }; 164 | ``` 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-awesome-pagination", 3 | "version": "0.3.8", 4 | "description": "An opinionated, more awesome approach to pagination in Gatsby", 5 | "files": [ 6 | "lib" 7 | ], 8 | "main": "lib/index.js", 9 | "browser": "lib/components.js", 10 | "scripts": { 11 | "prepack": "npm run build", 12 | "build": "npm run prepack:babel && npm run prepack:flow", 13 | "prepack:babel": "babel src/ -d lib", 14 | "prepack:flow": "flow-copy-source src lib", 15 | "test": "jest" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/GatsbyCentral/gatsby-plugin-awesome-pagination.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/GatsbyCentral/gatsby-plugin-awesome-pagination/issues" 23 | }, 24 | "homepage": "https://github.com/GatsbyCentral/gatsby-plugin-awesome-pagination#readme", 25 | "keywords": [ 26 | "gatsby", 27 | "gatsby-plugin", 28 | "pagination" 29 | ], 30 | "author": "Callum Macdonald", 31 | "license": "MIT", 32 | "devDependencies": { 33 | "babel-cli": "^6.26.0", 34 | "babel-jest": "^23.6.0", 35 | "babel-plugin-add-module-exports": "^0.2.1", 36 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 37 | "babel-plugin-transform-react-jsx": "^6.24.1", 38 | "babel-preset-env": "^1.7.0", 39 | "flow-bin": "^0.72.0", 40 | "flow-copy-source": "^1.3.0", 41 | "jest": "^23.6.0" 42 | }, 43 | "dependencies": { 44 | "lodash": "^4.17.21" 45 | }, 46 | "peerDependencies": { 47 | "gatsby": ">=2.0.0", 48 | "react": ">=16.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Link } from "gatsby"; 3 | 4 | const linkOrText = ( 5 | label: string, 6 | link: string, 7 | activeStyle: {}, 8 | inactiveStyle: {} 9 | ): React.Node => 10 | link ? ( 11 | 12 | {label} 13 | 14 | ) : ( 15 | {label} 16 | ); 17 | 18 | type PaginationLinksProps = { 19 | previousLabel?: string, 20 | nextLabel?: string, 21 | pageLabel?: string, 22 | separator?: string, 23 | pageContext: { 24 | pageNumber: number, 25 | humanPageNumber: number, 26 | skip: number, 27 | limit: number, 28 | numberOfPages: number, 29 | previousPagePath?: string, 30 | nextPagePath?: string 31 | } 32 | }; 33 | 34 | export const PaginationLinks = ({ 35 | previousLabel = "← previous", 36 | nextLabel = "next →", 37 | pageLabel = "Page: %d", 38 | separator = " - ", 39 | activeStyle = {}, 40 | inactiveStyle = { textDecorationLine: "line-through", color: "grey" }, 41 | pageContext: { 42 | pageNumber, 43 | humanPageNumber, 44 | skip, 45 | limit, 46 | numberOfPages, 47 | previousPagePath, 48 | nextPagePath 49 | } 50 | }: PaginationLinksProps): React.Node => { 51 | return ( 52 |
53 | {linkOrText(previousLabel, previousPagePath, activeStyle, inactiveStyle)} 54 | {separator} 55 | {pageLabel.replace("%d", humanPageNumber)} 56 | {separator} 57 | {linkOrText(nextLabel, nextPagePath)} 58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import isString from "lodash/fp/isString"; 4 | import get from "lodash/fp/get"; 5 | import times from "lodash/fp/times"; 6 | import cloneDeep from "lodash/fp/cloneDeep"; 7 | import isInteger from "lodash/fp/isInteger"; 8 | 9 | import { 10 | paginatedPath, 11 | getPreviousItem, 12 | getNextItem, 13 | calculateSkip 14 | } from "./utils"; 15 | 16 | import type { PathPrefix } from "./utils"; 17 | 18 | type CreatePage = ({}) => void; 19 | 20 | type PaginateOpts = { 21 | createPage: CreatePage, 22 | items: {}[], 23 | itemsPerPage: number, 24 | itemsPerFirstPage?: number, 25 | pathPrefix: PathPrefix, 26 | component: string, 27 | context?: {} 28 | }; 29 | export const paginate = (opts: PaginateOpts): void => { 30 | const { 31 | createPage, 32 | items, 33 | itemsPerPage, 34 | itemsPerFirstPage, 35 | pathPrefix, 36 | component, 37 | context 38 | } = opts; 39 | 40 | // How many items do we have in total? We use `items.length` here. In fact, we 41 | // could just accept an integer in the API as the actual contents of `items` 42 | // is never used. 43 | const totalItems = items.length; 44 | // If `itemsPerFirstPage` is specified, use that value for the first page, 45 | // otherwise use `itemsPerPage`. 46 | // $FlowExpectError 47 | const firstPageCount: number = isInteger(itemsPerFirstPage) 48 | ? itemsPerFirstPage 49 | : itemsPerPage; 50 | 51 | // How many page should we have? 52 | const numberOfPages = 53 | // If there are less than `firstPageCount` items, we'll only have 1 page 54 | totalItems <= firstPageCount 55 | ? 1 56 | : Math.ceil((totalItems - firstPageCount) / itemsPerPage) + 1; 57 | 58 | // Iterate as many times as we need pages 59 | times((pageNumber: number) => { 60 | // Create the path for this page 61 | const path = paginatedPath(pathPrefix, pageNumber, numberOfPages); 62 | 63 | // Calculate the path for the previous and next pages 64 | const previousPagePath = paginatedPath( 65 | pathPrefix, 66 | pageNumber - 1, 67 | numberOfPages 68 | ); 69 | const nextPagePath = paginatedPath( 70 | pathPrefix, 71 | pageNumber + 1, 72 | numberOfPages 73 | ); 74 | 75 | // Call `createPage()` for this paginated page 76 | createPage({ 77 | path, 78 | component, 79 | // Clone the passed `context` and extend our new pagination context values 80 | // on top of it. 81 | context: Object.assign({}, cloneDeep(context), { 82 | pageNumber, 83 | humanPageNumber: pageNumber + 1, 84 | skip: calculateSkip(pageNumber, firstPageCount, itemsPerPage), 85 | limit: pageNumber === 0 ? firstPageCount : itemsPerPage, 86 | numberOfPages, 87 | previousPagePath, 88 | nextPagePath 89 | }) 90 | }); 91 | })(numberOfPages); 92 | }; 93 | 94 | type CreatePagePerItemOpts = { 95 | createPage: CreatePage, 96 | items: {}[], 97 | itemToPath: string | (({}) => string), 98 | itemToId: string | (({}) => string), 99 | component: string 100 | }; 101 | export const createPagePerItem = (opts: CreatePagePerItemOpts): void => { 102 | const { createPage, items, itemToPath, itemToId, component } = opts; 103 | 104 | // $FlowExpectError 105 | const getPath: ({}) => string = isString(itemToPath) 106 | ? get(itemToPath) 107 | : itemToPath; 108 | // $FlowExpectError 109 | const getId: ({}) => string = isString(itemToId) ? get(itemToId) : itemToId; 110 | 111 | // We cannot use `forEach()` here because in the FP version of lodash, the 112 | // iteratee is capped to a single argument, the item itself. We cannot get the 113 | // item's index in the array. So instead we use `times()` and provide the 114 | // length of the array. 115 | times((index: number) => { 116 | const item = items[index]; 117 | const path = getPath(item); 118 | const id = getId(item); 119 | 120 | // NOTE: If there is no previous / next item, we set an empty string as the 121 | // value for the next and previous path and ID. Gatsby ignores context 122 | // values which are undefined, so we need these to exist. 123 | const previousItem = getPreviousItem(items, index); 124 | const previousPath = getPath(previousItem) || ""; 125 | const nextItem = getNextItem(items, index); 126 | const nextPath = getPath(nextItem) || ""; 127 | 128 | // Does the item have a `context` field? 129 | const itemContext = get("context")(item) || {}; 130 | const context = Object.assign({}, itemContext, { 131 | pageId: id, 132 | previousPagePath: previousPath, 133 | previousItem: previousItem, 134 | previousPageId: getId(previousItem) || "", 135 | nextPagePath: nextPath, 136 | nextItem: nextItem, 137 | nextPageId: getId(nextItem) || "" 138 | }); 139 | 140 | // Call `createPage()` for this item 141 | createPage({ 142 | path, 143 | component, 144 | context 145 | }); 146 | })(items.length); 147 | }; 148 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import get from "lodash/fp/get"; 3 | 4 | type ReturnedItem = {} | void; 5 | 6 | export const getPreviousItem = (items: {}[], index: number): ReturnedItem => { 7 | // If this is the first (or -1) element, there is no previous, so return 8 | // undefined 9 | if (index <= 0) { 10 | return; 11 | } 12 | 13 | // Get the previous element 14 | return get(`[${index - 1}]`, items); 15 | }; 16 | 17 | export const getNextItem = (items: {}[], index: number): ReturnedItem => { 18 | // If this is the last element (or later), there is no previous, so return 19 | // undefined 20 | if (index >= items.length - 1) { 21 | return; 22 | } 23 | 24 | // Get the previous element 25 | return get(`[${index + 1}]`, items); 26 | }; 27 | 28 | type PathPrefixFunction = ({ 29 | pageNumber: number, 30 | numberOfPages: number 31 | }) => string; 32 | 33 | export type PathPrefix = string | PathPrefixFunction; 34 | 35 | export const paginatedPath = ( 36 | pathPrefix: PathPrefix, 37 | pageNumber: number, 38 | numberOfPages: number 39 | ): string => { 40 | // If this page is less than zero (-1 for example), then it it does not 41 | // exist, return an empty string. 42 | if (pageNumber < 0) { 43 | return ""; 44 | } 45 | 46 | // If this page number (which is zero indexed) plus one is more than the total 47 | // number of pages, then this page does not exist, so return an empty string. 48 | if (pageNumber + 1 > numberOfPages) { 49 | return ""; 50 | } 51 | 52 | // Calculate a path prefix either by calling `pathPrefix()` if it's a function 53 | // or simply using `pathPrefix` if it is a string. 54 | const prefix = 55 | typeof pathPrefix === "function" 56 | ? pathPrefix({ pageNumber, numberOfPages }) 57 | : pathPrefix; 58 | 59 | // If this is page 0, return only the pathPrefix 60 | if (pageNumber === 0) { 61 | return prefix; 62 | } 63 | 64 | // Otherwise, add a slash and the number + 1. We add 1 because `pageNumber` is 65 | // zero indexed, but for human consuption, we want 1 indexed numbers. 66 | return `${prefix !== "/" ? prefix : ""}/${pageNumber + 1}`; 67 | // NOTE: If `pathPrefix` is a single slash (the index page) then we do not 68 | // want to output two slashes, so we omit it. 69 | }; 70 | 71 | export const calculateSkip = ( 72 | pageNumber: number, 73 | firstPageCount: number, 74 | itemsPerPage: number 75 | ): number => { 76 | if (pageNumber === 0) { 77 | return 0; 78 | } else if (pageNumber === 1) { 79 | return firstPageCount; 80 | } else { 81 | return firstPageCount + itemsPerPage * (pageNumber - 1); 82 | } 83 | }; 84 | --------------------------------------------------------------------------------