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

+ + } /> - - - - - - - - + } /> - - - - - - + + } /> + } /> + - - - - - - -
namesexborn
Carolus Haverbekem1832
Emma de Millianof1842
-
- ); - } -} + } /> + +
+ ); +}; 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 sort; + } + + return sortOrder === 'desc' ? ( + desc + ) : ( + asc + ); + }; + + const isActive = (field: SortBy) => sortBy === field; + + return ( + + + + + + + + + + + + + + + + + + {people.map(person => ( + + ))} + +
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')} + motherfather
+ ); +}; 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; +}