diff --git a/src/App.tsx b/src/App.tsx index a2bd924300..1d4b9145ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,10 +4,10 @@ import * as postService from './api/todos'; import { UserWarning } from './UserWarning'; import TodoItem from './components/TodoItem'; +import TempTodoItem from './components/TempTodoItem'; import Header from './components/Header'; import Footer from './components/Footer'; import ErrorNotification from './components/ErrorNotification'; -import DevHelper from './components/DevHelper'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { useTodos } from './hooks/useTodos'; @@ -27,6 +27,9 @@ export const App: React.FC = () => { handleClearCompleted, handleToggleCompleted, handleToggleAll, + renamingTodo, + setRenamingTodo, + handleUpdateTodoTitle, } = useTodos(); if (!postService.USER_ID) { @@ -35,13 +38,6 @@ export const App: React.FC = () => { return (
- {/*'remove before deploy !!!'*/} - - {/*'remove before deploy !!!'*/}

todos

{ isLoading={isLoading} setErrorMessage={setErrorMessage} handleToggleAll={handleToggleAll} + renamingTodo={renamingTodo} />
@@ -62,18 +59,15 @@ export const App: React.FC = () => { onDelete={deleteTodo} editingTodos={editingTodos} handleToggleCompleted={handleToggleCompleted} + renamingTodo={renamingTodo} + setRenamingTodo={setRenamingTodo} + handleUpdateTodoTitle={handleUpdateTodoTitle} /> ))} {tempTodo && ( - + )} diff --git a/src/components/DevHelper.tsx b/src/components/DevHelper.tsx index 28c25d74c7..53e4e15360 100644 --- a/src/components/DevHelper.tsx +++ b/src/components/DevHelper.tsx @@ -14,12 +14,12 @@ const fakeTodos: string[] = [ 'fugiat veniam minus', 'et porro tempora', 'laboriosam mollitia et enim ', - ' quasi adipisci quia provident illum', - 'qui ullam ratione quibusdam quia omnis', - 'illo expedita quia in', + 'quasi adipisci quia provident illum', + 'qui ullam ratione quibusdam quia omnis', + 'illo expedita quia in', 'quo adipisci enim quam ut ab', 'molestiae perspiciatis ipsa', - 'illo est ratione quia maiores aut', + 'illo est ratione quia maiores aut', ]; const DevHelper: React.FC = ({ diff --git a/src/components/Form.tsx b/src/components/Form.tsx index 9368df78ba..2ea1a1e635 100644 --- a/src/components/Form.tsx +++ b/src/components/Form.tsx @@ -4,19 +4,21 @@ type Props = { onCreateTodo: (todoTitle: string) => Promise; isLoading: boolean; setErrorMessage: (message: string) => void; + renamingTodo: number | null; }; const Form: React.FC = ({ onCreateTodo, isLoading, setErrorMessage, + renamingTodo, }) => { const [todoTitle, setTodoTitle] = useState(''); const titleField = useRef(null); useEffect(() => { - if (titleField.current && !isLoading) { + if (titleField.current && !isLoading && !renamingTodo) { titleField.current.focus(); } }, [isLoading]); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 931d0d26b8..7ab0e10601 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -10,6 +10,7 @@ type Props = { isLoading: boolean; setErrorMessage: (message: string) => void; handleToggleAll: () => void; + renamingTodo: number | null; }; const Header: React.FC = ({ @@ -18,6 +19,7 @@ const Header: React.FC = ({ isLoading, setErrorMessage, handleToggleAll, + renamingTodo, }) => { const isAllCompleted = todos.every(todo => todo.completed); @@ -37,6 +39,7 @@ const Header: React.FC = ({ onCreateTodo={onCreateTodo} isLoading={isLoading} setErrorMessage={setErrorMessage} + renamingTodo={renamingTodo} />
); diff --git a/src/components/TempTodoItem.tsx b/src/components/TempTodoItem.tsx new file mode 100644 index 0000000000..0abbc26497 --- /dev/null +++ b/src/components/TempTodoItem.tsx @@ -0,0 +1,42 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/control-has-associated-label */ + +import cn from 'classnames'; + +import React from 'react'; +import { Todo } from '../types/Todo'; + +type Props = { + todo: Todo; + editingTodos: number[]; +}; + +const TodoItem: React.FC = ({ todo, editingTodos }) => { + return ( +
+ + + + {todo.title} + + + + +
+
+
+
+
+ ); +}; + +export default TodoItem; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index a05737b0c5..5cbeb730ee 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -3,7 +3,7 @@ import cn from 'classnames'; -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Todo } from '../types/Todo'; type Props = { @@ -11,6 +11,9 @@ type Props = { onDelete: (id: number) => void; editingTodos: number[]; handleToggleCompleted: (id: number) => void; + renamingTodo: number | null; + setRenamingTodo: (id: number | null) => void; + handleUpdateTodoTitle: (id: number, title: string) => void; }; const TodoItem: React.FC = ({ @@ -18,7 +21,16 @@ const TodoItem: React.FC = ({ onDelete, editingTodos, handleToggleCompleted, + renamingTodo, + setRenamingTodo, + handleUpdateTodoTitle, }) => { + const [inputValue, setInputValue] = useState(''); + + const isRenaming = renamingTodo === todo.id; + + const todoField = useRef(null); + const handleDelete = () => { onDelete(todo.id); }; @@ -27,6 +39,55 @@ const TodoItem: React.FC = ({ handleToggleCompleted(todo.id); }; + const handleEditTodo = () => { + setRenamingTodo(todo.id); + setInputValue(todo.title); + }; + + const handleChangeValue = (event: React.ChangeEvent) => { + setInputValue(event.target.value); + }; + + const handleSubmitChange = (event: React.FormEvent) => { + event.preventDefault(); + if (inputValue === '') { + onDelete(todo.id); + + return; + } + + if (inputValue !== todo.title) { + handleUpdateTodoTitle(todo.id, inputValue); + setRenamingTodo(null); + + return; + } + + setRenamingTodo(null); + }; + + useEffect(() => { + const handleEsc = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setRenamingTodo(null); + + return; + } + }; + + window.addEventListener('keyup', handleEsc); + + return () => { + window.removeEventListener('keyup', handleEsc); + }; + }, []); + + useEffect(() => { + if (todoField.current) { + todoField.current.focus(); + } + }, [renamingTodo]); + return (
= ({ /> - - {todo.title} - + {!isRenaming && ( + - × - + // onClick={handleEditTodo} + > + {todo.title} + + )} + + {isRenaming && ( +
+ +
+ )} + + {!isRenaming && ( + + )}
{ const [errorMessage, setErrorMessage] = useState(null); const [activeFilter, setActiveFilter] = useState(Filters.All); const [editingTodos, setEditingTodos] = useState([]); + const [renamingTodo, setRenamingTodo] = useState(null); const isAllCompleted = todos.every(todo => todo.completed); @@ -132,6 +133,23 @@ export const useTodos = () => { }); }; + const handleUpdateTodoTitle = (id: number, title: string) => { + const newTodo = todos.find(todo => todo.id === id); + + if (!newTodo) { + setErrorMessage('Todo not found'); + + return; + } + + const updatedTodo: Todo = { + ...newTodo, + title, + }; + + updateTodo(updatedTodo); + }; + useEffect(() => { loadTodos(); }, []); @@ -159,5 +177,8 @@ export const useTodos = () => { handleToggleCompleted, handleToggleAll, isAllCompleted, + renamingTodo, + setRenamingTodo, + handleUpdateTodoTitle, }; }; diff --git a/src/styles/todo.scss b/src/styles/todo.scss index c7f93ff6b9..cdadf964d9 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 { @@ -72,7 +72,7 @@ &__title-field { width: 100%; - padding: 11px 14px; + padding: 12px 15px; font-size: inherit; line-height: inherit; @@ -80,8 +80,8 @@ font-weight: inherit; color: inherit; - border: 1px solid #999; - box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + border: none; + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); &::placeholder { font-style: italic;