diff --git a/package-lock.json b/package-lock.json
index b8a41fe9..b2af14eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,13 +14,15 @@
"bulma": "^1.0.1",
"classnames": "^2.5.1",
"react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^7.13.0"
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
+ "@types/classnames": "^2.3.0",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
@@ -87,6 +89,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"dev": true,
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
@@ -435,6 +438,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -457,6 +461,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@@ -1170,10 +1175,11 @@
}
},
"node_modules/@mate-academy/scripts": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.5.tgz",
- "integrity": "sha512-mHRY2FkuoYCf5U0ahIukkaRo5LSZsxrTSgMJheFoyf3VXsTvfM9OfWcZIDIDB521kdPrScHHnRp+JRNjCfUO5A==",
+ "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",
@@ -1210,15 +1216,13 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
"integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/@mate-academy/stylelint-config/node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dev": true,
- "peer": true,
"dependencies": {
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
@@ -1245,7 +1249,6 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz",
"integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -1258,7 +1261,6 @@
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-7.0.2.tgz",
"integrity": "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==",
"dev": true,
- "peer": true,
"dependencies": {
"flat-cache": "^3.2.0"
},
@@ -1277,7 +1279,6 @@
"resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz",
"integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==",
"dev": true,
- "peer": true,
"dependencies": {
"@types/minimist": "^1.2.2",
"camelcase-keys": "^7.0.0",
@@ -1304,7 +1305,6 @@
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
"integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=12.0"
},
@@ -1321,7 +1321,6 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -1472,7 +1471,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -1520,7 +1518,6 @@
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz",
"integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">= 18"
}
@@ -1549,7 +1546,6 @@
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dev": true,
- "peer": true,
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
@@ -1563,7 +1559,6 @@
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz",
"integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==",
"dev": true,
- "peer": true,
"dependencies": {
"@octokit/request": "^9.0.0",
"@octokit/types": "^13.0.0",
@@ -1577,8 +1572,7 @@
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "2.21.3",
@@ -1640,7 +1634,6 @@
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
"dev": true,
- "peer": true,
"dependencies": {
"@octokit/endpoint": "^10.0.0",
"@octokit/request-error": "^6.0.1",
@@ -1656,7 +1649,6 @@
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz",
"integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==",
"dev": true,
- "peer": true,
"dependencies": {
"@octokit/types": "^13.0.0"
},
@@ -1844,7 +1836,6 @@
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz",
"integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==",
"dev": true,
- "peer": true,
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
@@ -2145,6 +2136,16 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/classnames": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.0.tgz",
+ "integrity": "sha512-3GsbOoDYteFShlrBTKzI2Eii4vPg/jAf7LXRIn0WQePKlmhpkV0KoTMuawA7gZJkrbPrZGwv9IEAfIWaOaQK8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "classnames": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@@ -2171,8 +2172,7 @@
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/@types/node": {
"version": "20.14.10",
@@ -2187,8 +2187,7 @@
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.12",
@@ -2201,6 +2200,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dev": true,
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -2211,6 +2211,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"dev": true,
+ "peer": true,
"dependencies": {
"@types/react": "*"
}
@@ -2276,6 +2277,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
"integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"dev": true,
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
@@ -2321,7 +2323,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
"integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"dev": true,
- "peer": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
@@ -2390,7 +2391,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
"integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
"dev": true,
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.16.0",
@@ -2455,6 +2455,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true,
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2778,7 +2779,6 @@
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
"integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2924,8 +2924,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/binary-extensions": {
"version": "2.3.0",
@@ -2976,8 +2975,7 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/browserslist": {
"version": "4.23.2",
@@ -2998,6 +2996,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001640",
"electron-to-chromium": "^1.4.820",
@@ -3091,7 +3090,6 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -3104,7 +3102,6 @@
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz",
"integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==",
"dev": true,
- "peer": true,
"dependencies": {
"camelcase": "^6.3.0",
"map-obj": "^4.1.0",
@@ -3123,7 +3120,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -3234,7 +3230,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",
@@ -3293,7 +3290,6 @@
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
- "peer": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
@@ -3381,6 +3377,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",
@@ -3491,6 +3500,7 @@
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
"dev": true,
"hasInstallScript": true,
+ "peer": true,
"dependencies": {
"@cypress/request": "^3.0.0",
"@cypress/xvfb": "^1.2.4",
@@ -3742,7 +3752,6 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
"integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -3755,7 +3764,6 @@
"resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
"integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
"dev": true,
- "peer": true,
"dependencies": {
"decamelize": "^1.1.0",
"map-obj": "^1.0.0"
@@ -3772,7 +3780,6 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3782,7 +3789,6 @@
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
"integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3958,6 +3964,7 @@
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
"integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-colors": "^4.1.1",
"strip-ansi": "^6.0.1"
@@ -4229,6 +4236,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -4326,6 +4334,7 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true,
+ "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -4423,6 +4432,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
"dev": true,
+ "peer": true,
"dependencies": {
"array-includes": "^3.1.7",
"array.prototype.findlastindex": "^1.2.3",
@@ -5170,7 +5180,6 @@
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
"dev": true,
- "peer": true,
"bin": {
"flat": "cli.js"
}
@@ -5525,7 +5534,6 @@
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
- "peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -5557,7 +5565,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
- "peer": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -5704,7 +5711,6 @@
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
"integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=6"
}
@@ -5795,7 +5801,6 @@
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
- "peer": true,
"bin": {
"he": "bin/he"
}
@@ -5805,7 +5810,6 @@
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
"integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
"dev": true,
- "peer": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -5818,7 +5822,6 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
- "peer": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -5830,8 +5833,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/html-tags": {
"version": "3.3.1",
@@ -5924,7 +5926,6 @@
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
"integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -6299,7 +6300,6 @@
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -7040,7 +7040,6 @@
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=8"
},
@@ -7139,7 +7138,6 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=4"
}
@@ -7173,7 +7171,6 @@
"resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
"integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
"dev": true,
- "peer": true,
"dependencies": {
"arrify": "^1.0.1",
"is-plain-obj": "^1.1.0",
@@ -7188,7 +7185,6 @@
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7198,7 +7194,6 @@
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.6.0.tgz",
"integrity": "sha512-hxjt4+EEB0SA0ZDygSS015t65lJw/I2yRCS3Ae+SJ5FrbzrXgfYwJr96f0OvIXdj7h4lv/vLCrH3rkiuizFSvw==",
"dev": true,
- "peer": true,
"dependencies": {
"ansi-colors": "^4.1.3",
"browser-stdout": "^1.3.1",
@@ -7234,7 +7229,6 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -7247,7 +7241,6 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -7257,7 +7250,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
- "peer": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -7269,15 +7261,13 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/mocha/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
- "peer": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -7868,7 +7858,6 @@
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
"integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
"dev": true,
- "peer": true,
"dependencies": {
"hosted-git-info": "^4.0.1",
"is-core-module": "^2.5.0",
@@ -8414,6 +8403,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
@@ -8492,6 +8482,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
"dev": true,
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -8520,6 +8511,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -8651,7 +8643,6 @@
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -8664,7 +8655,6 @@
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
- "peer": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -8673,6 +8663,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -8684,6 +8675,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -8707,12 +8699,49 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
+ "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
+ "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-router-dom": {
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
+ "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/read-pkg": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz",
"integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==",
"dev": true,
- "peer": true,
"dependencies": {
"@types/normalize-package-data": "^2.4.0",
"normalize-package-data": "^3.0.2",
@@ -8731,7 +8760,6 @@
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz",
"integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==",
"dev": true,
- "peer": true,
"dependencies": {
"find-up": "^5.0.0",
"read-pkg": "^6.0.0",
@@ -8749,7 +8777,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -8762,7 +8789,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -8787,7 +8813,6 @@
"resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz",
"integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==",
"dev": true,
- "peer": true,
"dependencies": {
"indent-string": "^5.0.0",
"strip-indent": "^4.0.0"
@@ -8804,7 +8829,6 @@
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
"integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -9137,6 +9161,7 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -9174,7 +9199,6 @@
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
- "peer": true,
"dependencies": {
"randombytes": "^2.1.0"
}
@@ -9185,6 +9209,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",
@@ -9381,7 +9411,6 @@
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
"dev": true,
- "peer": true,
"dependencies": {
"spdx-expression-parse": "^3.0.0",
"spdx-license-ids": "^3.0.0"
@@ -9391,15 +9420,13 @@
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
"integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
- "peer": true,
"dependencies": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
@@ -9409,8 +9436,7 @@
"version": "3.0.18",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz",
"integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/sshpk": {
"version": "1.18.0",
@@ -9608,7 +9634,6 @@
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz",
"integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==",
"dev": true,
- "peer": true,
"dependencies": {
"min-indent": "^1.0.1"
},
@@ -9647,8 +9672,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
"integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/stylelint": {
"version": "16.7.0",
@@ -10058,7 +10082,6 @@
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz",
"integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -10249,6 +10272,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10282,8 +10306,7 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/universalify": {
"version": "2.0.1",
@@ -10372,7 +10395,6 @@
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"dev": true,
- "peer": true,
"dependencies": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
@@ -10406,6 +10428,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
"integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
"dev": true,
+ "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.39",
@@ -10718,8 +10741,7 @@
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
"integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
@@ -10822,7 +10844,6 @@
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
- "peer": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
@@ -10841,7 +10862,6 @@
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
}
@@ -10851,7 +10871,6 @@
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
"integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
"dev": true,
- "peer": true,
"dependencies": {
"camelcase": "^6.0.0",
"decamelize": "^4.0.0",
diff --git a/package.json b/package.json
index ad2e836e..c11d6b84 100644
--- a/package.json
+++ b/package.json
@@ -10,13 +10,15 @@
"bulma": "^1.0.1",
"classnames": "^2.5.1",
"react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^7.13.0"
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
+ "@types/classnames": "^2.3.0",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
diff --git a/src/App.tsx b/src/App.tsx
index 5d253930..fc36acd4 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,43 +1,32 @@
import React from 'react';
+import { Navigate, Route, Routes } from 'react-router-dom';
import '@fortawesome/fontawesome-free/css/all.css';
import 'bulma/css/bulma.css';
import './App.scss';
-// import peopleFromServer from './people.json';
+import { Header } from './components/Header';
+import { HomePage } from './pages/HomePage';
+import { PeoplePage } from './pages/PeoplePage';
+import { NotFoundPage } from './pages/NotFoundPage';
-export class App extends React.Component {
- state = {};
+export const App: React.FC = () => {
+ return (
+
+
- render() {
- return (
-
-
People table
+
+ } />
-
-
-
- | name |
- sex |
- born |
-
-
+ } />
-
-
- | Carolus Haverbeke |
- m |
- 1832 |
-
+
+ } />
+ } />
+
-
- | Emma de Milliano |
- f |
- 1842 |
-
-
-
-
- );
- }
-}
+
} />
+
+
+ );
+};
diff --git a/src/api/people.ts b/src/api/people.ts
new file mode 100644
index 00000000..b20e5d9a
--- /dev/null
+++ b/src/api/people.ts
@@ -0,0 +1,24 @@
+import { Person, PersonWithParents } from '../types/Person';
+
+const API_URL =
+ 'https://mate-academy.github.io/react_people-table/api/people.json';
+
+export async function getPeople(): Promise {
+ const response = await fetch(API_URL);
+
+ if (!response.ok) {
+ throw new Error('Failed to load people');
+ }
+
+ const people: Person[] = await response.json();
+
+ const byName = new Map();
+
+ people.forEach(p => byName.set(p.name, p));
+
+ return people.map(person => ({
+ ...person,
+ mother: person.motherName ? (byName.get(person.motherName) ?? null) : null,
+ father: person.fatherName ? (byName.get(person.fatherName) ?? null) : null,
+ }));
+}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 00000000..45145f24
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,18 @@
+import { NavLink } from 'react-router-dom';
+
+export const Header = () => (
+
+
+
+);
diff --git a/src/components/PeopleTable/PeopleTable.scss b/src/components/PeopleTable/PeopleTable.scss
new file mode 100644
index 00000000..1fbae3dd
--- /dev/null
+++ b/src/components/PeopleTable/PeopleTable.scss
@@ -0,0 +1,25 @@
+.PeopleTable {
+ border-collapse: collapse;
+}
+
+.PeopleTable th.is-active {
+ font-weight: 700;
+ text-decoration: underline;
+}
+
+.Person.is-selected {
+ background-color: #fff3c4; // any visible highlight
+}
+
+.PersonName--m {
+ color: blue;
+}
+
+.PersonName--f {
+ color: red;
+}
+
+.PersonName--plain {
+ color: black;
+ font-weight: 700;
+}
diff --git a/src/components/PeopleTable/PeopleTable.tsx b/src/components/PeopleTable/PeopleTable.tsx
new file mode 100644
index 00000000..e87554b0
--- /dev/null
+++ b/src/components/PeopleTable/PeopleTable.tsx
@@ -0,0 +1,91 @@
+import { PersonWithParents } from '../../types/Person';
+import { PersonRow } from './PersonRow';
+import './PeopleTable.scss';
+
+type SortBy = 'name' | 'sex' | 'born' | 'died';
+type SortOrder = 'asc' | 'desc';
+
+type Props = {
+ people: PersonWithParents[];
+ selectedSlug: string | undefined;
+ sortBy: string | null;
+ sortOrder: string | null;
+ onSort: (field: SortBy) => void;
+};
+
+const SORTABLE: SortBy[] = ['name', 'sex', 'born', 'died'];
+
+export const PeopleTable: React.FC = ({
+ people,
+ selectedSlug,
+ sortBy,
+ sortOrder,
+ onSort,
+}) => {
+ const renderSortIcon = (field: SortBy) => {
+ if (sortBy !== field) {
+ return
;
+ }
+
+ return sortOrder === 'desc' ? (
+
+ ) : (
+
+ );
+ };
+
+ const isActive = (field: SortBy) => sortBy === field;
+
+ return (
+
+
+
+ | onSort('name')}
+ style={{ cursor: 'pointer' }}
+ >
+ name {renderSortIcon('name')}
+ |
+
+ onSort('sex')}
+ style={{ cursor: 'pointer' }}
+ >
+ sex {renderSortIcon('sex')}
+ |
+
+ onSort('born')}
+ style={{ cursor: 'pointer' }}
+ >
+ born {renderSortIcon('born')}
+ |
+
+ onSort('died')}
+ style={{ cursor: 'pointer' }}
+ >
+ died {renderSortIcon('died')}
+ |
+
+ mother |
+ father |
+
+
+
+
+ {people.map(person => (
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/PeopleTable/PersonName.tsx b/src/components/PeopleTable/PersonName.tsx
new file mode 100644
index 00000000..6bb1bedb
--- /dev/null
+++ b/src/components/PeopleTable/PersonName.tsx
@@ -0,0 +1,28 @@
+import classNames from 'classnames';
+import { Link, useLocation } from 'react-router-dom';
+import { Person } from '../../types/Person';
+
+type Props = {
+ person?: Person | null;
+ nameFallback?: string; // used when parent not found
+};
+
+export const PersonName: React.FC = ({ person, nameFallback }) => {
+ const location = useLocation();
+
+ if (!person) {
+ return {nameFallback ?? ''};
+ }
+
+ return (
+
+ {person.name}
+
+ );
+};
diff --git a/src/components/PeopleTable/PersonRow.tsx b/src/components/PeopleTable/PersonRow.tsx
new file mode 100644
index 00000000..322816b1
--- /dev/null
+++ b/src/components/PeopleTable/PersonRow.tsx
@@ -0,0 +1,35 @@
+import classNames from 'classnames';
+import { PersonWithParents } from '../../types/Person';
+import { PersonName } from './PersonName';
+
+type Props = {
+ person: PersonWithParents;
+ isSelected: boolean;
+};
+
+export const PersonRow: React.FC = ({ person, isSelected }) => (
+
+ |
+
+ |
+ {person.sex} |
+ {person.born} |
+ {person.died} |
+
+
+ {person.mother ? (
+
+ ) : (
+
+ )}
+ |
+
+
+ {person.father ? (
+
+ ) : (
+
+ )}
+ |
+
+);
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 00000000..81eb77f8
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1 @@
+export const HomePage = () => Home page
;
diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx
new file mode 100644
index 00000000..a7c48762
--- /dev/null
+++ b/src/pages/NotFoundPage.tsx
@@ -0,0 +1 @@
+export const NotFoundPage = () => Page not found
;
diff --git a/src/pages/PeoplePage.tsx b/src/pages/PeoplePage.tsx
new file mode 100644
index 00000000..05999970
--- /dev/null
+++ b/src/pages/PeoplePage.tsx
@@ -0,0 +1,167 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { useParams, useSearchParams } from 'react-router-dom';
+import { getPeople } from '../api/people';
+import { Loader } from '../components/Loader';
+import { PeopleTable } from '../components/PeopleTable/PeopleTable';
+import { PersonWithParents } from '../types/Person';
+
+type SortBy = 'name' | 'sex' | 'born' | 'died';
+type SortOrder = 'asc' | 'desc';
+
+const VALID_SORT_BY: SortBy[] = ['name', 'sex', 'born', 'died'];
+
+function isValidSortBy(value: string | null): value is SortBy {
+ return !!value && VALID_SORT_BY.includes(value as SortBy);
+}
+
+function normalize(s: string) {
+ return s.trim().toLowerCase();
+}
+
+export const PeoplePage: React.FC = () => {
+ const { slug } = useParams();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const [people, setPeople] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [hasError, setHasError] = useState(false);
+
+ const queryFromUrl = searchParams.get('query') ?? '';
+ const sortByFromUrl = searchParams.get('sortBy');
+ const sortOrderFromUrl =
+ (searchParams.get('sortOrder') as SortOrder | null) ?? null;
+
+ const [queryDraft, setQueryDraft] = useState(queryFromUrl);
+
+ // keep input synced on initial load / back-forward navigation
+ useEffect(() => {
+ setQueryDraft(queryFromUrl);
+ }, [queryFromUrl]);
+
+ useEffect(() => {
+ setIsLoading(true);
+ setHasError(false);
+
+ getPeople()
+ .then(setPeople)
+ .catch(() => setHasError(true))
+ .finally(() => setIsLoading(false));
+ }, []);
+
+ // debounce URL update for query (500ms)
+ useEffect(() => {
+ const id = window.setTimeout(() => {
+ const next = new URLSearchParams(searchParams);
+
+ const value = queryDraft.trim();
+
+ if (value) {
+ next.set('query', value);
+ } else {
+ next.delete('query');
+ }
+
+ setSearchParams(next, { replace: true });
+ }, 500);
+
+ return () => window.clearTimeout(id);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [queryDraft]);
+
+ const preparedPeople = useMemo(() => {
+ const q = normalize(queryFromUrl);
+
+ let result = people;
+
+ if (q) {
+ result = result.filter(p => {
+ const name = normalize(p.name);
+ const motherName = normalize(p.motherName ?? '');
+ const fatherName = normalize(p.fatherName ?? '');
+
+ return (
+ name.includes(q) || motherName.includes(q) || fatherName.includes(q)
+ );
+ });
+ }
+
+ if (isValidSortBy(sortByFromUrl)) {
+ const order: SortOrder = sortOrderFromUrl === 'desc' ? 'desc' : 'asc';
+ const dir = order === 'asc' ? 1 : -1;
+
+ result = [...result].sort((a, b) => {
+ const field = sortByFromUrl;
+
+ const av = a[field];
+ const bv = b[field];
+
+ if (typeof av === 'number' && typeof bv === 'number') {
+ return (av - bv) * dir;
+ }
+
+ return String(av).localeCompare(String(bv)) * dir;
+ });
+ }
+
+ return result;
+ }, [people, queryFromUrl, sortByFromUrl, sortOrderFromUrl]);
+
+ // Highlight: only if slug exists in full people list
+ const selectedSlug = useMemo(() => {
+ if (!slug) return undefined;
+ return people.some(p => p.slug === slug) ? slug : undefined;
+ }, [slug, people]);
+
+ const onSort = (field: SortBy) => {
+ const next = new URLSearchParams(searchParams);
+ const currentBy = next.get('sortBy');
+ const currentOrder = (next.get('sortOrder') as SortOrder | null) ?? null;
+
+ if (currentBy !== field) {
+ next.set('sortBy', field);
+ next.set('sortOrder', 'asc');
+ } else {
+ const nextOrder: SortOrder = currentOrder === 'asc' ? 'desc' : 'asc';
+ next.set('sortOrder', nextOrder);
+ }
+
+ setSearchParams(next);
+ };
+
+ return (
+
+
Peope page
+
+
+
+
+ setQueryDraft(e.target.value)}
+ />
+
+
+
+ {isLoading &&
}
+ {hasError && (
+
Failed to load people
+ )}
+
+ {!isLoading && !hasError && (
+
+ )}
+
+ );
+};
diff --git a/src/types/Person.ts b/src/types/Person.ts
index 5e65e546..35e9d953 100644
--- a/src/types/Person.ts
+++ b/src/types/Person.ts
@@ -1,9 +1,16 @@
+export type Sex = 'm' | 'f';
+
export interface Person {
+ name: string;
+ sex: Sex;
born: number;
died: number;
- fatherName: string | null;
- motherName: string | null;
- name: string;
- sex: string;
+ motherName?: string;
+ fatherName?: string;
slug: string;
}
+
+export interface PersonWithParents extends Person {
+ mother?: Person | null;
+ father?: Person | null;
+}