Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

almost Fully functional Todo App🎀 #803

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,34 @@ and implement the ability to toggle and rename todos.

Toggle the `completed` status on `TodoStatus` change:

- covered the todo with a loader overlay while waiting for API response;
- the status should be changed on success;
- show the `Unable to update a todo` notification in case of API error.
+ covered the todo with a loader overlay while waiting for API response;
+ the status should be changed on success;
+ show the `Unable to update a todo` notification in case of API error.

Add the ability to toggle the completed status of all the todos with the `toggleAll` checkbox:

- `toggleAll` button should have `active` class only if all the todos are completed;
- `toggleAll` click changes its status to the opposite one, and sets this new status to all the todos;
- it should work the same as several individual updates of the todos which statuses were actually changed;
- do send requests for the todos that were not changed;
+ `toggleAll` button should have `active` class only if all the todos are completed;
+ `toggleAll` click changes its status to the opposite one, and sets this new status to all the todos;
🤨 it should work the same as several individual updates of the todos which statuses were actually changed;
🤨 do send requests for the todos that were not changed;

## Renaming a todo

Implement the ability to edit a todo title on double click:

- show the edit form instead of the title and remove button;
- saves changes on the form submit (just press `Enter`);
- save changes when the field loses focus (`onBlur`);
- if the new title is the same as the old one just cancel editing;
- cancel editing on `Esс` key `keyup` event;
- if the new title is empty delete the todo the same way the `x` button does it;
- if the title was changed show the loader while waiting for the API response;
- update the todo title on success;
- show `Unable to update a todo` in case of API error;
- or the deletion error message if we tried to delete the todo.
+ show the edit form instead of the title and remove button;
+ saves changes on the form submit (just press `Enter`);
+ save changes when the field loses focus (`onBlur`);
+ if the new title is the same as the old one just cancel editing;
+ cancel editing on `Esс` key `keyup` event;
+ if the new title is empty delete the todo the same way the `x` button does it;
+ if the title was changed show the loader while waiting for the API response;
+ update the todo title on success;
+ show `Unable to update a todo` in case of API error;
+ or the deletion error message if we tried to delete the todo.

## Instructions

- 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://polinavafik.github.io/react_todo-app-with-api/) and add it to the PR description.
107 changes: 90 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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, {
useEffect,
useState,
useMemo,
useRef,
} from 'react';
import classNames from 'classnames';
import './styles/App.scss';
import { TodoFilter } from './types/TodoFilter';
import { TodoList } from './components/TodoList';
import { TodoHeader } from './components/TodoHeader';
import { TodoFooter } from './components/TodoFooter';
import { getFilteredTodos } from './utils/getFilteredTodos';
import { CurrentError } from './types/CurrentError';
import { useTodo } from './Context/TodoContext';
import { USER_ID } from './utils/constants';
import * as todoService from './api/todos';

export const App: React.FC = () => {
if (!USER_ID) {
return <UserWarning />;
}
const [todoFilter, setTodoFilter] = useState<TodoFilter>(TodoFilter.All);

const {
todos,
setTodos,
error,
setError,
} = useTodo();

useEffect(() => {
todoService.getTodos(USER_ID)
.then(setTodos)
.catch(() => {
setError(CurrentError.LoadingError);
});
}, []);

const timerId = useRef<number>(0);

useEffect(() => {
if (timerId.current) {
window.clearTimeout(timerId.current);
}

timerId.current = window.setTimeout(() => {
setError(CurrentError.Default);
}, 3000);
}, [error]);

const filteredTodos = useMemo(() => {
return getFilteredTodos(todos, todoFilter);
}, [todos, todoFilter]);

const handleSetTodoFilter = (filter: TodoFilter) => (
setTodoFilter(filter)
);

return (
<section className="section container">
<p className="title is-4">
Copy all you need from the prev task:
<br />
<a href="https://github.com/mate-academy/react_todo-app-add-and-delete#react-todo-app-add-and-delete">React Todo App - Add and Delete</a>
</p>

<p className="subtitle">Styles are already copied</p>
</section>
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>

<div className="todoapp__content">
<TodoHeader />

<TodoList
todos={filteredTodos}
/>

{!!todos.length && (
<TodoFooter
filter={todoFilter}
setFilter={handleSetTodoFilter}
/>
)}
</div>

<div
data-cy="ErrorNotification"
className={classNames(
'notification',
'is-danger',
'is-light',
'has-text-weight-normal',
{ hidden: !error },
)}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => setError(CurrentError.Default)}
/>
{error}
</div>
Comment on lines +77 to +94

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a component ErrorNotification and move the logic for errors to it too

</div>
);
};
207 changes: 207 additions & 0 deletions src/Context/TodoContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import React, {
createContext,
useContext,
useMemo,
useState,
} from 'react';
import { CurrentError } from '../types/CurrentError';
import * as todoService from '../api/todos';
import { Todo } from '../types/Todo';
import { getCompletedTodos } from '../utils/getCompletedTodos';
import { getActiveTodos } from '../utils/getActiveTodos';

type Props = {
children: React.ReactNode
};

interface TodoContextInterface {
todos: Todo[],
setTodos: React.Dispatch<React.SetStateAction<Todo[]>>,
tempTodo: Todo | null;
setTempTodo: React.Dispatch<React.SetStateAction<Todo | null>>;
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
error: CurrentError,
setError: (error: CurrentError) => void;
handleToggleChange: (todo: Todo) => void;
handleTodoDelete: (id: number) => void;
handleTodoAdd: (newTodo: Omit<Todo, 'id'>) => Promise<void>
handleTodoRename: (todo: Todo, newTodoTitle: string) => Promise<void> | void,
completedTodos: Todo[];
activeTodos: Todo[];
handleClearCompleted: () => void;
processingTodoIds: number[];
setProcessingTodoIds: (todoIdsToDelete: number[]) => void;
}

const initalContext: TodoContextInterface = {
todos: [],
setTodos: () => {},
tempTodo: null,
setTempTodo: () => {},
isLoading: false,
setIsLoading: () => {},
error: CurrentError.Default,
setError: () => {},
handleToggleChange: () => {},
handleTodoDelete: () => {},
handleTodoAdd: async () => {},
handleTodoRename: async () => {},
completedTodos: [],
activeTodos: [],
handleClearCompleted: () => {},
processingTodoIds: [],
setProcessingTodoIds: () => {},
};

export const TodoContext = createContext(initalContext);

export const TodoProvider: React.FC<Props> = ({ children }) => {
const [todos, setTodos] = useState<Todo[]>([]);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [error, setError] = useState(CurrentError.Default);
const [isLoading, setIsLoading] = useState(false);
const [processingTodoIds, setProcessingTodoIds] = useState<number[]>([]);

const completedTodos = getCompletedTodos(todos);
const activeTodos = getActiveTodos(todos);

const handleTodoDelete = (todoId: number) => {
setIsLoading(true);
setProcessingTodoIds(prevState => [...prevState, todoId]);

todoService.deleteTodo(todoId)
.then(() => {
setTodos(prevTodos => {
return prevTodos.filter(todo => todo.id !== todoId);
});
})
.catch(() => {
setError(CurrentError.DeleteError);
})
.finally(() => {
setProcessingTodoIds(
(prevState) => prevState.filter(id => id !== todoId),
);
setIsLoading(false);
});
};

const handleTodoAdd = (newTodo: Omit<Todo, 'id'>) => {
setIsLoading(true);

return todoService.addTodo(newTodo)
.then(createdTodo => {
setTodos((prevTodos) => [...prevTodos, createdTodo]);
})
.catch(() => {
setError(CurrentError.AddError);
throw new Error();
})
.finally(() => {
setIsLoading(false);
setTempTodo(null);
});
};

const handleTodoRename = (todo: Todo, newTodoTitle: string) => {
if (todo.title === newTodoTitle) {
return;
}

if (!newTodoTitle.trim()) {
setError(CurrentError.EmptyTitleError);

return;
}

setIsLoading(true);
setProcessingTodoIds(prevState => [...prevState, todo.id]);

// eslint-disable-next-line consistent-return

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't disable eslint, I think in this case you actually don't need to return

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I don't have return, then when you edit todo, after edit on load it shows the previous version of the title and then shows the new one (after load)

but with return, everything works as it should...

return todoService
.updateTodo({
...todo,
title: newTodoTitle,
})
.then(updatedTodo => {
setTodos(prevState => prevState.map(currTodo => {
return currTodo.id !== updatedTodo.id
? currTodo
: updatedTodo;
}));
})
.catch(() => {
setError(CurrentError.UpdateError);
throw new Error(CurrentError.UpdateError);
})
.finally(() => {
setProcessingTodoIds(
(prevState) => prevState.filter(id => id !== todo.id),
);
setIsLoading(false);
});
};

const handleToggleChange = (todo: Todo) => {
setIsLoading(true);
setProcessingTodoIds(prevState => [...prevState, todo.id]);

todoService.updateTodo({
...todo,
completed: !todo.completed,
})
.then((updatedTodo) => {
setTodos(prevState => prevState
.map(currTodo => (
currTodo.id === updatedTodo.id
? updatedTodo
: currTodo
)));
})
.catch(() => {
setError(CurrentError.UpdateError);
throw new Error(CurrentError.UpdateError);
})
.finally(() => {
setProcessingTodoIds(
(prevState) => prevState.filter(id => id !== todo.id),
);
setIsLoading(false);
});
};

const handleClearCompleted = () => {
completedTodos.forEach(({ id }) => handleTodoDelete(id));
};

const value = useMemo(() => ({
todos,
setTodos,
tempTodo,
setTempTodo,
isLoading,
setIsLoading,
error,
setError,
handleToggleChange,
handleTodoDelete,
handleTodoAdd,
handleTodoRename,
completedTodos,
activeTodos,
handleClearCompleted,
processingTodoIds,
setProcessingTodoIds,
}), [todos, error, isLoading, tempTodo]);

return (
<TodoContext.Provider
value={value}
>
{children}
</TodoContext.Provider>
);
};

export const useTodo = () => useContext(TodoContext);
18 changes: 18 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const getTodos = (userId: number) => {
return client.get<Todo[]>(`/todos?userId=${userId}`);
};

export const deleteTodo = (todoId: number) => {
return client.delete(`/todos/${todoId}`);
};

export const addTodo = (todo: Omit<Todo, 'id'>) => {
return client.post<Todo>('/todos', todo);
};

export const updateTodo = (todoToUpdate: Todo): Promise<Todo> => {
return client.patch(`/todos/${todoToUpdate.id}`, todoToUpdate);
};
Loading