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 (
-
+
+
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 && (
+
+ )}
+
+ {!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 (
+
+ );
+ },
+);
+
+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"]
}