Skip to content

Commit

Permalink
renaming function done
Browse files Browse the repository at this point in the history
  • Loading branch information
Iryna Mariiko authored and Iryna Mariiko committed Dec 26, 2024
1 parent e21709c commit 12e12c9
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 41 deletions.
104 changes: 92 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import React, { useEffect, useState, useMemo, useRef } from 'react';
import { UserWarning } from './UserWarning';
import { Header } from './components/Header';
import { Todo } from './types/Todo';
import { getTodos, addTodos, deleteTodos, USER_ID } from './api/todos';
import {
getTodos,
addTodos,
deleteTodos,
USER_ID,
updateTodos,
} from './api/todos';
import { TodoItem } from './components/TodoItem';
import { Errors } from './components/Errors';
import { Footer } from './components/Footer';
Expand All @@ -16,7 +22,9 @@ export const App: React.FC = () => {
const [currentFilter, setCurrentFilter] = useState<FilterBy>(FilterBy.All);
const [currentTodoIds, setCurrentTodoIds] = useState<number[]>([]);
const [isInputDisabled, setIsInputDisabled] = useState(false);
const [isToggleAllActive, setIsToggleAllActive] = useState(false);
const [query, setQuery] = useState('');
const [isLoading, setIsLoading] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -109,19 +117,87 @@ export const App: React.FC = () => {
});
};

const toggleTodoStatus = (updatedTodo: Todo) => {
setTodos(prevTodos => {
const copyTodos = [...prevTodos];
useEffect(() => {
const allCompleted = todos.every(todo => todo.completed);

setIsToggleAllActive(allCompleted);
}, [todos]);

const handleToggleAll = () => {
let updatedTodos: Todo[];
const allTodosCompleted = todos.every(todo => todo.completed);

if (allTodosCompleted) {
updatedTodos = todos.map(todo => ({
...todo,
completed: false,
}));
} else {
updatedTodos = todos
.filter(todo => !todo.completed)
.map(todo => ({
...todo,
completed: true,
}));
}

for (let i = 0; i < copyTodos.length; ++i) {
if (copyTodos[i].id === updatedTodo.id) {
copyTodos[i] = updatedTodo;
break;
Promise.all(updatedTodos.map(todo => updateTodos(todo)))
.then(() => {
if (allTodosCompleted) {
setTodos(updatedTodos);
} else {
setTodos(prevTodos =>
prevTodos.map(todo => {
if (allTodosCompleted) {
return todo;
}

return { ...todo, completed: true };
}),
);
}
}
})
.catch(() => {
setError(ErrorMessage.UpdateTodo);
setTodos(prevTodos => prevTodos);
});
};

return copyTodos;
});
const toggleTodoStatus = (updatedTodo: Todo) => {
const updatedTodoCopy = { ...updatedTodo };

setCurrentTodoIds([...currentTodoIds, updatedTodoCopy.id]);

updatedTodoCopy.completed = !updatedTodoCopy.completed;
updateTodos(updatedTodoCopy)
.then(() => {
setTodos(currentTodo =>
currentTodo.map(todo =>
todo.id === updatedTodoCopy.id
? { ...todo, completed: updatedTodoCopy.completed }
: todo,
),
);
})
.catch(() => {
setError(ErrorMessage.UpdateTodo);
setTodos(prevTodos => prevTodos);
})
.finally(() => setCurrentTodoIds([]));
};

const handleRenamingTodo = (renamedTodo: Todo) => {
setIsLoading(true);
updateTodos(renamedTodo)
.then(() => {
setTodos(currentTodos =>
currentTodos.map(todo =>
todo.id === renamedTodo.id ? renamedTodo : todo,
),
);
})
.catch(() => setError(ErrorMessage.UpdateTodo))
.finally(() => setIsLoading(false));
};

if (!USER_ID) {
Expand All @@ -140,6 +216,9 @@ export const App: React.FC = () => {
query={query}
setQuery={setQuery}
inputRef={inputRef}
handleToggleAll={handleToggleAll}
isToggleAllActive={isToggleAllActive}
todos={todos}
/>

<section className="todoapp__main" data-cy="TodoList">
Expand All @@ -149,7 +228,8 @@ export const App: React.FC = () => {
todo={todo}
onToggleStatus={toggleTodoStatus}
handleDeleteTodo={handleDeleteTodo}
isLoading={currentTodoIds.includes(todo.id)}
isLoading={currentTodoIds.includes(todo.id) || isLoading}
onRenamingTodo={handleRenamingTodo}
/>
))}
</section>
Expand Down
4 changes: 4 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export const addTodos = ({ userId, title, completed }: Omit<Todo, 'id'>) => {
export const deleteTodos = (todoId: number) => {
return client.delete(`/todos/${todoId}`);
};

export const updateTodos = (todo: Todo) => {
return client.patch<Todo>(`/todos/${todo.id}`, todo);
};
22 changes: 17 additions & 5 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import { Todo } from '../../types/Todo';
import { ErrorMessage } from '../../types/ErrorMessage';
import classNames from 'classnames';

type Props = {
onTodo: (todo: Todo) => void;
Expand All @@ -9,6 +10,9 @@ type Props = {
query: string;
setQuery: (v: string) => void;
inputRef: React.RefObject<HTMLInputElement>;
handleToggleAll: () => void;
isToggleAllActive: boolean;
todos: Todo[];
};

export const Header: React.FC<Props> = ({
Expand All @@ -18,6 +22,9 @@ export const Header: React.FC<Props> = ({
query,
setQuery,
inputRef,
handleToggleAll,
isToggleAllActive,
todos,
}) => {
useEffect(() => {
if (inputRef.current && !isInputDisabled) {
Expand Down Expand Up @@ -53,11 +60,16 @@ export const Header: React.FC<Props> = ({
return (
<header className="todoapp__header">
{/* this button should have `active` class only if all todos are completed */}
<button
type="button"
className="todoapp__toggle-all active"
data-cy="ToggleAllButton"
/>
{todos.length > 0 && (
<button
type="button"
className={classNames('todoapp__toggle-all', {
active: isToggleAllActive,
})}
data-cy="ToggleAllButton"
onClick={handleToggleAll}
/>
)}

{/* Add a todo on form submit */}
<form onSubmit={handleAddTodo}>
Expand Down
105 changes: 83 additions & 22 deletions src/components/TodoItem/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React, { useState } from 'react';
import { Todo } from '../../types/Todo';
import classNames from 'classnames';

Expand All @@ -6,53 +7,113 @@ type Props = {
onToggleStatus: (v: Todo) => void;
handleDeleteTodo: (v: number) => void;
isLoading: boolean;
onRenamingTodo: (v: Todo) => void;
};

export const TodoItem: React.FC<Props> = ({
todo,
todo: todoItem, // змінив ім'я параметра
onToggleStatus,
handleDeleteTodo,
isLoading,
onRenamingTodo,
}) => {
const handleChangeStatus = () => {
const updatedTodo = { ...todo, completed: !todo.completed };
const [isEditing, setIsEditing] = useState(false);
const [renamedTodo, setRenamedTodo] = useState(todoItem.title);

onToggleStatus(updatedTodo);
const handleDoubleClick = () => {
setIsEditing(true);
};

const handleRenamingTodo = (
event: // eslint-disable-next-line
React.KeyboardEvent<HTMLInputElement> | React.FocusEvent<HTMLInputElement>,
) => {
if ('key' in event && event.key === 'Escape') {
setIsEditing(false);
setRenamedTodo(todoItem.title);

return;
}

if ('key' in event && event.key !== 'Enter') {
return;
}

const trimmedTitle = renamedTodo.trim();

if (trimmedTitle === todoItem.title) {
setIsEditing(false); // Залишити закритим, якщо нічого не змінилося

return;
}

if (!trimmedTitle) {
handleDeleteTodo(todoItem.id);

return;
}

// Викликаємо onRenamingTodo без .catch(), якщо це не асинхронна функція
onRenamingTodo({
...todoItem,
title: trimmedTitle,
});

// Якщо редагування успішне, закриваємо форму
setIsEditing(false);
};

return (
<div
data-cy="Todo"
className={classNames('todo', {
completed: todo.completed,
completed: todoItem.completed && !isEditing,
})}
key={todo.id}
key={todoItem.id}
>
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
checked={todo.completed}
checked={todoItem.completed}
aria-label="Mark as completed"
onChange={handleChangeStatus}
onChange={() => onToggleStatus(todoItem)}
/>
</label>

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

{/* Remove button appears only on hover */}
<button
type="button"
className="todo__remove"
data-cy="TodoDelete"
onClick={() => handleDeleteTodo(todo.id)}
disabled={isLoading}
>
×
</button>
{isEditing ? (
<input
type="text"
value={renamedTodo}
onChange={e => setRenamedTodo(e.target.value)}
onBlur={handleRenamingTodo}
onKeyUp={handleRenamingTodo}
autoFocus
data-cy="TodoTitleField"
className="todo__title todo__title--editing"
/>
) : (
<>
<span
data-cy="TodoTitle"
className="todo__title"
onDoubleClick={handleDoubleClick}
>
{todoItem.title}
</span>

<button
type="button"
className="todo__remove"
data-cy="TodoDelete"
onClick={() => handleDeleteTodo(todoItem.id)}
disabled={isLoading}
>
×
</button>
</>
)}

{/* Overlay for loader during status change */}
<div
Expand Down
10 changes: 8 additions & 2 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 All @@ -33,6 +33,12 @@

word-break: break-all;
transition: color 0.4s;

&--editing {
font-size: 24px;
font-family: inherit;
text-decoration: none;
}
}

&.completed &__title {
Expand Down
1 change: 1 addition & 0 deletions src/types/ErrorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export enum ErrorMessage {
Load = 'Unable to load todos',
Add = 'Unable to add a todo',
Delete = 'Unable to delete a todo',
UpdateTodo = 'Unable to update a todo',
}

0 comments on commit 12e12c9

Please sign in to comment.