From 8d79d1c5c91fc7fc60ef551e2bc5135fd2fb5211 Mon Sep 17 00:00:00 2001
From: Nazarii AlieKsieiev
Date: Tue, 29 Oct 2024 13:55:09 +0200
Subject: [PATCH] Developed
---
cypress/integration/page.spec.js | 6 +-
package-lock.json | 257 ++++++++++++-------
package.json | 2 +-
src/App.tsx | 180 +++----------
src/Store.tsx | 150 +++++++++++
src/UserWarning.tsx | 15 ++
src/api/todos.ts | 24 ++
src/components/Notification/Notification.tsx | 29 +++
src/components/TodoFilter/TodoFilter.tsx | 119 +++++++++
src/components/TodoForm/TodoForm.tsx | 122 +++++++++
src/components/TodoItem/TodoItem.tsx | 106 ++++++++
src/components/TodoList/TodoList.tsx | 171 ++++++++++++
src/index.tsx | 15 +-
src/styles/index.scss | 6 +-
src/styles/todo.scss | 56 +++-
src/styles/todoapp.scss | 18 +-
src/types/ErrorMessage.ts | 7 +
src/types/Status.ts | 5 +
src/types/Todo.ts | 6 +
src/utils/autoCloseNotification.ts | 7 +
src/utils/fetchClient.ts | 46 ++++
21 files changed, 1082 insertions(+), 265 deletions(-)
create mode 100644 src/Store.tsx
create mode 100644 src/UserWarning.tsx
create mode 100644 src/api/todos.ts
create mode 100644 src/components/Notification/Notification.tsx
create mode 100644 src/components/TodoFilter/TodoFilter.tsx
create mode 100644 src/components/TodoForm/TodoForm.tsx
create mode 100644 src/components/TodoItem/TodoItem.tsx
create mode 100644 src/components/TodoList/TodoList.tsx
create mode 100644 src/types/ErrorMessage.ts
create mode 100644 src/types/Status.ts
create mode 100644 src/types/Todo.ts
create mode 100644 src/utils/autoCloseNotification.ts
create mode 100644 src/utils/fetchClient.ts
diff --git a/cypress/integration/page.spec.js b/cypress/integration/page.spec.js
index 0875764e1..5b6f0e428 100644
--- a/cypress/integration/page.spec.js
+++ b/cypress/integration/page.spec.js
@@ -58,9 +58,9 @@ Cypress.on('fail', e => {
});
describe('', () => {
- beforeEach(() => {
- if (failed) Cypress.runner.stop();
- });
+ // beforeEach(() => {
+ // if (failed) Cypress.runner.stop();
+ // });
describe('Page with no todos', () => {
beforeEach(() => {
diff --git a/package-lock.json b/package-lock.json
index 0adcc869f..fe8e1978c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
@@ -1170,10 +1170,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": "1.9.12",
+ "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.12.tgz",
+ "integrity": "sha512-/OcmxMa34lYLFlGx7Ig926W1U1qjrnXbjFJ2TzUcDaLmED+A5se652NcWwGOidXRuMAOYLPU2jNYBEkKyXrFJA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@octokit/rest": "^17.11.2",
"@types/get-port": "^4.2.0",
@@ -1862,208 +1863,252 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz",
- "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz",
+ "integrity": "sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz",
- "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.2.tgz",
+ "integrity": "sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz",
- "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.2.tgz",
+ "integrity": "sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz",
- "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.2.tgz",
+ "integrity": "sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.2.tgz",
+ "integrity": "sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.2.tgz",
+ "integrity": "sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz",
- "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.2.tgz",
+ "integrity": "sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz",
- "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.2.tgz",
+ "integrity": "sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz",
- "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.2.tgz",
+ "integrity": "sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz",
- "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.2.tgz",
+ "integrity": "sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz",
- "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.2.tgz",
+ "integrity": "sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==",
"cpu": [
"ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz",
- "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.2.tgz",
+ "integrity": "sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==",
"cpu": [
"riscv64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz",
- "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.2.tgz",
+ "integrity": "sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==",
"cpu": [
"s390x"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz",
- "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz",
+ "integrity": "sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz",
- "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.2.tgz",
+ "integrity": "sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz",
- "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.2.tgz",
+ "integrity": "sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz",
- "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.2.tgz",
+ "integrity": "sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw==",
"cpu": [
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz",
- "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.2.tgz",
+ "integrity": "sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -2146,10 +2191,11 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
- "dev": true
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/get-port": {
"version": "4.2.0",
@@ -7092,10 +7138,11 @@
}
},
"node_modules/micromatch": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
- "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -8239,10 +8286,11 @@
"dev": true
},
"node_modules/path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
+ "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"isarray": "0.0.1"
}
@@ -8275,10 +8323,11 @@
"dev": true
},
"node_modules/picocolors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
- "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
- "dev": true
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -8396,9 +8445,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.39",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
- "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
+ "version": "8.4.47",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+ "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
@@ -8414,10 +8463,11 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.1",
- "source-map-js": "^1.2.0"
+ "picocolors": "^1.1.0",
+ "source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -9005,12 +9055,13 @@
}
},
"node_modules/rollup": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz",
- "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.2.tgz",
+ "integrity": "sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@types/estree": "1.0.5"
+ "@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -9020,22 +9071,24 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.18.1",
- "@rollup/rollup-android-arm64": "4.18.1",
- "@rollup/rollup-darwin-arm64": "4.18.1",
- "@rollup/rollup-darwin-x64": "4.18.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.18.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.18.1",
- "@rollup/rollup-linux-arm64-gnu": "4.18.1",
- "@rollup/rollup-linux-arm64-musl": "4.18.1",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.18.1",
- "@rollup/rollup-linux-s390x-gnu": "4.18.1",
- "@rollup/rollup-linux-x64-gnu": "4.18.1",
- "@rollup/rollup-linux-x64-musl": "4.18.1",
- "@rollup/rollup-win32-arm64-msvc": "4.18.1",
- "@rollup/rollup-win32-ia32-msvc": "4.18.1",
- "@rollup/rollup-win32-x64-msvc": "4.18.1",
+ "@rollup/rollup-android-arm-eabi": "4.24.2",
+ "@rollup/rollup-android-arm64": "4.24.2",
+ "@rollup/rollup-darwin-arm64": "4.24.2",
+ "@rollup/rollup-darwin-x64": "4.24.2",
+ "@rollup/rollup-freebsd-arm64": "4.24.2",
+ "@rollup/rollup-freebsd-x64": "4.24.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.24.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.24.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.24.2",
+ "@rollup/rollup-linux-arm64-musl": "4.24.2",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.24.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.24.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.24.2",
+ "@rollup/rollup-linux-x64-gnu": "4.24.2",
+ "@rollup/rollup-linux-x64-musl": "4.24.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.24.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.24.2",
+ "@rollup/rollup-win32-x64-msvc": "4.24.2",
"fsevents": "~2.3.2"
}
},
@@ -9368,10 +9421,11 @@
"dev": true
},
"node_modules/source-map-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
- "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
+ "license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -10402,14 +10456,15 @@
}
},
"node_modules/vite": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
- "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
+ "version": "5.4.10",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
+ "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
- "postcss": "^8.4.39",
- "rollup": "^4.13.0"
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -10428,6 +10483,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
+ "sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -10445,6 +10501,9 @@
"sass": {
"optional": true
},
+ "sass-embedded": {
+ "optional": true
+ },
"stylus": {
"optional": true
},
diff --git a/package.json b/package.json
index e6134ce84..91d7489b9 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
diff --git a/src/App.tsx b/src/App.tsx
index a399287bd..74a900241 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,157 +1,51 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
/* eslint-disable jsx-a11y/control-has-associated-label */
-import React from 'react';
+import { TodoFilter } from './components/TodoFilter/TodoFilter';
+import React, { useContext, useEffect } from 'react';
+import { UserWarning } from './UserWarning';
+import { getTodos, USER_ID } from './api/todos';
+import { TodoList } from './components/TodoList/TodoList';
+import { ErrorMessage } from './types/ErrorMessage';
+import { TodoForm } from './components/TodoForm/TodoForm';
+import { Notification } from './components/Notification/Notification';
+import { DispatchContext, StateContext } from './Store';
+import { onAutoCloseNotification } from './utils/autoCloseNotification';
export const App: React.FC = () => {
+ const dispatch = useContext(DispatchContext);
+ const { todos } = useContext(StateContext);
+
+ useEffect(() => {
+ dispatch({ type: 'download' });
+ getTodos()
+ .then(downloadedTodos => {
+ dispatch({ type: 'downloadSuccess', todos: downloadedTodos });
+ })
+ .catch(() => {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.load });
+ })
+ .finally(() => {
+ onAutoCloseNotification(dispatch);
+ });
+ }, []);
+
+ if (!USER_ID) {
+ return ;
+ }
+
return (
todos
-
- {/* this button should have `active` class only if all todos are completed */}
-
-
- {/* Add a todo on form submit */}
-
-
-
-
-
- {/* Hide the footer if there are no todos */}
-
-
- 3 items left
-
-
- {/* Active link should have the 'selected' class */}
-
-
- All
-
-
-
- Active
-
-
-
- Completed
-
-
-
- {/* this button should be disabled if there are no completed todos */}
-
- Clear completed
-
-
+ {todos.length > 0 &&
}
+
+
);
};
diff --git a/src/Store.tsx b/src/Store.tsx
new file mode 100644
index 000000000..ef3720714
--- /dev/null
+++ b/src/Store.tsx
@@ -0,0 +1,150 @@
+import React, { useReducer } from 'react';
+import { ErrorMessage } from './types/ErrorMessage';
+import { Todo } from './types/Todo';
+import { Status } from './types/Status';
+export type Action =
+ | { type: 'download' }
+ | { type: 'downloadSuccess'; todos: Todo[] }
+ | { type: 'failure'; errorMessage: ErrorMessage | null }
+ | { type: 'closeNotification' }
+ | { type: 'filterStatus'; status: Status }
+ | { type: 'addTodo'; tempTodo: Todo }
+ | { type: 'addingSuccess'; newTodo: Todo }
+ | { type: 'reset' }
+ | { type: 'startAction'; selectedTodo: number[] }
+ | { type: 'deletingSuccesses' }
+ | { type: 'editTodoSuccess'; index: number; updatedTodo: Todo }
+ | { type: 'toggleAllSuccesses'; completed: boolean };
+
+interface RootState {
+ todos: Todo[];
+ tempTodo: Todo | null;
+ selectedTodo: number[];
+ status: Status;
+ isProcessing: boolean;
+ errorMessage: ErrorMessage | null;
+}
+
+const initialState: RootState = {
+ todos: [],
+ tempTodo: null,
+ selectedTodo: [],
+ status: Status.all,
+ isProcessing: false,
+ errorMessage: null,
+};
+
+const reducer = (state: RootState, action: Action): RootState => {
+ switch (action.type) {
+ case 'download':
+ return {
+ ...state,
+ isProcessing: true,
+ errorMessage: null,
+ };
+ case 'downloadSuccess':
+ return {
+ ...state,
+ todos: action.todos,
+ isProcessing: false,
+ };
+ case 'failure':
+ return {
+ ...state,
+ errorMessage: action.errorMessage,
+ isProcessing: false,
+ };
+ case 'closeNotification':
+ return {
+ ...state,
+ errorMessage: null,
+ };
+ case 'filterStatus':
+ return {
+ ...state,
+ status: action.status,
+ };
+ case 'addTodo':
+ return {
+ ...state,
+ tempTodo: action.tempTodo,
+ isProcessing: true,
+ errorMessage: null,
+ };
+ case 'addingSuccess':
+ return {
+ ...state,
+ isProcessing: false,
+ tempTodo: null,
+ todos: [...state.todos, action.newTodo],
+ };
+ case 'reset':
+ return {
+ ...state,
+ tempTodo: null,
+ selectedTodo: [],
+ };
+ case 'startAction':
+ return {
+ ...state,
+ isProcessing: true,
+ errorMessage: null,
+ selectedTodo: action.selectedTodo,
+ };
+ case 'deletingSuccesses':
+ const newTodos = state.todos.filter(
+ todo => !state.selectedTodo.includes(todo.id),
+ );
+
+ return {
+ ...state,
+ todos: newTodos,
+ isProcessing: false,
+ };
+ case 'editTodoSuccess': {
+ const updatedTodos = state.todos.map((todo, idx) =>
+ idx === action.index ? action.updatedTodo : todo,
+ );
+
+ return {
+ ...state,
+ todos: updatedTodos,
+ isProcessing: false,
+ };
+ }
+
+ case 'toggleAllSuccesses': {
+ const updatedTodos = state.todos.map(todo => ({
+ ...todo,
+ completed: action.completed,
+ }));
+
+ return {
+ ...state,
+ todos: updatedTodos,
+ isProcessing: false,
+ };
+ }
+
+ default:
+ return state;
+ }
+};
+
+export const StateContext = React.createContext(initialState);
+// eslint-disable-next-line @typescript-eslint/no-unused-vars, prettier/prettier
+export const DispatchContext = React.createContext((action: Action) => { });
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const GlobalStateProvider: React.FC = ({ children }) => {
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/UserWarning.tsx b/src/UserWarning.tsx
new file mode 100644
index 000000000..fa25838e6
--- /dev/null
+++ b/src/UserWarning.tsx
@@ -0,0 +1,15 @@
+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 000000000..602161194
--- /dev/null
+++ b/src/api/todos.ts
@@ -0,0 +1,24 @@
+import { Todo } from '../types/Todo';
+import { client } from '../utils/fetchClient';
+
+export const USER_ID = 1781;
+
+export const getTodos = () => {
+ return client.get(`/todos?userId=${USER_ID}`);
+};
+
+export const deleteTodo = (todoId: number) => {
+ return client.delete(`/todos/${todoId}`);
+};
+
+export const addTodo = (title: string) => {
+ return client.post(`/todos`, { title, userId: USER_ID, completed: false });
+};
+
+export const toggleTodo = (id: number, status: boolean) => {
+ return client.patch(`/todos/${id}`, { completed: !status });
+};
+
+export const editTodo = (id: number, title: string) => {
+ return client.patch(`/todos/${id}`, { title });
+};
diff --git a/src/components/Notification/Notification.tsx b/src/components/Notification/Notification.tsx
new file mode 100644
index 000000000..f4792fd92
--- /dev/null
+++ b/src/components/Notification/Notification.tsx
@@ -0,0 +1,29 @@
+import React, { useContext, useMemo } from 'react';
+import cn from 'classNames';
+import { DispatchContext, StateContext } from '../../Store';
+
+export const Notification: React.FC = () => {
+ const { errorMessage } = useContext(StateContext);
+ const hasError = useMemo(() => !Boolean(errorMessage), [errorMessage]);
+ const dispatch = useContext(DispatchContext);
+ const onCloseNotification = () => {
+ dispatch({ type: 'closeNotification' });
+ };
+
+ return (
+
+
+ {errorMessage}
+
+ );
+};
diff --git a/src/components/TodoFilter/TodoFilter.tsx b/src/components/TodoFilter/TodoFilter.tsx
new file mode 100644
index 000000000..d39a06753
--- /dev/null
+++ b/src/components/TodoFilter/TodoFilter.tsx
@@ -0,0 +1,119 @@
+import React, { useCallback, useContext, useMemo } from 'react';
+import cn from 'classNames';
+
+import { Status } from '../../types/Status';
+import { DispatchContext, StateContext } from '../../Store';
+import { deleteTodo } from '../../api/todos';
+import { ErrorMessage } from '../../types/ErrorMessage';
+import { onAutoCloseNotification } from '../../utils/autoCloseNotification';
+
+export const TodoFilter: React.FC = () => {
+ const { todos, status } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ const onSetStatus = useCallback(
+ (event: React.MouseEvent) => {
+ const a = event.target as HTMLAnchorElement;
+ const statusValue =
+ (a.getAttribute('href')?.replace('#/', '') as Status) || Status.all;
+
+ dispatch({ type: 'filterStatus', status: statusValue });
+ },
+ [dispatch],
+ );
+
+ const completedTodos = useMemo(() => {
+ const completed = todos.filter(todo => todo.completed).length;
+
+ return completed;
+ }, [todos]);
+ const todosCounter = useMemo(() => {
+ const notCompletedTodos = todos.filter(todo => !todo.completed).length;
+ const message =
+ notCompletedTodos === 1
+ ? `${notCompletedTodos} item left`
+ : `${notCompletedTodos} items left`;
+
+ return message;
+ }, [todos]);
+
+ const isActiveButton = useCallback(
+ (value: string) => value === status,
+ [status],
+ );
+
+ const capitalize = useCallback((word: string) => {
+ if (!word) {
+ return '';
+ }
+
+ return word[0].toUpperCase() + word.slice(1).toLowerCase();
+ }, []);
+
+ const handleClearCompleted = async () => {
+ const completedIds = todos
+ .filter(todo => todo.completed)
+ .map(todo => todo.id);
+
+ dispatch({ type: 'startAction', selectedTodo: completedIds });
+
+ const toDeleteTodo: number[] = [];
+
+ try {
+ await Promise.all(
+ completedIds.map(async id => {
+ try {
+ await deleteTodo(id);
+ toDeleteTodo.push(id);
+ } catch (error) {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.delete });
+ onAutoCloseNotification(dispatch);
+ }
+ }),
+ );
+
+ dispatch({ type: 'startAction', selectedTodo: toDeleteTodo });
+ toDeleteTodo.forEach(() => {
+ dispatch({ type: 'deletingSuccesses' });
+ });
+ } catch {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.delete });
+ onAutoCloseNotification(dispatch);
+ } finally {
+ dispatch({ type: 'reset' });
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/TodoForm/TodoForm.tsx b/src/components/TodoForm/TodoForm.tsx
new file mode 100644
index 000000000..5cca68da3
--- /dev/null
+++ b/src/components/TodoForm/TodoForm.tsx
@@ -0,0 +1,122 @@
+import { useContext, useEffect, useRef, useState } from 'react';
+import { Todo } from '../../types/Todo';
+import cn from 'classNames';
+import { DispatchContext, StateContext } from '../../Store';
+import { ErrorMessage } from '../../types/ErrorMessage';
+import { onAutoCloseNotification } from '../../utils/autoCloseNotification';
+import { addTodo, toggleTodo, USER_ID } from '../../api/todos';
+
+export const TodoForm: React.FC = () => {
+ const [isActive, setIsActive] = useState(false);
+ const [todoTitle, setTodoTitle] = useState('');
+
+ const titleField = useRef(null);
+ const { todos, isProcessing, tempTodo } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ const handleNewTodoForm = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ if (isProcessing) {
+ return;
+ }
+
+ if (!todoTitle.trim()) {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.add });
+ setTodoTitle('');
+ onAutoCloseNotification(dispatch);
+
+ return;
+ }
+
+ const temp = {
+ title: todoTitle.trim(),
+ userId: USER_ID,
+ id: 0,
+ completed: false,
+ };
+
+ dispatch({ type: 'addTodo', tempTodo: temp });
+
+ addTodo(todoTitle.trim())
+ .then(newTodo => {
+ dispatch({ type: 'addingSuccess', newTodo: newTodo as Todo });
+ setTodoTitle('');
+ })
+ .catch(() => {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.add });
+ onAutoCloseNotification(dispatch);
+ })
+ .finally(() => {
+ dispatch({ type: 'reset' });
+ });
+ };
+
+ const handleToggleAll = async () => {
+ const allCompleted = todos.every(todo => todo.completed);
+ const completed = !allCompleted;
+ const updatedIds = todos
+ .filter(todo => todo.completed !== completed)
+ .map(todo => todo.id);
+
+ dispatch({ type: 'startAction', selectedTodo: updatedIds });
+
+ try {
+ await Promise.all(
+ todos
+ .filter(todo => todo.completed !== completed)
+ .map(async todo => toggleTodo(todo.id, completed)),
+ );
+
+ dispatch({ type: 'toggleAllSuccesses', completed });
+ } catch {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.update });
+ } finally {
+ dispatch({ type: 'reset' });
+ }
+ };
+
+ useEffect(() => {
+ const currentField = titleField.current;
+
+ if (currentField !== null) {
+ currentField.focus();
+ }
+ }, [titleField, todos, tempTodo]);
+
+ useEffect(() => {
+ const isAllCompleted = todos.every(todo => todo.completed === true);
+
+ if (isAllCompleted && todos.length > 0) {
+ setIsActive(true);
+ } else {
+ setIsActive(false);
+ }
+ }, [todos]);
+
+ return (
+
+ {todos.length > 0 && (
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx
new file mode 100644
index 000000000..77f0fb203
--- /dev/null
+++ b/src/components/TodoItem/TodoItem.tsx
@@ -0,0 +1,106 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import cn from 'classNames';
+import { Todo } from '../../types/Todo';
+import { useContext, useEffect, useRef } from 'react';
+import { StateContext } from '../../Store';
+
+interface Props {
+ index: number;
+ todo: Todo;
+ isEditing: number | null;
+ newTitle: string;
+ onDelete: (id: number) => void;
+ onToggle: (todo: Todo, index: number) => void;
+ onStartEditing: (value: string, id: number) => void;
+ onNewTodoTitle: (value: string) => void;
+ onChangeTodoTitle: (
+ event: React.FormEvent,
+ todo: Todo,
+ index: number,
+ ) => void;
+ onKey: (event: React.KeyboardEvent) => void;
+}
+
+export const TodoItem: React.FC = ({
+ index,
+ todo,
+ onDelete,
+ onToggle,
+ newTitle,
+ isEditing,
+ onStartEditing,
+ onNewTodoTitle,
+ onChangeTodoTitle,
+ onKey,
+}) => {
+ const newTitleFiled = useRef(null);
+ const { selectedTodo } = useContext(StateContext);
+
+ useEffect(() => {
+ const currentField = newTitleFiled.current;
+
+ if (currentField !== null) {
+ currentField.focus();
+ }
+ }, [newTitleFiled, isEditing]);
+ const { id, title, completed } = todo;
+
+ return (
+
+
+ onToggle(todo, index)}
+ />
+
+ {isEditing === id ? (
+
+ ) : (
+ <>
+
onStartEditing(title, id)}
+ >
+ {title}
+
+
+
onDelete(todo.id)}
+ >
+ ×
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx
new file mode 100644
index 000000000..bc3862cf0
--- /dev/null
+++ b/src/components/TodoList/TodoList.tsx
@@ -0,0 +1,171 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import { useContext, useMemo, useState } from 'react';
+import { Todo } from '../../types/Todo';
+import { TodoItem } from '../TodoItem/TodoItem';
+import { DispatchContext, StateContext } from '../../Store';
+import { Status } from '../../types/Status';
+import { ErrorMessage } from '../../types/ErrorMessage';
+import { onAutoCloseNotification } from '../../utils/autoCloseNotification';
+import { deleteTodo, editTodo, toggleTodo } from '../../api/todos';
+
+export const TodoList: React.FC = () => {
+ const [isEditing, setIsEditing] = useState(null);
+ const [newTodoTitle, setNewTodoTitle] = useState('');
+ const [isFailed, setIsFailed] = useState(false);
+ const { todos, status, tempTodo } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ const handleStartEdit = (prevTitle: string, id: number) => {
+ if (isFailed) {
+ return;
+ }
+
+ setIsEditing(id);
+ setNewTodoTitle(prevTitle);
+ };
+
+ const handleKey = (event: React.KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setIsEditing(null);
+ setNewTodoTitle('');
+ }
+ };
+
+ const handleDeleteTodo = (id: number) => {
+ dispatch({ type: 'startAction', selectedTodo: [id] });
+
+ deleteTodo(id)
+ .then(() => {
+ dispatch({ type: 'deletingSuccesses' });
+ })
+ .catch(() => {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.delete });
+ onAutoCloseNotification(dispatch);
+ })
+ .finally(() => {
+ dispatch({ type: 'reset' });
+ });
+ };
+
+ const handleToggleTodo = async (todo: Todo, index: number) => {
+ const { completed, id } = todo;
+
+ try {
+ dispatch({ type: 'startAction', selectedTodo: [id] });
+ const updatedTodo = await toggleTodo(id, completed);
+
+ dispatch({
+ type: 'editTodoSuccess',
+ index,
+ updatedTodo: updatedTodo as Todo,
+ });
+ } catch {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.update });
+ } finally {
+ dispatch({ type: 'reset' });
+ onAutoCloseNotification(dispatch);
+ }
+ };
+
+ const handleEditTitle = async (
+ event: React.FormEvent,
+ todo: Todo,
+ index: number,
+ ) => {
+ event.preventDefault();
+
+ const { title, id } = todo;
+
+ if (title === newTodoTitle) {
+ setIsEditing(null);
+ setNewTodoTitle('');
+
+ return;
+ }
+
+ if (!newTodoTitle.trim()) {
+ handleDeleteTodo(id);
+
+ return;
+ }
+
+ try {
+ dispatch({ type: 'startAction', selectedTodo: [id] });
+
+ const updatedTodo = await editTodo(id, newTodoTitle.trim());
+
+ dispatch({
+ type: 'editTodoSuccess',
+ index,
+ updatedTodo: updatedTodo as Todo,
+ });
+
+ setNewTodoTitle('');
+ setIsEditing(null);
+ setIsFailed(false);
+ } catch {
+ dispatch({ type: 'failure', errorMessage: ErrorMessage.update });
+ setIsFailed(true);
+ } finally {
+ dispatch({ type: 'reset' });
+ }
+ };
+
+ const visibleTodos = useMemo(() => {
+ switch (status) {
+ case Status.all:
+ return todos;
+ case Status.active:
+ return todos.filter(todo => !todo.completed);
+ case Status.completed:
+ return todos.filter(todo => todo.completed);
+ default:
+ return todos;
+ }
+ }, [status, todos]);
+
+ return (
+
+ {visibleTodos.map((todo, i) => (
+
+ ))}
+
+ {tempTodo && (
+
+
+
+
+
+
+ {tempTodo.title}
+
+
+
+ ×
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/index.tsx b/src/index.tsx
index a9689cb38..8520c0da4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,11 +1,14 @@
import { createRoot } from 'react-dom/client';
-import './styles/index.css';
-import './styles/todo-list.css';
-import './styles/filters.css';
+import 'bulma/css/bulma.css';
+import '@fortawesome/fontawesome-free/css/all.css';
+import './styles/index.scss';
import { App } from './App';
+import { GlobalStateProvider } from './Store';
-const container = document.getElementById('root') as HTMLDivElement;
-
-createRoot(container).render( );
+createRoot(document.getElementById('root') as HTMLDivElement).render(
+
+
+ ,
+);
diff --git a/src/styles/index.scss b/src/styles/index.scss
index a34eec7c6..bccd80c8b 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -20,6 +20,6 @@ body {
pointer-events: none;
}
-@import './todoapp';
-@import './todo';
-@import './filter';
+@import "./todoapp";
+@import "./todo";
+@import "./filter";
diff --git a/src/styles/todo.scss b/src/styles/todo.scss
index 4576af434..c7f93ff6b 100644
--- a/src/styles/todo.scss
+++ b/src/styles/todo.scss
@@ -15,13 +15,13 @@
&__status-label {
cursor: pointer;
- background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
+ background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center left;
}
&.completed &__status-label {
- background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
+ background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
}
&__status {
@@ -92,8 +92,58 @@
.overlay {
position: absolute;
- inset: 0;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 58px;
opacity: 0.5;
}
}
+
+.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;
+}
diff --git a/src/styles/todoapp.scss b/src/styles/todoapp.scss
index e289a9458..ad28bcb2f 100644
--- a/src/styles/todoapp.scss
+++ b/src/styles/todoapp.scss
@@ -1,5 +1,6 @@
+
.todoapp {
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 24px;
font-weight: 300;
color: #4d4d4d;
@@ -8,8 +9,7 @@
&__content {
margin-bottom: 20px;
background: #fff;
- box-shadow:
- 0 2px 4px 0 rgba(0, 0, 0, 0.2),
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
@@ -49,7 +49,7 @@
}
&::before {
- content: '❯';
+ content: "❯";
transform: translateY(2px) rotate(90deg);
line-height: 0;
}
@@ -69,7 +69,7 @@
border: none;
background: rgba(0, 0, 0, 0.01);
- box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
+ box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
&::placeholder {
font-style: italic;
@@ -97,8 +97,7 @@
text-align: center;
border-top: 1px solid #e6e6e6;
- box-shadow:
- 0 1px 1px rgba(0, 0, 0, 0.2),
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
@@ -122,6 +121,7 @@
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ transition: opacity 0.3s;
&:hover {
text-decoration: underline;
@@ -130,5 +130,9 @@
&:active {
text-decoration: none;
}
+
+ &:disabled {
+ visibility: hidden;
+ }
}
}
diff --git a/src/types/ErrorMessage.ts b/src/types/ErrorMessage.ts
new file mode 100644
index 000000000..6c93852c4
--- /dev/null
+++ b/src/types/ErrorMessage.ts
@@ -0,0 +1,7 @@
+export enum ErrorMessage {
+ load = 'Unable to load todos',
+ title = 'Title should not be empty',
+ add = 'Unable to add a todo',
+ delete = 'Unable to delete a todo',
+ update = 'Unable to update a todo',
+}
diff --git a/src/types/Status.ts b/src/types/Status.ts
new file mode 100644
index 000000000..66910400f
--- /dev/null
+++ b/src/types/Status.ts
@@ -0,0 +1,5 @@
+export enum Status {
+ all = 'all',
+ active = 'active',
+ completed = 'completed',
+}
diff --git a/src/types/Todo.ts b/src/types/Todo.ts
new file mode 100644
index 000000000..3f52a5fdd
--- /dev/null
+++ b/src/types/Todo.ts
@@ -0,0 +1,6 @@
+export interface Todo {
+ id: number;
+ userId: number;
+ title: string;
+ completed: boolean;
+}
diff --git a/src/utils/autoCloseNotification.ts b/src/utils/autoCloseNotification.ts
new file mode 100644
index 000000000..8750dc234
--- /dev/null
+++ b/src/utils/autoCloseNotification.ts
@@ -0,0 +1,7 @@
+import { Action } from '../Store';
+
+export const onAutoCloseNotification = (dispatch: (action: Action) => void) => {
+ setTimeout(() => {
+ dispatch({ type: 'closeNotification' });
+ }, 3000);
+};
diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts
new file mode 100644
index 000000000..708ac4c17
--- /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(100)
+ .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'),
+};