From a277f8ed50af6a9488af03139001432432c73649 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 3 Jul 2026 22:44:38 +0300 Subject: [PATCH 1/2] complete part 1 of todo app --- package-lock.json | 9 +- package.json | 2 +- src/App.tsx | 499 ++++++++++++++++++++++++++++++++-------------- src/api/todos.ts | 2 +- 4 files changed, 354 insertions(+), 158 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c15974bf..322b43c0d 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", @@ -1183,10 +1183,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", diff --git a/package.json b/package.json index 3a362ba82..2a26e11a6 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 bf9c75980..5e2ad44ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,62 @@ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { UserWarning } from './UserWarning'; -import { USER_ID } from './api/todos'; +import { getTodos, USER_ID } from './api/todos'; +import { Todo } from './types/Todo'; +import classNames from 'classnames'; export const App: React.FC = () => { + const [title, setTitle] = useState(''); + const [todos, setTodos] = useState([]); + const [error, setError] = useState(null); + const [filter, setFilter] = useState('all'); + + useEffect(() => { + setError(null); + getTodos() + .then(setTodos) + .catch(() => setError('Unable to load todos')); + }, []); + + useEffect(() => { + if (error === null) { + return; + } + + setTimeout(() => { + setError(null); + }, 3000); + }, [error]); + + const activeTodosCount = todos.filter(todo => !todo.completed).length; + + let visibleTodos = todos; + + if (filter === 'active') { + visibleTodos = todos.filter(todo => !todo.completed); + } + + if (filter === 'completed') { + visibleTodos = todos.filter(todo => todo.completed); + } + + if (filter === 'active') { + visibleTodos = todos.filter(todo => !todo.completed); + } + if (!USER_ID) { return ; } + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (!title.trim()) { + setError('Title should not be empty'); + } + }; + return (

todos

@@ -23,182 +71,329 @@ export const App: React.FC = () => { /> {/* Add a todo on form submit */} -
+ setTitle(event.target.value)} />
-
- {/* This is a completed todo */} -
- - - - Completed Todo - + {todos.length > 0 && ( +
+ {visibleTodos.map(todo => { + return ( +
+ - {/* Remove button appears only on hover */} - + + {todo.title} + - {/* overlay will cover the todo while it is being deleted or updated */} -
-
-
-
-
- - {/* This todo is an active todo */} -
- - - - Not Completed Todo - - + {/* Remove button appears only on hover */} + + + {/* overlay will cover the todo while it is being deleted or updated */} +
+
+
+
+
+ ); + })} +
+ )} -
-
-
-
-
- - {/* This todo is being edited */} -
- - - {/* This form is shown instead of the title and remove button */} -
- -
- -
-
-
-
-
- - {/* This todo is in loadind state */} -
- - - - Todo is being saved now + {todos.length > 0 && ( +
+ + {activeTodosCount} items left - + {/* Active link should have the 'selected' class */} +
- - {/* Hide the footer if there are no todos */} - + Clear completed + + + )}
{/* DON'T use conditional rendering to hide the notification */} {/* Add the 'hidden' class to hide the message smoothly */}
-
); + + // return ( + //
+ //

todos

+ + //
+ //
+ // {/* this button should have `active` class only if all todos are completed */} + //
+ + //
+ // {/* This is a completed todo */} + //
+ // + + // + // Completed Todo + // + + // {/* Remove button appears only on hover */} + // + + // {/* overlay will cover the todo while it is being deleted or updated */} + //
+ //
+ //
+ //
+ //
+ + // {/* This todo is an active todo */} + //
+ // + + // + // Not Completed Todo + // + // + + //
+ //
+ //
+ //
+ //
+ + // {/* This todo is being edited */} + //
+ // + + // {/* This form is shown instead of the title and remove button */} + //
+ // + //
+ + //
+ //
+ //
+ //
+ //
+ + // {/* This todo is in loadind state */} + //
+ // + + // + // Todo is being saved now + // + + // + + // {/* 'is-active' class puts this modal on top of the todo */} + //
+ //
+ //
+ //
+ //
+ //
+ + // {/* Hide the footer if there are no todos */} + //
+ // + // 3 items left + // + + // {/* Active link should have the 'selected' class */} + // + + // {/* this button should be disabled if there are no completed todos */} + // + //
+ //
+ + // {/* DON'T use conditional rendering to hide the notification */} + // {/* Add the 'hidden' class to hide the message smoothly */} + //
+ //
+ //
+ // ); }; diff --git a/src/api/todos.ts b/src/api/todos.ts index 8a0b90aa0..e52ecd06a 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,7 +1,7 @@ import { Todo } from '../types/Todo'; import { client } from '../utils/fetchClient'; -export const USER_ID = 0; +export const USER_ID = 4344; export const getTodos = () => { return client.get(`/todos?userId=${USER_ID}`); From 40a18af1d329653250085ae99e571523f79b20c5 Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 5 Jul 2026 11:33:46 +0300 Subject: [PATCH 2/2] Create components and fix errors --- README.md | 2 +- src/App.tsx | 162 ++---------------- .../ErrorNotification/ErrorNotification.tsx | 30 ++++ src/components/ErrorNotification/index.ts | 1 + src/components/Footer/Footer.tsx | 79 +++++++++ src/components/Footer/index.ts | 1 + src/components/Header/Header.tsx | 32 ++++ src/components/Header/index.ts | 1 + src/components/TodoItem/TodoItem.tsx | 42 +++++ src/components/TodoItem/index.ts | 1 + src/components/TodoList/TodoList.tsx | 19 ++ src/components/TodoList/index.ts | 1 + .../UserWarning}/UserWarning.tsx | 0 src/types/ErrorMessages.ts | 7 + src/types/Filter.ts | 1 + 15 files changed, 235 insertions(+), 144 deletions(-) create mode 100644 src/components/ErrorNotification/ErrorNotification.tsx create mode 100644 src/components/ErrorNotification/index.ts create mode 100644 src/components/Footer/Footer.tsx create mode 100644 src/components/Footer/index.ts create mode 100644 src/components/Header/Header.tsx create mode 100644 src/components/Header/index.ts create mode 100644 src/components/TodoItem/TodoItem.tsx create mode 100644 src/components/TodoItem/index.ts create mode 100644 src/components/TodoList/TodoList.tsx create mode 100644 src/components/TodoList/index.ts rename src/{ => components/UserWarning}/UserWarning.tsx (100%) create mode 100644 src/types/ErrorMessages.ts create mode 100644 src/types/Filter.ts diff --git a/README.md b/README.md index da0116e63..2800e8fa5 100644 --- a/README.md +++ b/README.md @@ -70,4 +70,4 @@ Filter todos by status `All` / `Active` / `Completed`: - Install Prettier Extention and use this [VSCode settings](https://mate-academy.github.io/fe-program/tools/vscode/settings.json) to enable format on save. - 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-loading-todos/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://Roma-Yamshchikov.github.io/react_todo-app-loading-todos/) and add it to the PR description. diff --git a/src/App.tsx b/src/App.tsx index 5e2ad44ae..3b964b4b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,28 @@ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/control-has-associated-label */ import React, { useEffect, useState } from 'react'; -import { UserWarning } from './UserWarning'; +import { UserWarning } from './components/UserWarning/UserWarning'; import { getTodos, USER_ID } from './api/todos'; import { Todo } from './types/Todo'; -import classNames from 'classnames'; +import { ErrorNotification } from './components/ErrorNotification'; +import { Header } from './components/Header'; +import { TodoList } from './components/TodoList'; +import { Footer } from './components/Footer'; +import { Filter } from './types/Filter'; +import { ErrorMessages } from './types/ErrorMessages'; export const App: React.FC = () => { - const [title, setTitle] = useState(''); const [todos, setTodos] = useState([]); + const [title, setTitle] = useState(''); + + const [filter, setFilter] = useState('all'); const [error, setError] = useState(null); - const [filter, setFilter] = useState('all'); useEffect(() => { setError(null); getTodos() .then(setTodos) - .catch(() => setError('Unable to load todos')); + .catch(() => setError(ErrorMessages.LoadTodos)); }, []); useEffect(() => { @@ -33,10 +39,6 @@ export const App: React.FC = () => { let visibleTodos = todos; - if (filter === 'active') { - visibleTodos = todos.filter(todo => !todo.completed); - } - if (filter === 'completed') { visibleTodos = todos.filter(todo => todo.completed); } @@ -62,145 +64,19 @@ export const App: React.FC = () => {

todos

-
- {/* this button should have `active` class only if all todos are completed */} - - - {/* overlay will cover the todo while it is being deleted or updated */} -
-
-
-
-
- ); - })} - - )} - - {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/components/ErrorNotification/ErrorNotification.tsx b/src/components/ErrorNotification/ErrorNotification.tsx new file mode 100644 index 000000000..f71004704 --- /dev/null +++ b/src/components/ErrorNotification/ErrorNotification.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import classNames from 'classnames'; + +type Props = { + error: string | null; + setError: (value: string | null) => void; +}; + +export const ErrorNotification: React.FC = ({ error, setError }) => { + return ( +
+
+ ); +}; diff --git a/src/components/ErrorNotification/index.ts b/src/components/ErrorNotification/index.ts new file mode 100644 index 000000000..8cb478792 --- /dev/null +++ b/src/components/ErrorNotification/index.ts @@ -0,0 +1 @@ +export * from './ErrorNotification'; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 000000000..33f99d948 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,79 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Filter } from '../../types/Filter'; + +type FilterOption = { + href: string; + title: string; + value: Filter; + dataCy: string; +}; + +type Props = { + activeTodosCount: number; + filter: Filter; + setFilter: React.Dispatch>; +}; + +export const Footer: React.FC = ({ + activeTodosCount, + filter, + setFilter, +}) => { + const filterOptions: FilterOption[] = [ + { + href: '#/', + title: 'All', + value: 'all', + dataCy: 'FilterLinkAll', + }, + { + href: '#/active', + title: 'Active', + value: 'active', + dataCy: 'FilterLinkActive', + }, + { + href: '#/completed', + title: 'Completed', + value: 'completed', + dataCy: 'FilterLinkCompleted', + }, + ]; + + return ( +
+ + {activeTodosCount} items left + + + {/* Active link should have the 'selected' class */} + + + {/* this button should be disabled if there are no completed todos */} + +
+ ); +}; diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts new file mode 100644 index 000000000..ddcc5a9cd --- /dev/null +++ b/src/components/Footer/index.ts @@ -0,0 +1 @@ +export * from './Footer'; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 000000000..7800d94c4 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +type Props = { + title: string; + handleSubmit: (event: React.FormEvent) => void; + setTitle: React.Dispatch>; +}; + +export const Header: React.FC = ({ title, handleSubmit, setTitle }) => { + return ( +
+ {/* this button should have `active` class only if all todos are completed */} +
+ ); +}; diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts new file mode 100644 index 000000000..266dec8a1 --- /dev/null +++ b/src/components/Header/index.ts @@ -0,0 +1 @@ +export * from './Header'; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx new file mode 100644 index 000000000..8afe78883 --- /dev/null +++ b/src/components/TodoItem/TodoItem.tsx @@ -0,0 +1,42 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import React from 'react'; +import classNames from 'classnames'; +import { Todo } from '../../types/Todo'; + +type Props = { + todo: Todo; +}; + +export const TodoItem: React.FC = ({ todo }) => { + return ( +
+ + + + {todo.title} + + + {/* Remove button appears only on hover */} + + + {/* overlay will cover the todo while it is being deleted or updated */} +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoItem/index.ts b/src/components/TodoItem/index.ts new file mode 100644 index 000000000..21f4abac3 --- /dev/null +++ b/src/components/TodoItem/index.ts @@ -0,0 +1 @@ +export * from './TodoItem'; diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx new file mode 100644 index 000000000..7797c82b5 --- /dev/null +++ b/src/components/TodoList/TodoList.tsx @@ -0,0 +1,19 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import React from 'react'; +import { Todo } from '../../types/Todo'; +import { TodoItem } from '../TodoItem'; + +type Props = { + visibleTodos: Todo[]; +}; + +export const TodoList: React.FC = ({ visibleTodos }) => { + return ( +
+ {visibleTodos.map(todo => ( + + ))} +
+ ); +}; diff --git a/src/components/TodoList/index.ts b/src/components/TodoList/index.ts new file mode 100644 index 000000000..f239f4345 --- /dev/null +++ b/src/components/TodoList/index.ts @@ -0,0 +1 @@ +export * from './TodoList'; diff --git a/src/UserWarning.tsx b/src/components/UserWarning/UserWarning.tsx similarity index 100% rename from src/UserWarning.tsx rename to src/components/UserWarning/UserWarning.tsx diff --git a/src/types/ErrorMessages.ts b/src/types/ErrorMessages.ts new file mode 100644 index 000000000..fe0591d07 --- /dev/null +++ b/src/types/ErrorMessages.ts @@ -0,0 +1,7 @@ +export enum ErrorMessages { + LoadTodos = 'Unable to load todos', + EmptyTitle = 'Title should not be empty', + AddTodo = 'Unable to add a todo', + DeleteTodo = 'Unable to delete a todo', + UpdateTodo = 'Unable to update a todo', +} diff --git a/src/types/Filter.ts b/src/types/Filter.ts new file mode 100644 index 000000000..5d90d45a1 --- /dev/null +++ b/src/types/Filter.ts @@ -0,0 +1 @@ +export type Filter = 'all' | 'active' | 'completed';