From c4a4f4854e1e2d504262bc92d06304a6bb9bb0f2 Mon Sep 17 00:00:00 2001 From: Daryna Pidlutska Date: Wed, 27 Sep 2023 08:01:16 +0200 Subject: [PATCH 1/4] add working version --- src/App.tsx | 105 ++++++++++++++++---- src/Context/TodoContext.tsx | 175 ++++++++++++++++++++++++++++++++++ src/api/todos.ts | 29 ++++++ src/components/TodoFilter.tsx | 91 ++++++++++++++++++ src/components/TodoForm.tsx | 101 ++++++++++++++++++++ src/components/TodoItem.tsx | 123 ++++++++++++++++++++++++ src/components/TodoList.tsx | 31 ++++++ src/index.tsx | 7 +- src/styles/index.scss | 2 +- src/types/Todo.ts | 6 ++ src/types/TodosFilter.ts | 5 + src/utils/constants.ts | 1 + src/utils/countTodos.ts | 9 ++ src/utils/fetchClient.ts | 46 +++++++++ src/utils/getFilteredTodos.ts | 15 +++ 15 files changed, 728 insertions(+), 18 deletions(-) create mode 100644 src/Context/TodoContext.tsx create mode 100644 src/api/todos.ts create mode 100644 src/components/TodoFilter.tsx create mode 100644 src/components/TodoForm.tsx create mode 100644 src/components/TodoItem.tsx create mode 100644 src/components/TodoList.tsx create mode 100644 src/types/Todo.ts create mode 100644 src/types/TodosFilter.ts create mode 100644 src/utils/constants.ts create mode 100644 src/utils/countTodos.ts create mode 100644 src/utils/fetchClient.ts create mode 100644 src/utils/getFilteredTodos.ts diff --git a/src/App.tsx b/src/App.tsx index 5749bdf784..5a2d433773 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,97 @@ -/* eslint-disable max-len */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; -import { UserWarning } from './UserWarning'; - -const USER_ID = 0; +import React, { + useContext, + useEffect, useMemo, useRef, useState, +} from 'react'; +import cn from 'classnames'; +import { TodoForm } from './components/TodoForm'; +import { TodoList } from './components/TodoList'; +import { TodoFilter } from './components/TodoFilter'; +import * as todoService from './api/todos'; +import { TodosFilter } from './types/TodosFilter'; +import { getFilteredTodos } from './utils/getFilteredTodos'; +import { TodoContext } from './Context/TodoContext'; export const App: React.FC = () => { - if (!USER_ID) { - return ; - } + const { + todos, + setTodos, + errorMessage, + setErrorMessage, + } = useContext(TodoContext); + + const [filter, setFilter] = useState(TodosFilter.All); + + useEffect(() => { + todoService.getTodos() + .then(setTodos) + .catch(() => { + setErrorMessage('Unable to load todos'); + }); + }, []); + + const timerId = useRef(0); + + useEffect(() => { + if (timerId.current) { + window.clearTimeout(timerId.current); + } + + timerId.current = window.setTimeout(() => { + setErrorMessage(''); + }, 3000); + }, [errorMessage]); + + const filteredTodos = useMemo(() => { + return getFilteredTodos(todos, filter); + }, [todos, filter]); return ( -
-

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

+
+

todos

+ +
+ + + {Boolean(todos.length) && ( + + )} -

Styles are already copied

-
+ + + {/* Notification is shown in case of any error */} + {/* Add the 'hidden' class to hide the message smoothly */} +
+
+ ); }; diff --git a/src/Context/TodoContext.tsx b/src/Context/TodoContext.tsx new file mode 100644 index 0000000000..41df2a9793 --- /dev/null +++ b/src/Context/TodoContext.tsx @@ -0,0 +1,175 @@ +import React, { createContext, useMemo, useState } from 'react'; +import { Todo } from '../types/Todo'; +import { USER_ID } from '../utils/constants'; +import * as todoService from '../api/todos'; +import { getCompletedTodos, getUncompletedTodos} from '../utils/countTodos'; + +interface TodoContextTypes { + todos: Todo[]; + setTodos: React.Dispatch>; + completedTodos: Todo[]; + uncompletedTodos: Todo[]; + errorMessage: string; + setErrorMessage: React.Dispatch>; + tempTodo: Todo | null; + setTempTodo: React.Dispatch>; + todosIdToProcess: number[]; + setTodosIdToProcess: React.Dispatch>; + handleAddTodo: (todoTitle: string) => Promise; + handleDeleteTodo: (todoId: number) => void; + handleRenameTodo: (todo: Todo, newTodoTitle: string) => void; + handleClearCompletedTodos: () => void; + handleStatusTodoChange: (todo: Todo) => void; +} + +const initTodoContext: TodoContextTypes = { + todos: [], + setTodos: () => {}, + completedTodos: [], + uncompletedTodos: [], + errorMessage: '', + setErrorMessage: () => {}, + tempTodo: null, + setTempTodo: () => {}, + todosIdToProcess: [], + setTodosIdToProcess: () => {}, + handleAddTodo: async () => {}, + handleDeleteTodo: () => {}, + handleRenameTodo: () => {}, + handleClearCompletedTodos: () => {}, + handleStatusTodoChange: () => {}, +}; + +export const TodoContext = createContext(initTodoContext); + +type Props = { + children: React.ReactNode; +}; + +export const TodoProvider: React.FC = ({ children }) => { + const [todos, setTodos] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const [todosIdToProcess, setTodosIdToProcess] = useState([]); + const [tempTodo, setTempTodo] = useState(null); + const completedTodos = getCompletedTodos(todos); + const uncompletedTodos = getUncompletedTodos(todos); + + const handleAddTodo = (todoTitle: string) => { + setTempTodo({ + id: 0, + title: todoTitle, + userId: USER_ID, + completed: false, + }); + + return todoService + .addTodo(todoTitle) + .then((newTodo) => { + setTodos((prevTodos) => [...prevTodos, newTodo]); + }) + .catch((error) => { + setErrorMessage('Unable to add a todo'); + throw error; + }) + .finally(() => { + setTempTodo(null); + }); + }; + + const handleDeleteTodo = (todoId: number) => { + setTodosIdToProcess(prevState => [...prevState, todoId]); + + todoService + .deleteTodo(todoId) + .then(() => { + setTodos(prevTodos => prevTodos.filter(({ id }) => id !== todoId)); + }) + .catch(() => { + setErrorMessage('Unable to delete a todo'); + }) + .finally(() => { + setTodosIdToProcess(prevState => prevState.filter(id => id !== todoId)); + }); + }; + + const handleRenameTodo = (todo: Todo, newTodoTitle: string) => { + setTodosIdToProcess(prevState => [...prevState, todo.id]); + todoService + .updateTodo({ + id: todo.id, + title: newTodoTitle, + userId: todo.userId, + completed: todo.completed, + }) + .then(updatedTodo => { + setTodos(prevTodos => prevTodos.map(currentTodo => ( + currentTodo.id !== updatedTodo.id + ? currentTodo + : updatedTodo + ))); + }) + .catch(() => { + setErrorMessage('Unable to update a todo'); + }) + .finally(() => { + setTodosIdToProcess( + prevState => prevState.filter(id => id !== todo.id), + ); + }); + }; + + const handleClearCompletedTodos = () => { + completedTodos.forEach(todo => ( + handleDeleteTodo(todo.id))); + }; + + const handleStatusTodoChange = (todo: Todo) => { + setTodosIdToProcess(prevState => [...prevState, todo.id]); + todoService + .updateTodo({ + ...todo, + completed: !todo.completed, + }) + .then(updatedTodo => { + setTodos(prevTodos => prevTodos.map(currentTodo => ( + currentTodo.id !== updatedTodo.id + ? currentTodo + : updatedTodo + ))); + }) + .catch(() => { + setErrorMessage('Unable to update a todo'); + }) + .finally(() => { + setTodosIdToProcess( + prevState => prevState.filter(id => id !== todo.id), + ); + }); + }; + + const value = useMemo(() => ({ + todos, + setTodos, + completedTodos, + uncompletedTodos, + errorMessage, + setErrorMessage, + tempTodo, + setTempTodo, + todosIdToProcess, + setTodosIdToProcess, + handleAddTodo, + handleDeleteTodo, + handleRenameTodo, + handleClearCompletedTodos, + handleStatusTodoChange, + }), [todos, errorMessage, tempTodo, todosIdToProcess]); + + return ( + + {children} + + ); +}; diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 0000000000..cda61333f8 --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,29 @@ +import { Todo } from '../types/Todo'; +import { client } from '../utils/fetchClient'; +import { USER_ID } from '../utils/constants'; + +export const getTodos = () => { + return client.get(`/todos?userId=${USER_ID}`); +}; + +export const deleteTodo = (todoId: number) => { + return client.delete(`/todos/${todoId}`); +}; + +export const addTodo = (todoTitle: string) => { + return client.post('/todos', { + title: todoTitle, + userId: USER_ID, + completed: false, + }); +}; + +export const updateTodo = ({ + id, title, userId, completed, +}: Todo): Promise => { + return client.patch(`/todos/${id}`, { + title, + userId, + completed, + }); +}; diff --git a/src/components/TodoFilter.tsx b/src/components/TodoFilter.tsx new file mode 100644 index 0000000000..44a12a3d2c --- /dev/null +++ b/src/components/TodoFilter.tsx @@ -0,0 +1,91 @@ +import React, { useContext } from 'react'; +import cn from 'classnames'; +import { TodosFilter } from '../types/TodosFilter'; +import { TodoContext } from '../Context/TodoContext'; + +const setFilterHref = (filter: TodosFilter) => { + switch (filter) { + case TodosFilter.Active: + return '#/active'; + + case TodosFilter.Completed: + return '#/completed'; + + case TodosFilter.All: + default: + return '#/'; + } +}; + +const setFilterDataCy = (filter: TodosFilter) => { + switch (filter) { + case TodosFilter.Active: + return 'FilterLinkActive'; + + case TodosFilter.Completed: + return 'FilterLinkCompleted'; + + case TodosFilter.All: + default: + return 'FilterLinkAll'; + } +}; + +type Props = { + filter: TodosFilter; + onFilterChange: (filter: TodosFilter) => void; +}; + +export const TodoFilter: React.FC = ({ + filter, + onFilterChange, +}) => { + const { + completedTodos, + uncompletedTodos, + + handleClearCompletedTodos, + } = useContext(TodoContext); + const uncompletedTodosAmount = uncompletedTodos.length; + const hasCompletedTodos = !!completedTodos.length; + + return ( +
+ {/* Hide the footer if there are no todos */} + + {`${uncompletedTodosAmount} items left`} + + + {/* Active filter should have a 'selected' class */} + + + {/* don't show this button if there are no completed todos */} + +
+ ); +}; diff --git a/src/components/TodoForm.tsx b/src/components/TodoForm.tsx new file mode 100644 index 0000000000..beb63639dd --- /dev/null +++ b/src/components/TodoForm.tsx @@ -0,0 +1,101 @@ +import React, { + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import cn from 'classnames'; +import { TodoContext } from '../Context/TodoContext'; + +export const TodoForm: React.FC = () => { + const { + todos, + completedTodos, + uncompletedTodos, + handleAddTodo, + setErrorMessage, + handleStatusTodoChange, + } = useContext(TodoContext); + + const isTodosToShow = !!todos.length; + const titleField = useRef(null); + const [title, setTitle] = useState(''); + const [isAdding, setIsAdding] = useState(false); + + const areAllTodoCompleted = todos.length === completedTodos.length; + + const handleTitleChange = (event: React.ChangeEvent) => { + setTitle(event.target.value); + }; + + const handleToggleAll = () => { + if (areAllTodoCompleted) { + completedTodos.forEach(todo => handleStatusTodoChange(todo)); + } else { + uncompletedTodos.forEach(todo => handleStatusTodoChange(todo)); + } + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + const preparedTitle = title.trim(); + + if (!preparedTitle) { + setErrorMessage('Title should not be empty'); + + return; + } + + setIsAdding(true); + + handleAddTodo(preparedTitle) + .then(() => { + setTitle(''); + }) + .catch(() => { + + }) + .finally(() => { + setIsAdding(false); + }); + }; + + useEffect(() => { + if (!isAdding) { + titleField.current?.focus(); + } + }, [isAdding]); + + return ( +
+ {/* this buttons is active only if there are some active todos */} + + {isTodosToShow && ( +
+ ); +}; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 0000000000..287dc72479 --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,123 @@ +import React, { useContext, useEffect, useRef, useState } from 'react'; +import cn from 'classnames'; +import { Todo } from '../types/Todo'; +import { TodoContext } from '../Context/TodoContext'; + +type Props = { + todo: Todo; +}; + +export const TodoItem: React.FC = ({ todo }) => { + const { + handleRenameTodo, + handleDeleteTodo, + todosIdToProcess, + handleStatusTodoChange, + } = useContext(TodoContext); + + const { completed, title, id } = todo; + const [isEditing, setIsEditing] = useState(false); + const [todoTitle, setTodoTitle] = useState(title); + // const [isCompleted, setIsCompleted] = useState(completed); + const isProcessing = todosIdToProcess.includes(id) || id === 0; + + const titleInput = useRef(null); + + useEffect(() => { + if (isEditing && titleInput.current) { + titleInput.current.focus(); + } + }, [isEditing]); + + const handleTodoSave = async (event: React.FormEvent) => { + event.preventDefault(); + + const preparedTodoTitle = todoTitle.trim(); + + if (preparedTodoTitle) { + await handleRenameTodo(todo, preparedTodoTitle); + } else { + await handleDeleteTodo(id); + } + + setIsEditing(false); + }; + + const handleTodoTitleChange = ( + event: React.ChangeEvent, + ) => { + setTodoTitle(event.target.value); + }; + + const handleTodoDoubleClick = () => { + setIsEditing(true); + }; + + return ( +
+ + + {/* This form is shown instead of the title and remove button */} + {isEditing + ? ( +
+ +
+ ) : ( + <> + + {title} + + + {/* Remove button appears only on hover */} + + + )} + + {/* overlay will cover the todo while it is being updated */} + {/* 'is-active' class puts this modal on top of the todo */} +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx new file mode 100644 index 0000000000..bbd696f2fd --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,31 @@ +import React, { useContext } from 'react'; +import { Todo } from '../types/Todo'; +import { TodoItem } from './TodoItem'; +import { TodoContext } from '../Context/TodoContext'; + +type Props = { + todos: Todo[]; +}; + +export const TodoList: React.FC = ({ + todos, +}) => { + const { tempTodo } = useContext(TodoContext); + + return ( +
+ {todos.map(todo => ( + + ))} + + {tempTodo && ( + + )} +
+ ); +}; diff --git a/src/index.tsx b/src/index.tsx index 7de19e0c70..d3ca54266f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,11 @@ import '@fortawesome/fontawesome-free/css/all.css'; import './styles/index.scss'; import { App } from './App'; +import { TodoProvider } from './Context/TodoContext'; createRoot(document.getElementById('root') as HTMLDivElement) - .render(); + .render( + + + , + ); diff --git a/src/styles/index.scss b/src/styles/index.scss index bccd80c8bc..5a6d933c84 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -14,7 +14,7 @@ body { min-height: 36px; } -.notification.hidden { +.hidden { min-height: 0; opacity: 0; pointer-events: none; diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 0000000000..3f52a5fdde --- /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/types/TodosFilter.ts b/src/types/TodosFilter.ts new file mode 100644 index 0000000000..5b2422df9e --- /dev/null +++ b/src/types/TodosFilter.ts @@ -0,0 +1,5 @@ +export enum TodosFilter { + All = 'All', + Active = 'Active', + Completed = 'Completed', +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000000..628918d43b --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1 @@ +export const USER_ID = 11513; diff --git a/src/utils/countTodos.ts b/src/utils/countTodos.ts new file mode 100644 index 0000000000..2c9c2fbefa --- /dev/null +++ b/src/utils/countTodos.ts @@ -0,0 +1,9 @@ +import { Todo } from '../types/Todo'; + +export const getUncompletedTodos = (todos: Todo[]) => { + return todos.filter(({ completed }) => !completed); +}; + +export const getCompletedTodos = (todos: Todo[]) => { + return todos.filter(({ completed }) => completed); +}; diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts new file mode 100644 index 0000000000..ca588ab63a --- /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', + }; + } + + // we wait for testing purpose to see loaders + 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'), +}; diff --git a/src/utils/getFilteredTodos.ts b/src/utils/getFilteredTodos.ts new file mode 100644 index 0000000000..14630e31a9 --- /dev/null +++ b/src/utils/getFilteredTodos.ts @@ -0,0 +1,15 @@ +import { Todo } from '../types/Todo'; +import { TodosFilter } from '../types/TodosFilter'; + +export const getFilteredTodos = (todos: Todo[], filter: TodosFilter) => { + switch (filter) { + case TodosFilter.Active: + return todos.filter(({ completed }) => !completed); + + case TodosFilter.Completed: + return todos.filter(({ completed }) => completed); + + default: + return todos; + } +}; From f77e360d36224cfb242e820a2fc7b2de0f7cbd83 Mon Sep 17 00:00:00 2001 From: Daryna Pidlutska Date: Wed, 27 Sep 2023 08:04:11 +0200 Subject: [PATCH 2/4] add readme --- README.md | 2 +- src/Context/TodoContext.tsx | 2 +- src/components/TodoItem.tsx | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index af7dae81f6..b69b06391d 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,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://dpidlutska.github.io/react_todo-app-with-api/) and add it to the PR description. diff --git a/src/Context/TodoContext.tsx b/src/Context/TodoContext.tsx index 41df2a9793..55d2462b36 100644 --- a/src/Context/TodoContext.tsx +++ b/src/Context/TodoContext.tsx @@ -2,7 +2,7 @@ import React, { createContext, useMemo, useState } from 'react'; import { Todo } from '../types/Todo'; import { USER_ID } from '../utils/constants'; import * as todoService from '../api/todos'; -import { getCompletedTodos, getUncompletedTodos} from '../utils/countTodos'; +import { getCompletedTodos, getUncompletedTodos } from '../utils/countTodos'; interface TodoContextTypes { todos: Todo[]; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 287dc72479..fb482c5769 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -1,4 +1,6 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { + useContext, useEffect, useRef, useState, +} from 'react'; import cn from 'classnames'; import { Todo } from '../types/Todo'; import { TodoContext } from '../Context/TodoContext'; From 32695aeb490148ecf89e02b9dfca5a8cdaca4058 Mon Sep 17 00:00:00 2001 From: Daryna Pidlutska Date: Thu, 19 Oct 2023 15:04:23 +0200 Subject: [PATCH 3/4] fix code --- src/App.tsx | 48 ++-------------------------- src/Context/TodoContext.tsx | 3 +- src/components/ErrorNotification.tsx | 42 ++++++++++++++++++++++++ src/components/TodoForm.tsx | 12 +++++-- src/components/TodoItem.tsx | 27 ++++++++++++---- 5 files changed, 76 insertions(+), 56 deletions(-) create mode 100644 src/components/ErrorNotification.tsx diff --git a/src/App.tsx b/src/App.tsx index 5a2d433773..74a5aa2eaa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,7 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ import React, { useContext, - useEffect, useMemo, useRef, useState, + useEffect, useMemo, useState, } from 'react'; -import cn from 'classnames'; import { TodoForm } from './components/TodoForm'; import { TodoList } from './components/TodoList'; import { TodoFilter } from './components/TodoFilter'; @@ -11,12 +9,12 @@ import * as todoService from './api/todos'; import { TodosFilter } from './types/TodosFilter'; import { getFilteredTodos } from './utils/getFilteredTodos'; import { TodoContext } from './Context/TodoContext'; +import { ErrorNotification } from './components/ErrorNotification'; export const App: React.FC = () => { const { todos, setTodos, - errorMessage, setErrorMessage, } = useContext(TodoContext); @@ -30,18 +28,6 @@ export const App: React.FC = () => { }); }, []); - const timerId = useRef(0); - - useEffect(() => { - if (timerId.current) { - window.clearTimeout(timerId.current); - } - - timerId.current = window.setTimeout(() => { - setErrorMessage(''); - }, 3000); - }, [errorMessage]); - const filteredTodos = useMemo(() => { return getFilteredTodos(todos, filter); }, [todos, filter]); @@ -63,35 +49,7 @@ export const App: React.FC = () => { )}
- - {/* Notification is shown in case of any error */} - {/* Add the 'hidden' class to hide the message smoothly */} -
-
+
); }; diff --git a/src/Context/TodoContext.tsx b/src/Context/TodoContext.tsx index 55d2462b36..d784e01c24 100644 --- a/src/Context/TodoContext.tsx +++ b/src/Context/TodoContext.tsx @@ -108,8 +108,9 @@ export const TodoProvider: React.FC = ({ children }) => { : updatedTodo ))); }) - .catch(() => { + .catch((error) => { setErrorMessage('Unable to update a todo'); + throw error; }) .finally(() => { setTodosIdToProcess( diff --git a/src/components/ErrorNotification.tsx b/src/components/ErrorNotification.tsx new file mode 100644 index 0000000000..ecb3855704 --- /dev/null +++ b/src/components/ErrorNotification.tsx @@ -0,0 +1,42 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import React, { useContext, useEffect, useRef } from 'react'; +import cn from 'classnames'; +import { TodoContext } from '../Context/TodoContext'; + +export const ErrorNotification: React.FC = () => { + const { + errorMessage, + setErrorMessage, + } = useContext(TodoContext); + + const timerId = useRef(0); + + useEffect(() => { + if (timerId.current) { + window.clearTimeout(timerId.current); + } + + timerId.current = window.setTimeout(() => { + setErrorMessage(''); + }, 3000); + }, [errorMessage]); + + return ( +
+
+ ); +}; diff --git a/src/components/TodoForm.tsx b/src/components/TodoForm.tsx index beb63639dd..8f710eb5b6 100644 --- a/src/components/TodoForm.tsx +++ b/src/components/TodoForm.tsx @@ -62,10 +62,16 @@ export const TodoForm: React.FC = () => { }; useEffect(() => { - if (!isAdding) { - titleField.current?.focus(); + if (titleField.current) { + titleField.current.focus(); } - }, [isAdding]); + }); + + // useEffect(() => { + // if (!isAdding) { + // titleField.current?.focus(); + // } + // }, [isAdding]); return (
diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index fb482c5769..2b51156c18 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -36,13 +36,18 @@ export const TodoItem: React.FC = ({ todo }) => { const preparedTodoTitle = todoTitle.trim(); - if (preparedTodoTitle) { - await handleRenameTodo(todo, preparedTodoTitle); - } else { - await handleDeleteTodo(id); - } + try { + if (preparedTodoTitle) { + await handleRenameTodo(todo, preparedTodoTitle); + } else { + await handleDeleteTodo(id); + } - setIsEditing(false); + setIsEditing(false); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + } }; const handleTodoTitleChange = ( @@ -55,6 +60,13 @@ export const TodoItem: React.FC = ({ todo }) => { setIsEditing(true); }; + const handlePressedKey = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setIsEditing(false); + setTodoTitle(title); + } + }; + return (
= ({ todo }) => { type="checkbox" className="todo__status" checked={completed} - onChange={() => handleStatusTodoChange(todo)} + onClick={() => handleStatusTodoChange(todo)} /> @@ -85,6 +97,7 @@ export const TodoItem: React.FC = ({ todo }) => { ref={titleInput} value={todoTitle} onChange={handleTodoTitleChange} + onKeyUp={handlePressedKey} /> ) : ( From 94f2ecf45416a9472ba1d238b5d74c4051919106 Mon Sep 17 00:00:00 2001 From: Daryna Pidlutska Date: Thu, 19 Oct 2023 16:24:28 +0200 Subject: [PATCH 4/4] fix code according to mentor comments --- src/components/TodoFilter.tsx | 64 ++++++++++++++++------------------- src/components/TodoForm.tsx | 11 +----- src/components/TodoItem.tsx | 5 --- 3 files changed, 31 insertions(+), 49 deletions(-) diff --git a/src/components/TodoFilter.tsx b/src/components/TodoFilter.tsx index 44a12a3d2c..b2657b9f07 100644 --- a/src/components/TodoFilter.tsx +++ b/src/components/TodoFilter.tsx @@ -3,31 +3,26 @@ import cn from 'classnames'; import { TodosFilter } from '../types/TodosFilter'; import { TodoContext } from '../Context/TodoContext'; -const setFilterHref = (filter: TodosFilter) => { +const setFilterDataCyAndHref = (filter: TodosFilter) => { switch (filter) { case TodosFilter.Active: - return '#/active'; + return { + href: '#/active', + dataCy: 'FilterLinkActive', + }; case TodosFilter.Completed: - return '#/completed'; + return { + href: '#/completed', + dataCy: 'FilterLinkCompleted', + }; case TodosFilter.All: default: - return '#/'; - } -}; - -const setFilterDataCy = (filter: TodosFilter) => { - switch (filter) { - case TodosFilter.Active: - return 'FilterLinkActive'; - - case TodosFilter.Completed: - return 'FilterLinkCompleted'; - - case TodosFilter.All: - default: - return 'FilterLinkAll'; + return { + href: '#/', + dataCy: 'FilterLinkAll', + }; } }; @@ -51,29 +46,30 @@ export const TodoFilter: React.FC = ({ return (