From 228517268c5f1097b7c0a0740fe1de4d3e30e99a Mon Sep 17 00:00:00 2001 From: Artem Faust <91615835+IronHedgehog@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:08:48 +0300 Subject: [PATCH 1/2] feat: add pagination --- package-lock.json | 12 ++- package.json | 2 +- src/App.tsx | 126 +++++++++-------------- src/components/Pagination/Pagination.tsx | 88 +++++++++++++++- 4 files changed, 141 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc04a7602..cb85c1267 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.9.12", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", @@ -1183,10 +1183,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.9.12", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.12.tgz", - "integrity": "sha512-/OcmxMa34lYLFlGx7Ig926W1U1qjrnXbjFJ2TzUcDaLmED+A5se652NcWwGOidXRuMAOYLPU2jNYBEkKyXrFJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -3256,7 +3257,8 @@ "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" }, "node_modules/clean-stack": { "version": "2.2.0", diff --git a/package.json b/package.json index f412fefe6..617386d19 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.9.12", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", diff --git a/src/App.tsx b/src/App.tsx index 189a990c8..51c01759e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,31 @@ -import React from 'react'; +import React, { useState } from 'react'; import './App.css'; +import { Pagination } from './components/Pagination'; import { getNumbers } from './utils'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const items = getNumbers(1, 42).map(n => `Item ${n}`); +enum PerPage { + THREE = 3, + FIVE = 5, + TEN = 10, + TWENTY = 20, +} + +const items: string[] = getNumbers(1, 42).map(n => `Item ${n}`); export const App: React.FC = () => { + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(PerPage.FIVE); + + const startIndex = (page - 1) * perPage; + const endIndex = Math.min(startIndex + perPage, items.length); + const visibleItems = items.slice(startIndex, endIndex); + return (

Items with Pagination

- Page 1 (items 1 - 5 of 42) + Page {page} (items {startIndex + 1} - {endIndex} of {items.length})

@@ -19,11 +33,17 @@ export const App: React.FC = () => {
@@ -32,78 +52,24 @@ export const App: React.FC = () => {
- {/* Move this markup to Pagination */} - + { + if (page !== selectedPage) { + setPage(selectedPage); + } + }} + /> ); diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index e417a09fc..0fb15487a 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -1 +1,87 @@ -export const Pagination = () => {}; +import cn from 'classnames'; +import React from 'react'; +interface Props { + total: number; + perPage: number; + currentPage: number; + onPageChange: (selectedPage: number) => void; +} + +export const Pagination: React.FC = ({ + total, + perPage = 5, + currentPage = 1, + onPageChange, +}) => { + const totalPages: number = Math.ceil(total / perPage); + const pages: number[] = Array.from({ length: totalPages }, (_, i) => i + 1); + + return ( + + ); +}; From 2baf70589f715a9bf217cb7466f959290ed89193 Mon Sep 17 00:00:00 2001 From: Artem Faust <91615835+IronHedgehog@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:23:49 +0300 Subject: [PATCH 2/2] fix: add react-router --- package-lock.json | 42 ++++++++++++++++++++++++ package.json | 1 + src/App.tsx | 31 ++++++++++------- src/components/Pagination/Pagination.tsx | 6 ++-- src/index.tsx | 8 ++++- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb85c1267..f3faea4e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router": "^7.18.0", "react-transition-group": "^4.4.5" }, "devDependencies": { @@ -3405,6 +3406,19 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -8736,6 +8750,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -9234,6 +9270,12 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index 617386d19..f67a81e92 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router": "^7.18.0", "react-transition-group": "^4.4.5" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index 51c01759e..e8bfabb83 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,6 @@ -import React, { useState } from 'react'; +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { useSearchParams } from 'react-router'; import './App.css'; import { Pagination } from './components/Pagination'; import { getNumbers } from './utils'; @@ -13,13 +15,25 @@ enum PerPage { const items: string[] = getNumbers(1, 42).map(n => `Item ${n}`); export const App: React.FC = () => { - const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(PerPage.FIVE); + const [searchParams, setSearchParams] = useSearchParams(); + const page = Number(searchParams.get('page')) || 1; + const perPage = Number(searchParams.get('perPage')) || PerPage.FIVE; const startIndex = (page - 1) * perPage; const endIndex = Math.min(startIndex + perPage, items.length); const visibleItems = items.slice(startIndex, endIndex); + const handlePageChange = (newPage: number) => { + searchParams.set('page', newPage.toString()); + setSearchParams(searchParams); + }; + + const handlePerPageChange = (e: React.ChangeEvent) => { + searchParams.set('perPage', e.target.value); + searchParams.set('page', '1'); + setSearchParams(searchParams); + }; + return (

Items with Pagination

@@ -35,10 +49,7 @@ export const App: React.FC = () => { id="perPageSelector" className="form-control" value={perPage} - onChange={e => { - setPerPage(Number(e.target.value)); - setPage(1); - }} + onChange={handlePerPageChange} > @@ -56,11 +67,7 @@ export const App: React.FC = () => { total={items.length} perPage={perPage} currentPage={page} - onPageChange={(selectedPage: number) => { - if (page !== selectedPage) { - setPage(selectedPage); - } - }} + onPageChange={handlePageChange} />
    {visibleItems.map(item => { diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index 0fb15487a..6d19e543c 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -3,7 +3,7 @@ import React from 'react'; interface Props { total: number; perPage: number; - currentPage: number; + currentPage?: number; onPageChange: (selectedPage: number) => void; } @@ -54,7 +54,9 @@ export const Pagination: React.FC = ({ href={`#${num}`} onClick={e => { e.preventDefault(); - onPageChange(num); + if (num !== currentPage) { + onPageChange(num); + } }} > {num} diff --git a/src/index.tsx b/src/index.tsx index 226f3f4bd..d960d5966 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,11 @@ import { createRoot } from 'react-dom/client'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { BrowserRouter } from 'react-router'; import { App } from './App'; -createRoot(document.getElementById('root') as HTMLElement).render(); +createRoot(document.getElementById('root') as HTMLElement).render( + + + , +);