From d0ac4cae1ec8b4f1c7d55ce8caef41cfed7056c7 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 4 May 2026 15:27:07 +0100 Subject: [PATCH 1/2] solution --- .eslintrc.cjs | 2 +- README.md | 2 +- cypress/integration/page.spec.js | 6 +- package-lock.json | 129 +++------ package.json | 2 +- src/App.tsx | 161 ++--------- src/TodosContext/Context.tsx | 254 ++++++++++++++++++ src/TodosContext/index.ts | 0 .../TodoAppFooter/TodoAppFooter.tsx | 93 +++++++ src/components/TodoAppFooter/index.ts | 1 + src/components/TodoAppList/TodoAppList.tsx | 15 ++ src/components/TodoAppList/index.ts | 1 + .../TodoAppheader/TodoAppHeader.tsx | 73 +++++ src/components/TodoAppheader/index.ts | 1 + src/components/TodoItem/TodoItem.tsx | 130 +++++++++ src/components/TodoItem/index.ts | 1 + src/index.tsx | 7 +- src/styles/todo-list.scss | 1 + src/styles/todoapp.scss | 8 +- src/type.tsx | 7 + vite.config.ts | 6 +- 21 files changed, 656 insertions(+), 244 deletions(-) create mode 100644 src/TodosContext/Context.tsx create mode 100644 src/TodosContext/index.ts create mode 100644 src/components/TodoAppFooter/TodoAppFooter.tsx create mode 100644 src/components/TodoAppFooter/index.ts create mode 100644 src/components/TodoAppList/TodoAppList.tsx create mode 100644 src/components/TodoAppList/index.ts create mode 100644 src/components/TodoAppheader/TodoAppHeader.tsx create mode 100644 src/components/TodoAppheader/index.ts create mode 100644 src/components/TodoItem/TodoItem.tsx create mode 100644 src/components/TodoItem/index.ts create mode 100644 src/type.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b51149cf5..eed17345a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -90,7 +90,7 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unused-vars': ['error'], - '@typescript-eslint/indent': ['error', 2], + '@typescript-eslint/indent': 'off', '@typescript-eslint/ban-types': ['error', { extendDefaults: true, types: { diff --git a/README.md b/README.md index 903c876f9..12116b0c5 100644 --- a/README.md +++ b/README.md @@ -33,4 +33,4 @@ Implement a simple [TODO app](https://mate-academy.github.io/react_todo-app/) th - Implement a solution following the [React task guidelines](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline). - Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript). - Open another terminal and run tests with `npm test` to ensure your solution is correct. -- Replace `` with your GitHub username in the [DEMO LINK](https://.github.io/react_todo-app/) and add it to the PR description. +- Replace `` with your GitHub username in the [DEMO LINK](https://Bohdan259.github.io/react_todo-app/) and add it to the PR description. diff --git a/cypress/integration/page.spec.js b/cypress/integration/page.spec.js index 0875764e1..484dc34a8 100644 --- a/cypress/integration/page.spec.js +++ b/cypress/integration/page.spec.js @@ -7,7 +7,11 @@ const page = { todosCounter: () => cy.byDataCy('TodosCounter'), clearCompletedButton: () => cy.byDataCy('ClearCompletedButton'), footer: () => cy.byDataCy('Footer'), - localStorage: () => cy.getAllLocalStorage().its('http://localhost:3001'), + localStorage: () => + cy.getAllLocalStorage().then(storage => { + const domain = Object.keys(storage)[0]; + return storage[domain]; + }), data: () => page.localStorage().then(({ todos = '[]' }) => JSON.parse(todos)), visit: (initialTodos) => { diff --git a/package-lock.json b/package-lock.json index 1f19b4743..263a39a8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,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", @@ -87,6 +87,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 +436,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, @@ -457,6 +459,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": "^14 || ^16 || >=18" } @@ -1170,10 +1173,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", @@ -1210,15 +1214,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 +1247,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 +1259,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 +1277,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 +1303,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 +1319,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 +1469,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 +1516,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 +1544,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 +1557,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 +1570,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 +1632,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 +1647,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 +1834,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" } @@ -2171,8 +2160,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 +2175,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 +2188,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 +2199,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 +2265,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 +2311,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 +2379,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 +2443,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 +2767,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 +2912,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 +2963,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 +2984,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -3091,7 +3078,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -3104,7 +3090,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 +3108,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" }, @@ -3293,7 +3277,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", @@ -3491,6 +3474,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 +3726,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 +3738,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 +3754,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 +3763,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 +3938,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 +4210,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 +4308,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 +4406,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 +5154,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 +5508,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 +5539,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 +5685,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 +5775,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 +5784,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 +5796,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 +5807,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 +5900,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 +6274,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 +7014,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 +7112,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 +7145,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 +7159,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 +7168,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 +7203,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 +7215,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 +7224,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 +7235,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 +7832,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 +8377,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -8492,6 +8456,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 +8485,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 +8617,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 +8629,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 +8637,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 +8649,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" @@ -8712,7 +8678,6 @@ "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 +8696,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 +8713,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 +8725,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 +8749,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 +8765,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 +9097,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 +9135,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" } @@ -9381,7 +9341,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 +9350,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 +9366,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 +9564,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 +9602,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 +10012,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 +10202,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 +10236,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 +10325,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 +10358,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 +10671,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 +10774,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 +10792,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 +10801,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 91d7489b9..446974833 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,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 a399287bd..eee1f21c1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,156 +1,25 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; +import React, { useContext } from 'react'; +import { TodoAppHeader } from './components/TodoAppheader'; +import { TodoAppList } from './components/TodoAppList'; +import { TodoAppFooter } from './components/TodoAppFooter'; +import { TodosContext } from './TodosContext/Context'; export const App: React.FC = () => { + const todos = useContext(TodosContext).todos; + const isEmpty = todos.length === 0; + return (

todos

-
-
- {/* this button should have `active` class only if all todos are completed */} -
- -
- {/* This is a completed todo */} -
- - - - Completed Todo - - - {/* Remove button appears only on hover */} - -
- - {/* This todo is an active todo */} -
- - - - Not Completed Todo - - - -
- - {/* This todo is being edited */} -
- - - {/* This form is shown instead of the title and remove button */} -
- -
-
- - {/* This todo is in loadind state */} -
- - - - Todo is being saved now - - - -
-
- - {/* Hide the footer if there are no todos */} -
- - 3 items left - - - {/* Active link should have the 'selected' class */} - - - {/* this button should be disabled if there are no completed todos */} - -
+ + {!isEmpty && ( + <> + + + + )}
); diff --git a/src/TodosContext/Context.tsx b/src/TodosContext/Context.tsx new file mode 100644 index 000000000..bed88f527 --- /dev/null +++ b/src/TodosContext/Context.tsx @@ -0,0 +1,254 @@ +import React, { createContext, useEffect, useState } from 'react'; +import { FilterOptions, Todo } from '../type'; + +export const TodosContext = createContext<{ + todos: Todo[] | []; + setTodos: React.Dispatch>; +}>({ + todos: [], + setTodos: () => {}, +}); +export const NewTodoContext = createContext<{ + addTodo: Todo | null; + setAddTodo: React.Dispatch>; +}>({ + addTodo: null, + setAddTodo: () => {}, +}); + +type Props = { + children: React.ReactNode; +}; + +export const TodosFilterContext = createContext<{ + filter: FilterOptions; + setFilter: React.Dispatch>; +}>({ + filter: 'all', + setFilter: () => {}, +}); + +export const VisibleTodosContext = createContext<{ + visibleTodos: Todo[] | []; +}>({ + visibleTodos: [], +}); + +export const DeletingIdsContext = createContext<{ + deletingIds: number[] | []; + setDeletingIds: React.Dispatch>; +}>({ + deletingIds: [], + setDeletingIds: () => {}, +}); + +export const ClickedClearCompletedContext = createContext<{ + setClickedClearCompleted: React.Dispatch>; +}>({ + setClickedClearCompleted: () => {}, +}); + +export const ToggleTodosContext = createContext<{ + setToggleTodo: (todo: Todo) => void; +}>({ + setToggleTodo: () => {}, +}); + +export const ClickedToggleAllContext = createContext<{ + setClickToggleAll: React.Dispatch>; +}>({ + setClickToggleAll: () => {}, +}); + +export const DoubleClickEditContext = createContext<{ + editingTodo: Todo | null; + setEditingTodo: React.Dispatch>; +}>({ + editingTodo: null, + setEditingTodo: () => {}, +}); + +export const TodosProvider: React.FC = ({ children }) => { + const [todos, setTodos] = useState([]); + const [addTodo, setAddTodo] = useState(null); + const [filter, setFilter] = useState('all'); + const [deletingIds, setDeletingIds] = useState([]); + const [clickedClearCompleted, setClickedClearCompleted] = + useState(false); + const [toggleTodo, setToggleTodo] = useState(null); + const [clickToggleAll, setClickToggleAll] = useState(false); + const [editingTodo, setEditingTodo] = useState(null); + + const visibleTodos = todos.filter(todo => { + if (filter === 'active') { + return !todo.completed; + } + + if (filter === 'completed') { + return todo.completed; + } + + return true; + }); + + function saveTodos(arr: Todo[]) { + if (arr.length === 0) { + localStorage.removeItem('todos'); + } else { + localStorage.setItem('todos', JSON.stringify(arr)); + } + } + + useEffect(() => { + const saved = localStorage.getItem('todos'); + + if (saved) { + setTodos(JSON.parse(saved)); + } + }, []); + + function saveTodo(todo: Todo[]) { + setTodos(prev => { + const updatedTodos = [...prev, ...todo]; + + saveTodos(updatedTodos); + + return updatedTodos; + }); + } + + useEffect(() => { + if (addTodo) { + saveTodo([addTodo]); + setAddTodo(null); + } + }, [addTodo]); + + function deleteTodo() { + setTodos(prev => { + const updatedTodos = prev.filter(todo => !deletingIds.includes(todo.id)); + + saveTodos(updatedTodos); + + return updatedTodos; + }); + setDeletingIds([]); + } + + useEffect(() => { + if (deletingIds.length > 0) { + deleteTodo(); + } + }, [deletingIds]); + + function clearCompleted() { + setTodos(prev => { + const updatedTodos = prev.filter(todo => !todo.completed); + + saveTodos(updatedTodos); + + return updatedTodos; + }); + setClickedClearCompleted(false); + } + + useEffect(() => { + if (clickedClearCompleted) { + clearCompleted(); + } + }, [clickedClearCompleted]); + + function toggleTodoStatus(todo: Todo) { + setTodos(prev => { + const index = prev.findIndex(t => t.id === todo.id); + const updatedTodos = [...prev]; + + updatedTodos[index] = todo; + saveTodos(updatedTodos); + + return updatedTodos; + }); + setToggleTodo(null); + } + + useEffect(() => { + if (toggleTodo) { + toggleTodoStatus(toggleTodo); + } + }, [toggleTodo]); + + function toggleAll() { + setTodos(prev => { + const allCompleted = prev.every(todo => todo.completed); + const updatedTodos = prev.map(todo => ({ + ...todo, + completed: !allCompleted, + })); + + saveTodos(updatedTodos); + + return updatedTodos; + }); + setClickToggleAll(false); + } + + useEffect(() => { + if (clickToggleAll) { + toggleAll(); + } + }, [clickToggleAll]); + + function editTodo(todo: Todo) { + setTodos(prev => { + const updatedTodos = prev.map(t => { + return t.id === todo?.id ? todo : t; + }); + + saveTodos(updatedTodos); + + return updatedTodos; + }); + setEditingTodo(null); + } + + useEffect(() => { + if (editingTodo) { + editTodo(editingTodo); + } + }, [editingTodo]); + + return ( + + + + + + + + + + {children} + + + + + + + + + + ); +}; diff --git a/src/TodosContext/index.ts b/src/TodosContext/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/TodoAppFooter/TodoAppFooter.tsx b/src/components/TodoAppFooter/TodoAppFooter.tsx new file mode 100644 index 000000000..639426284 --- /dev/null +++ b/src/components/TodoAppFooter/TodoAppFooter.tsx @@ -0,0 +1,93 @@ +import React, { useCallback, useContext } from 'react'; +import { + ClickedClearCompletedContext, + TodosContext, + TodosFilterContext, +} from '../../TodosContext/Context'; +import classNames from 'classnames'; + +export const TodoAppFooter: React.FC = () => { + const filter = useContext(TodosFilterContext).filter; + const setFilter = useContext(TodosFilterContext).setFilter; + const todos = useContext(TodosContext).todos; + const counter = todos.filter(todo => !todo.completed).length; + const setClickClearButton = useContext( + ClickedClearCompletedContext, + ).setClickedClearCompleted; + const isCompletedTodo = todos.some(todo => todo.completed); + + const clickFilterAll = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setFilter('all'); + }, + [filter, todos], + ); + + const clickFilterActive = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setFilter('active'); + }, + [filter, todos], + ); + + const clickFilterCompleted = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setFilter('completed'); + }, + [filter, todos], + ); + + return ( + + ); +}; diff --git a/src/components/TodoAppFooter/index.ts b/src/components/TodoAppFooter/index.ts new file mode 100644 index 000000000..00b1d7d73 --- /dev/null +++ b/src/components/TodoAppFooter/index.ts @@ -0,0 +1 @@ +export { TodoAppFooter } from './TodoAppFooter'; diff --git a/src/components/TodoAppList/TodoAppList.tsx b/src/components/TodoAppList/TodoAppList.tsx new file mode 100644 index 000000000..83b8440a5 --- /dev/null +++ b/src/components/TodoAppList/TodoAppList.tsx @@ -0,0 +1,15 @@ +import React, { useContext } from 'react'; +import { TodoItem } from '../TodoItem'; +import { VisibleTodosContext } from '../../TodosContext/Context'; + +export const TodoAppList: React.FC = () => { + const todos = useContext(VisibleTodosContext).visibleTodos; + + return ( +
+ {todos.map(todo => ( + + ))} +
+ ); +}; diff --git a/src/components/TodoAppList/index.ts b/src/components/TodoAppList/index.ts new file mode 100644 index 000000000..b251b44e5 --- /dev/null +++ b/src/components/TodoAppList/index.ts @@ -0,0 +1 @@ +export { TodoAppList } from './TodoAppList'; diff --git a/src/components/TodoAppheader/TodoAppHeader.tsx b/src/components/TodoAppheader/TodoAppHeader.tsx new file mode 100644 index 000000000..85faccde3 --- /dev/null +++ b/src/components/TodoAppheader/TodoAppHeader.tsx @@ -0,0 +1,73 @@ +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { + ClickedToggleAllContext, + NewTodoContext, + TodosContext, +} from '../../TodosContext/Context'; + +export const TodoAppHeader: React.FC = () => { + const [title, setTitle] = useState(''); + const setTodo = useContext(NewTodoContext).setAddTodo; + const titleField = useRef(null); + const todos = useContext(TodosContext).todos; + const allTodosCompleted = + todos.length > 0 && todos.every(todo => todo.completed); + const setClickToggleAll = useContext( + ClickedToggleAllContext, + ).setClickToggleAll; + + useEffect(() => { + if (titleField.current) { + titleField.current.focus(); + } + }, [todos]); + + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!title.trim()) { + return; + } else { + setTodo({ + id: +new Date(), + title: title.trim(), + completed: false, + }); + setTitle(''); + } + }, + [title], + ); + + return ( +
+ {/* this button should have `active` class only if all todos are completed */} + {todos.length > 0 && +
+ ); +}; diff --git a/src/components/TodoAppheader/index.ts b/src/components/TodoAppheader/index.ts new file mode 100644 index 000000000..efaaa9f4f --- /dev/null +++ b/src/components/TodoAppheader/index.ts @@ -0,0 +1 @@ +export { TodoAppHeader } from './TodoAppHeader'; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx new file mode 100644 index 000000000..766f04068 --- /dev/null +++ b/src/components/TodoItem/TodoItem.tsx @@ -0,0 +1,130 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { Todo } from '../../type'; +import classNames from 'classnames'; +import { + DeletingIdsContext, + DoubleClickEditContext, + ToggleTodosContext, +} from '../../TodosContext/Context'; + +type Props = { + todo: Todo; +}; + +export const TodoItem: React.FC = ({ todo }) => { + const setDeleteTodo = useContext(DeletingIdsContext).setDeletingIds; + const onClickDelete = useCallback(() => { + setDeleteTodo(prev => [...prev, todo.id]); + }, []); + const setToggleTodo = useContext(ToggleTodosContext).setToggleTodo; + const [editTitle, setEditTitle] = useState(todo.title); + const [isDoubleClicked, setIsDoubleClicked] = useState(null); + const setEditingTodo = useContext(DoubleClickEditContext).setEditingTodo; + const clickEdit = isDoubleClicked === todo.id; + const titleField = React.useRef(null); + + useEffect(() => { + if (titleField.current && clickEdit) { + titleField.current.focus(); + } + }, [clickEdit]); + + const saveEdit = useCallback(() => { + if (!editTitle.trim()) { + setDeleteTodo(prev => [...prev, todo.id]); + } + + if (editTitle === todo.title) { + setIsDoubleClicked(null); + } else { + setEditingTodo({ + id: todo.id, + title: editTitle.trim(), + completed: todo.completed, + }); + setIsDoubleClicked(null); + } + }, [editTitle]); + + const onSubmitEdit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + saveEdit(); + }, + [editTitle], + ); + + const onKeyDownEdit = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setIsDoubleClicked(null); + } + }, + [], + ); + + return ( +
+ + {clickEdit && ( +
+ setEditTitle(e.target.value)} + onKeyDown={onKeyDownEdit} + onBlur={saveEdit} + /> +
+ )} + + {!clickEdit && ( + <> + { + setIsDoubleClicked(todo.id); + }} + > + {todo.title} + + + + + )} +
+ ); +}; diff --git a/src/components/TodoItem/index.ts b/src/components/TodoItem/index.ts new file mode 100644 index 000000000..e8da08541 --- /dev/null +++ b/src/components/TodoItem/index.ts @@ -0,0 +1 @@ +export { TodoItem } from './TodoItem'; diff --git a/src/index.tsx b/src/index.tsx index b2c38a17a..2de1d2aa3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,12 @@ import { createRoot } from 'react-dom/client'; import './styles/index.scss'; import { App } from './App'; +import { TodosProvider } from './TodosContext/Context'; const container = document.getElementById('root') as HTMLDivElement; -createRoot(container).render(); +createRoot(container).render( + + + , +); diff --git a/src/styles/todo-list.scss b/src/styles/todo-list.scss index 4576af434..90d81864a 100644 --- a/src/styles/todo-list.scss +++ b/src/styles/todo-list.scss @@ -80,6 +80,7 @@ font-weight: inherit; color: inherit; + box-sizing: border-box; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); diff --git a/src/styles/todoapp.scss b/src/styles/todoapp.scss index e289a9458..c4c0e8615 100644 --- a/src/styles/todoapp.scss +++ b/src/styles/todoapp.scss @@ -66,7 +66,8 @@ color: inherit; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - + + box-sizing: border-box; border: none; background: rgba(0, 0, 0, 0.01); box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); @@ -123,6 +124,11 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + &:disabled { + opacity: 0.5; + cursor: default; + pointer-events: none; + } &:hover { text-decoration: underline; } diff --git a/src/type.tsx b/src/type.tsx new file mode 100644 index 000000000..4c2752747 --- /dev/null +++ b/src/type.tsx @@ -0,0 +1,7 @@ +export interface Todo { + id: number; + title: string; + completed: boolean; +} + +export type FilterOptions = 'all' | 'active' | 'completed'; diff --git a/vite.config.ts b/vite.config.ts index 5a33944a9..e08cbeb8b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + server: { + port: 3000, + }, +}) \ No newline at end of file From 2e57b2e74449426a05b8be251d61f3e2480cbb2f Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 4 May 2026 15:27:33 +0100 Subject: [PATCH 2/2] sol --- src/components/TodoAppheader/TodoAppHeader.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/TodoAppheader/TodoAppHeader.tsx b/src/components/TodoAppheader/TodoAppHeader.tsx index 85faccde3..5883357bd 100644 --- a/src/components/TodoAppheader/TodoAppHeader.tsx +++ b/src/components/TodoAppheader/TodoAppHeader.tsx @@ -48,13 +48,14 @@ export const TodoAppHeader: React.FC = () => { return (
{/* this button should have `active` class only if all todos are completed */} - {todos.length > 0 && -