Skip to content

Commit

Permalink
working solution
Browse files Browse the repository at this point in the history
  • Loading branch information
Krykunov committed Nov 30, 2024
1 parent 3c4e532 commit 6236ae3
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 37 deletions.
24 changes: 9 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -27,6 +27,9 @@ export const App: React.FC = () => {
handleClearCompleted,
handleToggleCompleted,
handleToggleAll,
renamingTodo,
setRenamingTodo,
handleUpdateTodoTitle,
} = useTodos();

if (!postService.USER_ID) {
Expand All @@ -35,13 +38,6 @@ export const App: React.FC = () => {

return (
<div className="todoapp">
{/*'remove before deploy !!!'*/}
<DevHelper
onSetFakeTodos={addTodo}
onDeleteAllTodos={deleteTodo}
todos={todos}
/>
{/*'remove before deploy !!!'*/}
<h1 className="todoapp__title">todos</h1>
<div className="todoapp__content">
<Header
Expand All @@ -50,6 +46,7 @@ export const App: React.FC = () => {
isLoading={isLoading}
setErrorMessage={setErrorMessage}
handleToggleAll={handleToggleAll}
renamingTodo={renamingTodo}
/>

<section className="todoapp__main" data-cy="TodoList">
Expand All @@ -62,18 +59,15 @@ export const App: React.FC = () => {
onDelete={deleteTodo}
editingTodos={editingTodos}
handleToggleCompleted={handleToggleCompleted}
renamingTodo={renamingTodo}
setRenamingTodo={setRenamingTodo}
handleUpdateTodoTitle={handleUpdateTodoTitle}
/>
</CSSTransition>
))}
{tempTodo && (
<CSSTransition key={0} timeout={300} classNames="item">
<TodoItem
key={tempTodo.id}
todo={tempTodo}
onDelete={deleteTodo}
editingTodos={editingTodos}
handleToggleCompleted={handleToggleCompleted}
/>
<TempTodoItem todo={tempTodo} editingTodos={editingTodos} />
</CSSTransition>
)}
</TransitionGroup>
Expand Down
8 changes: 4 additions & 4 deletions src/components/DevHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({
Expand Down
4 changes: 3 additions & 1 deletion src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ type Props = {
onCreateTodo: (todoTitle: string) => Promise<void>;
isLoading: boolean;
setErrorMessage: (message: string) => void;
renamingTodo: number | null;
};

const Form: React.FC<Props> = ({
onCreateTodo,
isLoading,
setErrorMessage,
renamingTodo,
}) => {
const [todoTitle, setTodoTitle] = useState('');

const titleField = useRef<HTMLInputElement>(null);

useEffect(() => {
if (titleField.current && !isLoading) {
if (titleField.current && !isLoading && !renamingTodo) {
titleField.current.focus();
}
}, [isLoading]);
Expand Down
3 changes: 3 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Props = {
isLoading: boolean;
setErrorMessage: (message: string) => void;
handleToggleAll: () => void;
renamingTodo: number | null;
};

const Header: React.FC<Props> = ({
Expand All @@ -18,6 +19,7 @@ const Header: React.FC<Props> = ({
isLoading,
setErrorMessage,
handleToggleAll,
renamingTodo,
}) => {
const isAllCompleted = todos.every(todo => todo.completed);

Expand All @@ -37,6 +39,7 @@ const Header: React.FC<Props> = ({
onCreateTodo={onCreateTodo}
isLoading={isLoading}
setErrorMessage={setErrorMessage}
renamingTodo={renamingTodo}
/>
</header>
);
Expand Down
42 changes: 42 additions & 0 deletions src/components/TempTodoItem.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ todo, editingTodos }) => {
return (
<div key={todo.id} data-cy="Todo" className="todo">
<label className="todo__status-label">
<input data-cy="TodoStatus" type="checkbox" className="todo__status" />
</label>

<span data-cy="TodoTitle" className="todo__title">
{todo.title}
</span>

<button type="button" className="todo__remove" data-cy="TodoDelete">
×
</button>

<div
data-cy="TodoLoader"
className={cn('modal overlay', {
'is-active': editingTodos.includes(todo.id),
})}
>
<div className="modal-background has-background-white-ter" />
<div className="loader" />
</div>
</div>
);
};

export default TodoItem;
107 changes: 95 additions & 12 deletions src/components/TodoItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,34 @@

import cn from 'classnames';

import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Todo } from '../types/Todo';

type Props = {
todo: Todo;
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<Props> = ({
todo,
onDelete,
editingTodos,
handleToggleCompleted,
renamingTodo,
setRenamingTodo,
handleUpdateTodoTitle,
}) => {
const [inputValue, setInputValue] = useState<string>('');

const isRenaming = renamingTodo === todo.id;

const todoField = useRef<HTMLInputElement>(null);

const handleDelete = () => {
onDelete(todo.id);
};
Expand All @@ -27,6 +39,55 @@ const TodoItem: React.FC<Props> = ({
handleToggleCompleted(todo.id);
};

const handleEditTodo = () => {
setRenamingTodo(todo.id);
setInputValue(todo.title);
};

const handleChangeValue = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};

const handleSubmitChange = (event: React.FormEvent<HTMLFormElement>) => {
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 (
<div
key={todo.id}
Expand All @@ -43,18 +104,40 @@ const TodoItem: React.FC<Props> = ({
/>
</label>

<span data-cy="TodoTitle" className="todo__title">
{todo.title}
</span>
{!isRenaming && (
<span
data-cy="TodoTitle"
className="todo__title"
onDoubleClick={handleEditTodo}

<button
type="button"
className="todo__remove"
data-cy="TodoDelete"
onClick={handleDelete}
>
×
</button>
// onClick={handleEditTodo}
>
{todo.title}
</span>
)}

{isRenaming && (
<form onSubmit={handleSubmitChange} onBlur={handleSubmitChange}>
<input
ref={todoField}
className="todo__title-field"
type="text"
value={inputValue}
onChange={handleChangeValue}
/>
</form>
)}

{!isRenaming && (
<button
type="button"
className="todo__remove"
data-cy="TodoDelete"
onClick={handleDelete}
>
×
</button>
)}

<div
data-cy="TodoLoader"
Expand Down
21 changes: 21 additions & 0 deletions src/hooks/useTodos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const useTodos = () => {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [activeFilter, setActiveFilter] = useState<Filters>(Filters.All);
const [editingTodos, setEditingTodos] = useState<number[]>([]);
const [renamingTodo, setRenamingTodo] = useState<number | null>(null);

const isAllCompleted = todos.every(todo => todo.completed);

Expand Down Expand Up @@ -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();
}, []);
Expand Down Expand Up @@ -159,5 +177,8 @@ export const useTodos = () => {
handleToggleCompleted,
handleToggleAll,
isAllCompleted,
renamingTodo,
setRenamingTodo,
handleUpdateTodoTitle,
};
};
10 changes: 5 additions & 5 deletions src/styles/todo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -72,16 +72,16 @@

&__title-field {
width: 100%;
padding: 11px 14px;
padding: 12px 15px;

font-size: inherit;
line-height: inherit;
font-family: inherit;
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;
Expand Down

0 comments on commit 6236ae3

Please sign in to comment.