diff --git a/README.md b/README.md index 47a1add059..dddbd3147f 100644 --- a/README.md +++ b/README.md @@ -47,4 +47,4 @@ Implement the ability to edit a todo title on double click: - Implement a solution following the [React task guideline](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). -- Replace `` with your Github username in the [DEMO LINK](https://.github.io/react_todo-app-with-api/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://Bohdan259.github.io/react_todo-app-with-api/) and add it to the PR description. diff --git a/package-lock.json b/package-lock.json index 19701e8788..4bea4c4e1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ }, "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/node": "^20.14.10", @@ -89,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", @@ -448,6 +449,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, @@ -470,6 +472,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": "^14 || ^16 || >=18" } @@ -1183,10 +1186,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", @@ -1223,15 +1227,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", @@ -1258,7 +1260,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" }, @@ -1271,7 +1272,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" }, @@ -1290,7 +1290,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", @@ -1317,7 +1316,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" }, @@ -1334,7 +1332,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" } @@ -1485,7 +1482,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" }, @@ -1533,7 +1529,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" } @@ -1562,7 +1557,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" @@ -1576,7 +1570,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", @@ -1590,8 +1583,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", @@ -1653,7 +1645,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", @@ -1669,7 +1660,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" }, @@ -1857,7 +1847,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" } @@ -2184,8 +2173,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", @@ -2200,8 +2188,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", @@ -2214,6 +2201,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" @@ -2224,6 +2212,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": "*" } @@ -2298,6 +2287,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", @@ -2343,7 +2333,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", @@ -2412,7 +2401,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", @@ -2477,6 +2465,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" }, @@ -2800,7 +2789,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" } @@ -2946,8 +2934,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", @@ -2998,8 +2985,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", @@ -3020,6 +3006,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -3113,7 +3100,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -3126,7 +3112,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", @@ -3145,7 +3130,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" }, @@ -3315,7 +3299,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", @@ -3512,6 +3495,7 @@ "integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", @@ -3763,7 +3747,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" }, @@ -3776,7 +3759,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" @@ -3793,7 +3775,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" } @@ -3803,7 +3784,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" } @@ -3988,6 +3968,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" @@ -4259,6 +4240,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", @@ -4356,6 +4338,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" }, @@ -4453,6 +4436,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", @@ -5200,7 +5184,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" } @@ -5555,7 +5538,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", @@ -5587,7 +5569,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" }, @@ -5734,7 +5715,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" } @@ -5825,7 +5805,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" } @@ -5835,7 +5814,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" }, @@ -5848,7 +5826,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" }, @@ -5860,8 +5837,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", @@ -5954,7 +5930,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" } @@ -6329,7 +6304,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" } @@ -7070,7 +7044,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" }, @@ -7169,7 +7142,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" } @@ -7203,7 +7175,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", @@ -7218,7 +7189,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" } @@ -7228,7 +7198,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", @@ -7264,7 +7233,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" }, @@ -7277,7 +7245,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" } @@ -7287,7 +7254,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" }, @@ -7299,15 +7265,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" }, @@ -7898,7 +7862,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", @@ -8443,6 +8406,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -8521,6 +8485,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" @@ -8549,6 +8514,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" }, @@ -8679,7 +8645,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" }, @@ -8692,7 +8657,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" } @@ -8701,6 +8665,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" }, @@ -8712,6 +8677,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" @@ -8754,7 +8720,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", @@ -8773,7 +8738,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", @@ -8791,7 +8755,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" }, @@ -8804,7 +8767,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" }, @@ -8829,7 +8791,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" @@ -8846,7 +8807,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" }, @@ -9184,6 +9144,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", @@ -9221,7 +9182,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" } @@ -9428,7 +9388,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" @@ -9438,15 +9397,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" @@ -9456,8 +9413,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", @@ -9655,7 +9611,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" }, @@ -9694,8 +9649,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", @@ -10105,7 +10059,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" }, @@ -10296,6 +10249,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" @@ -10329,8 +10283,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", @@ -10419,7 +10372,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" @@ -10453,6 +10405,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", @@ -10765,8 +10718,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", @@ -10869,7 +10821,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", @@ -10888,7 +10839,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" } @@ -10898,7 +10848,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 b6062525ab..6d0f20adcc 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "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/node": "^20.14.10", diff --git a/src/App.tsx b/src/App.tsx index 81e011f432..fa1c25fab2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,369 @@ -/* eslint-disable max-len */ +/* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; -import { UserWarning } from './UserWarning'; - -const USER_ID = 0; +import React, { useEffect, useState } from 'react'; +import { + deleteTodo, + editingTodo, + getTodos, + postTodo, + updateTodo, + USER_ID, +} from './api/todos'; +import { TodoAppHeader } from './components/TodoAppHeader'; +import { TodoAppMain } from './components/TodoAppMain'; +import { TodoMainFooter } from './components/TodoMainFooter'; +import classNames from 'classnames'; +import { FilterOptions } from './components/TodoMainFooter/TodoMainFooter'; +import { EditTodo, Todo } from './types/Todo'; +import { ErrorMessage } from './types/ErrorMessage'; export const App: React.FC = () => { + const [notificationError, setNotificationError] = useState(false); + + const [errorTodos, setErrorTodos] = useState(false); + const [errorTitle, setErrorTitle] = useState(false); + const [errorPostTodo, setErrorPostTodo] = useState(false); + const [loadingPostTodo, setLoadingPostTodo] = useState(false); + + const [filterTodos, setFilterTodos] = useState('all'); + const [todos, setTodos] = useState([]); + const [loadingTodos, setLoadingTodos] = useState(false); + const [counter, setCounter] = useState(0); + + const [titleTodo, setTitleTodo] = useState(''); + const [tempTodo, setTempTodo] = useState(null); + + const [loaderDelete, setLoaderDelete] = useState(false); + const [selectedDeleteTodo, setSelectedDeleteTodo] = useState( + null, + ); + + const [deletingIds, setDeletingIds] = useState([]); + + const [deleteError, setDeleteError] = useState(false); + const [clearButton, setClearButton] = useState(false); + const [loaderDeleteCompleted, setLoaderDeleteCompleted] = useState(false); + + const [toggleTodo, setToggleTodo] = useState(null); + const [updatingIds, setUpdatingIds] = useState([]); + + const [loaderToggle, setLoaderToggle] = useState(false); + const [toggleError, setToggleError] = useState(false); + const [isClickToggleAllButton, setsIClickToggleAllButton] = useState(false); + const [loadingIds, setLoadingIds] = useState([]); + const [editTodo, setEditTodo] = useState(null); + const [selectedUpdateTodo, setSelectedUpdateTodo] = useState( + null, + ); + + const includesFalseCompleted = todos.every(todo => todo.completed); + + const visibleTodos = todos.filter(todo => { + if (filterTodos === 'active') { + return todo.completed === false; + } + + if (filterTodos === 'completed') { + return todo.completed; + } + + return todos; + }); + + function loadTodos() { + getTodos() + .then(serverTodos => { + setTodos(serverTodos); + setCounter(serverTodos.length); + }) + .catch(() => { + setErrorTodos(true); + setNotificationError(true); + }) + .finally(() => { + setLoadingTodos(false); + setTimeout(() => { + setNotificationError(false); + setErrorTodos(false); + }, 3000); + }); + } + + useEffect(() => { + setLoadingTodos(true); + loadTodos(); + }, []); + + function addTodo(newTodo: Todo) { + setLoadingPostTodo(true); + const { id, ...data } = newTodo; + + postTodo(data) + .then(serverTodo => { + setTempTodo(serverTodo); + setTodos(currentTodo => [...currentTodo, serverTodo]); + setCounter(currentCounter => currentCounter + 1); + }) + .catch(() => { + setNotificationError(true); + setErrorPostTodo(true); + }) + .finally(() => { + setTempTodo(null); + setLoadingPostTodo(false); + + setTimeout(() => { + setNotificationError(false); + setErrorPostTodo(false); + }, 3000); + }); + } + + useEffect(() => { + if (tempTodo) { + addTodo(tempTodo); + } + }, [tempTodo]); + + useEffect(() => { + if (notificationError) { + setTimeout(() => { + setNotificationError(false); + }, 3000); + } + }, [notificationError]); + + function deleteById(todoId: number, isSingle = false) { + setDeletingIds(prev => [...prev, todoId]); + + return deleteTodo(todoId) + .then(() => { + setTodos(currentTodos => + currentTodos.filter(todo => todo.id !== todoId), + ); + setCounter(currentCounter => currentCounter - 1); + }) + .catch(() => { + setNotificationError(true); + setDeleteError(true); + }) + .finally(() => { + setDeletingIds(prev => prev.filter(id => id !== todoId)); + if (isSingle) { + setLoaderDelete(false); + setSelectedDeleteTodo(null); + } + + setTimeout(() => { + setNotificationError(false); + setDeleteError(false); + }, 3000); + }); + } + + useEffect(() => { + if (selectedDeleteTodo) { + setLoaderDelete(true); + deleteById(selectedDeleteTodo, true); + } + }, [selectedDeleteTodo]); + + function deletAllCompletedTodos(todosArr: Todo[]) { + const completedTodos = todosArr.filter(todo => todo.completed); + + Promise.all(completedTodos.map(todo => deleteById(todo.id))).finally(() => { + setLoaderDeleteCompleted(false); + setClearButton(false); + }); + } + + useEffect(() => { + if (clearButton) { + setLoaderDeleteCompleted(true); + deletAllCompletedTodos(todos); + } + }, [clearButton]); + + function todoUpdateById(todo: Todo, isSingle = false) { + const { id, completed } = todo; + + setUpdatingIds(prev => [...prev, id]); + + return updateTodo({ id, completed }) + .then(newTodo => { + setTodos(currentTodos => { + const newPosts = [...currentTodos]; + const index = currentTodos.findIndex(t => t.id === newTodo.id); + + newPosts.splice(index, 1, newTodo); + + return newPosts; + }); + }) + .catch(() => { + setNotificationError(true); + setToggleError(true); + }) + .finally(() => { + setUpdatingIds(prev => prev.filter(todoId => todoId !== id)); + if (isSingle) { + setLoaderToggle(false); + setToggleTodo(null); + } + + setTimeout(() => { + setNotificationError(false); + setToggleError(false); + }, 3000); + }); + } + + useEffect(() => { + if (toggleTodo) { + setLoaderToggle(true); + todoUpdateById(toggleTodo, true); + } + }, [toggleTodo]); + + function toggleAll() { + const todosToUpdate = includesFalseCompleted + ? todos + : todos.filter(t => !t.completed); + + setsIClickToggleAllButton(false); + setLoadingIds(todosToUpdate.map(t => t.id)); + + Promise.all(todosToUpdate.map(todo => todoUpdateById(todo))).finally(() => { + setLoadingIds([]); + }); + } + + useEffect(() => { + if (isClickToggleAllButton) { + toggleAll(); + } + }, [isClickToggleAllButton]); + + function updateEditTodo(todo: EditTodo) { + const { id, title } = todo; + + editingTodo({ id, title }) + .then(newTodo => { + setTodos(currentTodos => { + const newPosts = [...currentTodos]; + const index = currentTodos.findIndex(t => t.id === newTodo.id); + + newPosts.splice(index, 1, newTodo); + + return newPosts; + }); + setSelectedUpdateTodo(null); + }) + .catch(() => { + setNotificationError(true); + setToggleError(true); + }) + .finally(() => { + setEditTodo(null); + setTimeout(() => { + setNotificationError(false); + setToggleError(false); + }, 3000); + }); + } + + useEffect(() => { + if (editTodo) { + updateEditTodo(editTodo); + } + }, [editTodo]); + if (!USER_ID) { - return ; + return ( +
+

+ Please get your userId {' '} + + here + {' '} + and save it in the app

const USER_ID = ...
+ All requests to the API must be sent with this + userId. +

+
+ ); } return ( -
-

- Copy all you need from the prev task: -
- - React Todo App - Add and Delete - -

- -

Styles are already copied

-
+
+

todos

+ +
+ + + + + {todos.length > 0 && ( + + )} +
+ + {/* DON'T use conditional rendering to hide the notification */} + {/* Add the 'hidden' class to hide the message smoothly */} +
+
+
); }; diff --git a/src/UserWarning.tsx b/src/UserWarning.tsx deleted file mode 100644 index fa25838e6a..0000000000 --- a/src/UserWarning.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -export const UserWarning: React.FC = () => ( -
-

- Please get your userId {' '} - - here - {' '} - and save it in the app

const USER_ID = ...
- All requests to the API must be sent with this - userId. -

-
-); diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 0000000000..5b64af10b4 --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,24 @@ +import { EditTodo, NewTodo, Todo, UpdateTodo } from '../types/Todo'; +import { client } from '../utils/fetchClient'; + +export const USER_ID = 4099; + +export const getTodos = () => { + return client.get(`/todos?userId=${USER_ID}`); +}; + +export const postTodo = (newTodo: NewTodo) => { + return client.post('/todos', newTodo); +}; + +export const deleteTodo = (idTodo: number) => { + return client.delete(`/todos/${idTodo}`); +}; + +export const updateTodo = ({ id, completed }: UpdateTodo) => { + return client.patch(`/todos/${id}`, { completed: !completed }); +}; + +export const editingTodo = ({ id, title }: EditTodo) => { + return client.patch(`/todos/${id}`, { title: title }); +}; diff --git a/src/components/TodoAppHeader/TodoAppHeader.tsx b/src/components/TodoAppHeader/TodoAppHeader.tsx new file mode 100644 index 0000000000..adec5cf5d3 --- /dev/null +++ b/src/components/TodoAppHeader/TodoAppHeader.tsx @@ -0,0 +1,111 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import { Todo } from '../../types/Todo'; +import { USER_ID } from '../../api/todos'; +import classNames from 'classnames'; +type Props = { + todos: Todo[]; + includesFalseCompleted: boolean; + loaderClearButton: boolean; + loaderDeleteButton: boolean; + errorPostTodo: boolean; + loadingPostTodo: boolean; + title: string; + setTitle: (title: string) => void; + setNotificationError: (error: boolean) => void; + setTempTodo: (todo: Todo) => void; + setErrorTitle: (error: boolean) => void; + onClickToggleAll: (click: boolean) => void; +}; + +export const TodoAppHeader = React.memo( + ({ + todos, + includesFalseCompleted, + loaderClearButton, + loaderDeleteButton, + errorPostTodo, + loadingPostTodo, + title, + setTitle, + setNotificationError, + setTempTodo, + setErrorTitle, + onClickToggleAll, + }) => { + const titleField = useRef(null); + + useEffect(() => { + if (titleField.current && !loaderDeleteButton) { + titleField.current.focus(); + } + + if (!loadingPostTodo && !errorPostTodo && !loaderClearButton) { + setTitle(''); + } + }, [loadingPostTodo, loaderDeleteButton, loaderClearButton]); + + const inputTitle = useCallback( + (event: React.ChangeEvent) => { + setTitle(event.target.value); + }, + [], + ); + + const inputOnKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + if (!title.trim()) { + setNotificationError(true); + setErrorTitle(true); + setTimeout(() => { + setErrorTitle(false); + }, 3000); + } else { + setTempTodo({ + id: 0, + userId: USER_ID, + title: title.trim(), + completed: false, + }); + } + } + }, + [title], + ); + + return ( +
+ {todos.length > 0 && ( +
+ ); + }, +); + +TodoAppHeader.displayName = 'TodoAppHeader'; diff --git a/src/components/TodoAppHeader/index.ts b/src/components/TodoAppHeader/index.ts new file mode 100644 index 0000000000..efaaa9f4f4 --- /dev/null +++ b/src/components/TodoAppHeader/index.ts @@ -0,0 +1 @@ +export { TodoAppHeader } from './TodoAppHeader'; diff --git a/src/components/TodoAppMain/TodoAppMain.tsx b/src/components/TodoAppMain/TodoAppMain.tsx new file mode 100644 index 0000000000..e67487932d --- /dev/null +++ b/src/components/TodoAppMain/TodoAppMain.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { TransitionGroup, CSSTransition } from 'react-transition-group'; +import { EditTodo, Todo } from '../../types/Todo'; +import { TodoItem } from '../TodoItem'; + +type Props = { + deletingIds: number[]; + updatingIds: number[]; + selectedUpdateTodo: number | null; + editTodo: EditTodo | null; + loadingIds: number[]; + loaderToggle: boolean; + loaderDelete: boolean; + todos: Todo[]; + tempTodo: Todo | null; + loaderClearButton: boolean; + onSelectedTodo: (todoId: number) => void; + setToggleTodo: (todo: Todo) => void; + setEditTodo: (todo: EditTodo) => void; + setSelectedUpdateTodo: (id: number | null) => void; +}; + +export const TodoAppMain = React.memo( + ({ + updatingIds, + deletingIds, + selectedUpdateTodo, + setSelectedUpdateTodo, + editTodo, + setEditTodo, + loadingIds, + setToggleTodo, + loaderClearButton, + todos, + tempTodo, + onSelectedTodo, + }) => { + return ( +
+ + {todos.map(todo => ( + + + + ))} + + {tempTodo && ( + + + + )} + +
+ ); + }, +); + +TodoAppMain.displayName = 'TodoAppMain'; diff --git a/src/components/TodoAppMain/index.ts b/src/components/TodoAppMain/index.ts new file mode 100644 index 0000000000..e574278938 --- /dev/null +++ b/src/components/TodoAppMain/index.ts @@ -0,0 +1 @@ +export { TodoAppMain } from './TodoAppMain'; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx new file mode 100644 index 0000000000..6b7c32139a --- /dev/null +++ b/src/components/TodoItem/TodoItem.tsx @@ -0,0 +1,156 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { EditTodo, Todo } from '../../types/Todo'; +import classNames from 'classnames'; + +type Props = { + updatingIds: number[]; + deletingIds: number[]; + selectedUpdateTodo: number | null; + editTodo: EditTodo | null; + loadingIds: number[]; + todo: Todo; + loaderClearButton: boolean; + tempTodo: Todo | null; + onSelectedTodo: (todoId: number) => void; + setToggleTodo: (todo: Todo) => void; + setEditTodo: (todo: EditTodo) => void; + setSelectedUpdateTodo: (id: number | null) => void; +}; + +export const TodoItem = React.memo( + ({ + updatingIds, + deletingIds, + selectedUpdateTodo, + setSelectedUpdateTodo, + editTodo, + loadingIds, + todo, + loaderClearButton, + tempTodo, + onSelectedTodo, + setToggleTodo, + setEditTodo, + }) => { + const titleField = useRef(null); + const [editTitle, setEditTitle] = useState(todo.title); + const showForm = selectedUpdateTodo === todo.id; + + const saveEdit = useCallback(() => { + if (todo.title === editTitle.trim()) { + setSelectedUpdateTodo(null); + + return; + } + + if (!editTitle.trim()) { + onSelectedTodo(todo.id); + } else { + setEditTodo({ + id: todo.id, + title: editTitle.trim(), + }); + } + }, [editTitle, todo, onSelectedTodo, setEditTitle]); + + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + saveEdit(); + }, + [editTitle, todo, onSelectedTodo, setEditTitle], + ); + + const handleEsc = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setSelectedUpdateTodo(null); + } + }, + [setSelectedUpdateTodo], + ); + + useEffect(() => { + if (titleField.current && selectedUpdateTodo !== null) { + titleField.current.focus(); + } + }, [selectedUpdateTodo]); + + return ( +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + {showForm && ( +
+ setEditTitle(event.target.value)} + value={editTitle} + onKeyDown={handleEsc} + onBlur={saveEdit} + /> +
+ )} + + {!showForm && ( + <> + setSelectedUpdateTodo(todo.id)} + > + {todo.title} + + + {/* Remove button appears only on hover */} + + + )} + + {/* overlay will cover the todo while it is being deleted or updated */} +
+
+
+
+
+ ); + }, +); + +TodoItem.displayName = 'TodoItem'; diff --git a/src/components/TodoItem/index.ts b/src/components/TodoItem/index.ts new file mode 100644 index 0000000000..e8da085412 --- /dev/null +++ b/src/components/TodoItem/index.ts @@ -0,0 +1 @@ +export { TodoItem } from './TodoItem'; diff --git a/src/components/TodoMainFooter/TodoMainFooter.tsx b/src/components/TodoMainFooter/TodoMainFooter.tsx new file mode 100644 index 0000000000..fb1b0737ca --- /dev/null +++ b/src/components/TodoMainFooter/TodoMainFooter.tsx @@ -0,0 +1,110 @@ +import React, { useCallback } from 'react'; +import { Todo } from '../../types/Todo'; +import classNames from 'classnames'; + +export type FilterOptions = 'all' | 'active' | 'completed'; + +type Props = { + counter: number; + filterTodos: FilterOptions; + todos: Todo[]; + onFilterTodos: (filter: FilterOptions) => void; + setTodos: React.Dispatch>; + setClearButton: (click: boolean) => void; +}; + +export const TodoMainFooter = React.memo( + ({ setClearButton, counter, filterTodos, todos, onFilterTodos }) => { + const totalCompleted = todos.filter(todo => todo.completed).length; + + const handleLinkAll = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + onFilterTodos('all'); + }, + [onFilterTodos], + ); + + const handleLinkActive = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + onFilterTodos('active'); + }, + [onFilterTodos], + ); + + const handleLinkCompleted = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + onFilterTodos('completed'); + }, + [onFilterTodos], + ); + + const handleLinkClearCompleted = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + setClearButton(true); + }, + [], + ); + + return ( +
+ {/* Hide the footer if there are no todos */} + + {counter - totalCompleted} items left + + + {/* Active link should have the 'selected' class */} + + + {/* this button should be disabled if there are no completed todos */} + +
+ ); + }, +); + +TodoMainFooter.displayName = 'TodoMainFooter'; diff --git a/src/components/TodoMainFooter/index.ts b/src/components/TodoMainFooter/index.ts new file mode 100644 index 0000000000..a6c6ee1ce4 --- /dev/null +++ b/src/components/TodoMainFooter/index.ts @@ -0,0 +1 @@ +export { TodoMainFooter } from './TodoMainFooter'; diff --git a/src/styles/animation.scss b/src/styles/animation.scss new file mode 100644 index 0000000000..3fdcc1ddf2 --- /dev/null +++ b/src/styles/animation.scss @@ -0,0 +1,46 @@ +.item-enter { + max-height: 0; + } + + .item-enter-active { + overflow: hidden; + max-height: 58px; + transition: max-height 0.3s ease-in-out; + } + + .item-exit { + max-height: 58px; + } + + .item-exit-active { + overflow: hidden; + max-height: 0; + transition: max-height 0.3s ease-in-out; + } + + .temp-item-enter { + max-height: 0; + } + + .temp-item-enter-active { + overflow: hidden; + max-height: 58px; + transition: max-height 0.3s ease-in-out; + } + + .temp-item-exit { + max-height: 58px; + } + + .temp-item-exit-active { + transform: translateY(-58px); + max-height: 0; + opacity: 0; + transition: 0.3s ease-in-out; + transition-property: opacity, max-height, transform; + } + + .has-error .temp-item-exit-active { + transform: translateY(0); + overflow: hidden; + } \ No newline at end of file diff --git a/src/types/ErrorMessage.ts b/src/types/ErrorMessage.ts new file mode 100644 index 0000000000..9a138570a1 --- /dev/null +++ b/src/types/ErrorMessage.ts @@ -0,0 +1,7 @@ +export enum ErrorMessage { + LoadTodos = 'Unable to load todos', + AddTodo = 'Unable to add a todo', + DeleteTodo = 'Unable to delete a todo', + EmptyTitle = 'Title should not be empty', + ToggleTodo = 'Unable to update a todo', +} diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 0000000000..fe50b5af23 --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,17 @@ +export interface Todo { + id: number; + userId: number; + title: string; + completed: boolean; +} + +export type NewTodo = Omit; + +export interface UpdateTodo { + id: number; + completed: boolean; +} +export interface EditTodo { + id: number; + title: string; +} diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts new file mode 100644 index 0000000000..4051667e8d --- /dev/null +++ b/src/utils/fetchClient.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +const BASE_URL = 'https://mate.academy/students-api'; + +// returns a promise resolved after a given delay +function wait(delay: number) { + return new Promise(resolve => { + setTimeout(resolve, delay); + }); +} + +// To have autocompletion and avoid mistypes +type RequestMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'; + +function request( + url: string, + method: RequestMethod = 'GET', + data: any = null, // we can send any data to the server +): Promise { + const options: RequestInit = { method }; + + if (data) { + // We add body and Content-Type only for the requests with data + options.body = JSON.stringify(data); + options.headers = { + 'Content-Type': 'application/json; charset=UTF-8', + }; + } + + // DON'T change the delay it is required for tests + return wait(200) + .then(() => fetch(BASE_URL + url, options)) + .then(response => { + if (!response.ok) { + throw new Error(); + } + + return response.json(); + }); +} + +export const client = { + get: (url: string) => request(url), + post: (url: string, data: any) => request(url, 'POST', data), + patch: (url: string, data: any) => request(url, 'PATCH', data), + delete: (url: string) => request(url, 'DELETE'), +}; diff --git a/tsconfig.json b/tsconfig.json index cfb168bb26..9ffda021b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "src" ], "compilerOptions": { + "jsx": "react-jsx", "sourceMap": false, "types": ["node", "cypress"] }