Skip to content

Commit

Permalink
refactored code
Browse files Browse the repository at this point in the history
  • Loading branch information
VikaChereushenko committed Dec 17, 2024
1 parent cc7c529 commit 294cfc6
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 341 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,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 `<your_account>` with your Github username in the [DEMO LINK](https://<your_account>.github.io/react_todo-app-with-api/) and add it to the PR description.
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://VikaChereushenko.github.io/react_todo-app-with-api/) and add it to the PR description.
247 changes: 17 additions & 230 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,18 @@ import { TodoList } from './components/TodoList/TodoList';
import { TempTodo } from './components/TempTodo/TempTodo';
import { Footer } from './components/Footer/Fotter';
import { Error } from './components/Error/Erros';
import { getTodos, addTodo, deleteTodo, updateTodo } from './api/todos';
import { getTodos } from './api/todos';
import { filterTodos } from './components/Helpers/Helpers';

import { Todo } from './types/Todo';
import { TodoStatus } from './types/Status';

function filterTodos(todos: Todo[], status: TodoStatus) {
const todosCopy = [...todos];

switch (status) {
case TodoStatus.active:
return todosCopy.filter(todo => !todo.completed);
case TodoStatus.completed:
return todosCopy.filter(todo => todo.completed);
case TodoStatus.all:
return todosCopy;
}
}

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState<TodoStatus>(TodoStatus.all);
const [title, setTitle] = useState('');
const [loading, setLoading] = useState(false);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [processedIs, setProcessedIs] = useState<number[]>([]);
const areAllTodosCompleted = todos.every(todo => todo.completed);
const [processedIds, setprocessedIds] = useState<number[]>([]);
const noTodos = todos.length === 0;
const filteredTodos = filterTodos(todos, status);

Expand All @@ -45,204 +30,6 @@ export const App: React.FC = () => {
.catch(() => setErrorMessage('Unable to load todos'));
}, []);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

const normalizeTitle = title.trim();

if (!normalizeTitle) {
setErrorMessage('Title should not be empty');

return;
}

setLoading(true);
const newTodo = {
userId: 2042,
title: normalizeTitle,
completed: false,
};

setTempTodo({
id: 0,
...newTodo,
});

addTodo(newTodo)
.then(response => {
setTitle('');
setTodos(existing => [...existing, response]);
setLoading(false);
})
.catch(() => setErrorMessage('Unable to add a todo'))
.finally(() => {
setLoading(false);
setTempTodo(null);
});
};

const handleDeleteOneTodo = (id: number) => {
setLoading(true);
setProcessedIs(existing => [...existing, id]);
deleteTodo(id)
.then(() => {
setTodos(existing => existing.filter(current => current.id !== id));
})
.catch(() => setErrorMessage('Unable to delete a todo'))
.finally(() => {
setProcessedIs([]);
setLoading(false);
});
};

const handleDeleteCompletedTodos = () => {
todos.forEach(todo => {
if (todo.completed) {
setLoading(true);
setProcessedIs(existing => [...existing, todo.id]);
deleteTodo(todo.id)
.then(() =>
setTodos(existing =>
existing.filter(current => current.id !== todo.id),
),
)
.catch(() => setErrorMessage('Unable to delete a todo'))
.finally(() => {
setLoading(false);
setProcessedIs(existing => existing.filter(id => id !== todo.id));
});
}
});
};

const handleStatusUpdate = (id: number) => {
setProcessedIs(existing => [...existing, id]);

const changeItem = todos.find(todo => todo.id === id);

if (changeItem) {
const toUpdate = { completed: !changeItem.completed };

updateTodo(id, toUpdate)
.then(() => {
setTodos(existing =>
existing.map(el =>
el.id === id ? { ...el, completed: toUpdate.completed } : el,
),
);
})
.catch(() => setErrorMessage('Unable to update a todo'))
.finally(() => {
setProcessedIs([]);
});
}
};

const handleTotalStatusUpdate = () => {
if (!areAllTodosCompleted) {
todos.forEach(todo => {
if (!todo.completed) {
setLoading(true);
setProcessedIs(existing => [...existing, todo.id]);
const toUpdate = { completed: true };

updateTodo(todo.id, toUpdate)
.then(() => {
setTodos(existing =>
existing.map(el =>
el.id === todo.id
? { ...el, completed: toUpdate.completed }
: el,
),
);
})
.catch(() => setErrorMessage('Unable to update a todo'))
.finally(() => {
setLoading(false);
setProcessedIs(existing => existing.filter(id => id !== todo.id));
});
}
});
}

if (areAllTodosCompleted) {
todos.forEach(todo => {
setLoading(true);
setProcessedIs(existing => [...existing, todo.id]);
const toUpdate = { completed: false };

updateTodo(todo.id, toUpdate)
.then(() => {
setTodos(existing =>
existing.map(el =>
el.id === todo.id
? { ...el, completed: toUpdate.completed }
: el,
),
);
})
.catch(() => setErrorMessage('Unable to update a todo'))
.finally(() => {
setLoading(false);
setProcessedIs(existing => existing.filter(id => id !== todo.id));
});
});
}
};

const handleTitleUpdate = (
id: number,
newTitle: string,
setTitleBeingUpdated: React.Dispatch<React.SetStateAction<boolean>>,
isTitleChanged: boolean,
e?: React.FormEvent<HTMLFormElement>,
) => {
e?.preventDefault();

const normalizeNewTitle = newTitle.trim();

if (!normalizeNewTitle) {
handleDeleteOneTodo(id);

return;
}

if (!isTitleChanged) {
setTitleBeingUpdated(false);

return;
}

const changeItem = todos.find(todo => todo.id === id);
const toUpdate = { title: normalizeNewTitle };

if (changeItem) {
setProcessedIs(existing => [...existing, id]);
updateTodo(id, toUpdate)
.then(() => {
setTodos(existing =>
existing.map(el =>
el.id === id ? { ...el, title: toUpdate.title } : el,
),
);
setTitleBeingUpdated(false);
})
.catch(() => setErrorMessage('Unable to update a todo'))
.finally(() => {
setProcessedIs([]);
});
}
};

const handleKeyUp = (
event: React.KeyboardEvent<HTMLInputElement>,
setTitleBeingUpdated: React.Dispatch<React.SetStateAction<boolean>>,
) => {
if (event.key === 'Escape') {
setTitleBeingUpdated(false);
}
};

if (!USER_ID) {
return <UserWarning />;
}
Expand All @@ -253,32 +40,32 @@ export const App: React.FC = () => {

<div className="todoapp__content">
<Header
allTodosCompleted={areAllTodosCompleted}
noTodos={noTodos}
onSubmit={handleSubmit}
loading={loading}
title={title}
onTitleChange={value => setTitle(value)}
onTotalStatusUpdate={handleTotalStatusUpdate}
todoList={todos}
onError={setErrorMessage}
updateTodoList={setTodos}
updateTempTodo={setTempTodo}
updateProcessedIds={setprocessedIds}
/>

<TodoList
filteredTodos={filteredTodos}
onDelete={handleDeleteOneTodo}
onStatusUpdate={handleStatusUpdate}
onUpdatedTitleSubmit={handleTitleUpdate}
onKeyUp={handleKeyUp}
processedIs={processedIs}
updateTodolist={setTodos}
onError={setErrorMessage}
todoList={todos}
processedIds={processedIds}
updateProcessedIds={setprocessedIds}
/>

{tempTodo && <TempTodo todo={tempTodo} />}

{!noTodos && (
<Footer
todos={todos}
todoList={todos}
updateTodolist={setTodos}
status={status}
onStatusChange={(value: TodoStatus) => setStatus(value)}
clearCompletedTodos={handleDeleteCompletedTodos}
updateProcessedIds={setprocessedIds}
onError={setErrorMessage}
/>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Error/Erros.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from 'classnames';

type Props = {
errorMessage: string;
hideError: (arg: string) => void;
hideError: (errorMessage: string) => void;
};

export const Error: React.FC<Props> = ({ errorMessage, hideError }) => {
Expand Down
55 changes: 42 additions & 13 deletions src/components/Footer/Fotter.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,58 @@
import React from 'react';
import React, { useCallback } from 'react';
import { useMemo } from 'react';
import classNames from 'classnames';

import { deleteTodo } from '../../api/todos';
import { capitalizeFirstLetter, filterOptions } from '../Helpers/Helpers';

import { Todo } from '../../types/Todo';
import { TodoStatus } from '../../types/Status';

type Props = {
todos: Todo[];
todoList: Todo[];
updateTodolist: React.Dispatch<React.SetStateAction<Todo[]>>;
status: TodoStatus;
onStatusChange: (arg: TodoStatus) => void;
clearCompletedTodos: () => void;
onStatusChange: (status: TodoStatus) => void;
updateProcessedIds: React.Dispatch<React.SetStateAction<number[]>>;
onError: React.Dispatch<React.SetStateAction<string>>;
};

export const Footer: React.FC<Props> = ({
todos,
todoList,
updateTodolist,
status,
onStatusChange,
clearCompletedTodos,
updateProcessedIds,
onError,
}) => {
const activeTodos = todos.filter(todo => !todo.completed);
const isAnyCompleted = todos.some(todo => todo.completed);
const filterOptions = Object.values(TodoStatus);
const activeTodos = useMemo(
() => todoList.filter(todo => !todo.completed),
[todoList],
);
const isAnyCompleted = useMemo(
() => todoList.some(todo => todo.completed),
[todoList],
);

const capitalizeFirstLetter = (value: TodoStatus) => {
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
};
const handleDeleteCompletedTodos = useCallback(() => {
todoList.forEach(todo => {
if (todo.completed) {
updateProcessedIds(existing => [...existing, todo.id]);
deleteTodo(todo.id)
.then(() =>
updateTodolist(existing =>
existing.filter(current => current.id !== todo.id),
),
)
.catch(() => onError('Unable to delete a todo'))
.finally(() => {
updateProcessedIds(existing =>
existing.filter(id => id !== todo.id),
);
});
}
});
}, [todoList]);

Check warning on line 55 in src/components/Footer/Fotter.tsx

View workflow job for this annotation

GitHub Actions / run_linter (20.x)

React Hook useCallback has missing dependencies: 'onError', 'updateProcessedIds', and 'updateTodolist'. Either include them or remove the dependency array. If 'updateProcessedIds' changes too often, find the parent component that defines it and wrap that definition in useCallback

return (
<footer className="todoapp__footer" data-cy="Footer">
Expand Down Expand Up @@ -58,7 +87,7 @@ export const Footer: React.FC<Props> = ({
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
disabled={!isAnyCompleted}
onClick={clearCompletedTodos}
onClick={handleDeleteCompletedTodos}
>
Clear completed
</button>
Expand Down
Loading

0 comments on commit 294cfc6

Please sign in to comment.