Skip to content

Commit

Permalink
create todo app
Browse files Browse the repository at this point in the history
  • Loading branch information
AlyonaV22 committed Dec 21, 2024
1 parent 62dbb71 commit 7e2b32c
Show file tree
Hide file tree
Showing 14 changed files with 717 additions and 21 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and implement the ability to toggle and rename todos.
## Toggling a todo status

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

- Install Prettier Extention and use this [VSCode settings](https://mate-academy.github.io/fe-program/tools/vscode/settings.json) to enable format on save.
- covered the todo with a loader overlay while waiting for API response;
- the status should be changed on success;
Expand Down Expand Up @@ -38,6 +39,7 @@ Implement the ability to edit a todo title on double click:
- or the deletion error message if we tried to delete the todo.

## If you want to enable tests

- open `cypress/integration/page.spec.js`
- replace `describe.skip` with `describe` for the root `describe`

Expand All @@ -47,4 +49,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://AlyonaV22.github.io/react_todo-app-with-api/) and add it to the PR description.
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
"@mate-academy/scripts": "^1.8.5",
"@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
242 changes: 227 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,238 @@
/* eslint-disable max-len */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useState, useEffect } from 'react';
import { UserWarning } from './UserWarning';
import {
addTodo,
deleteTodo,
getTodos,
updateTodo,
USER_ID,
} from './api/todos';

const USER_ID = 0;
import { Header } from './commponents/Header';
import { Footer } from './commponents/Footer';
import { Errors } from './commponents/Errors';
import { TodoCard } from './commponents/TodoCard';

import { Todo } from './types/Todo';
import { Status } from './types/Status';
import { ErrorType } from './types/Errors';

export const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [filterType, setFilterType] = useState<Status>(Status.All);
const [errorType, setErrorType] = useState<ErrorType>(ErrorType.EmptyTitle);
const [tempTodo, setTempTodo] = useState<Todo | null>(null);
const [todoLoading, setTodoLoading] = useState<Loading>({});

useEffect(() => {
const timeoutId = setTimeout(
() => setErrorType(ErrorType.EmptyTitle),
3000,
);

getTodos()
.then(setTodos)
.catch(() => {
setErrorType(ErrorType.UnableToLoad);
clearTimeout(timeoutId);
});

return () => clearTimeout(timeoutId);
}, []);

interface Loading {
[key: number]: number;
}

const loadingTodo = (todoList: Todo[]): Loading => {
return todoList.reduce((acc: Loading, todo: Todo): Loading => {
return {
...acc,
[todo.id]: todo.id,
};
}, {} as Loading);
};

const todoFilter = todos.filter(todo => {
switch (filterType) {
case Status.Active:
return !todo.completed;
case Status.Completed:
return todo.completed;
default:
return true;
}
});

const addNewTodo = (todoToAdd: Todo): void => {
setTempTodo(todoToAdd);

addTodo(todoToAdd)
.then(todoNew => {
setTodos(currentTodos => [...currentTodos, todoNew]);
})
.catch(() => {
setErrorType(ErrorType.UnableToAdd);
})
.finally(() => {
setTempTodo(null);
});
};

const deleteTodoItem = (todoId: number): void => {
deleteTodo(todoId)
.then(() => {
setTodos(currentTodos =>
currentTodos.filter(todo => todo.id !== todoId),
);
})
.catch(() => {
setErrorType(ErrorType.UnableToDelete);
})
.finally(() => {
setTempTodo(null);
});
};

const updateTodoItems = (
updateTodoItem: Todo,
key: keyof Todo,
value: boolean | string,
): Promise<boolean> => {
return updateTodo({ ...updateTodoItem, [key]: value })
.then(todoUpdated => {
setTodos(currentTodos =>
currentTodos.map(todo =>
todo.id === updateTodoItem.id ? todoUpdated : todo,
),
);

return false;
})
.catch(() => {
setErrorType(ErrorType.UnableToUpdate);

return true;
});
};

const loadedDeleteTodo = (): void => {
const completedTodos = todos.filter(todo => todo.completed);

setTodoLoading(loadingTodo(completedTodos));

Promise.allSettled(
completedTodos.map(todo => deleteTodo(todo.id).then(() => todo)),
)
.then(values => {
values.forEach(val => {
if (val.status === 'rejected') {
setErrorType(ErrorType.UnableToDelete);
} else {
setTodos(currentTodos => {
const todoId = val.value as Todo;

return currentTodos.filter(todo => todo.id !== todoId.id);
});
}
});
})
.finally(() => setTodoLoading({}));
};

const handleAllCompleted = (): void => {
const completedAllTodos = (
targetTodos: Todo[],
completed: boolean,
): Promise<void> => {
return Promise.all(
targetTodos.map(todo => updateTodo({ ...todo, completed })),
)
.then(() => {
setTodos(currentTodos =>
currentTodos.map(todo =>
targetTodos.some(t => t.id === todo.id)
? { ...todo, completed }
: todo,
),
);
})
.catch(() => {
setErrorType(ErrorType.UnableToUpdate);
})
.finally(() => {
setTodoLoading({});
});
};

const activeTodos = todos.filter(todo => !todo.completed);

if (activeTodos.length) {
setTodoLoading(loadingTodo(activeTodos));
completedAllTodos(activeTodos, true);
} else {
setTodoLoading(loadingTodo(todos));
completedAllTodos(todos, false);
}
};

const lengthOfTodo = todos.length;

if (!USER_ID) {
return <UserWarning />;
}

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">
<Header
todos={todos}
setErrorType={setErrorType}
onChangeTodoTask={setTempTodo}
tempTodo={tempTodo}
addNewTodo={addNewTodo}
handleAllCompleted={handleAllCompleted}
lengthOfTodo={lengthOfTodo}
/>

<section className="todoapp__main" data-cy="TodoList">
{todoFilter.map(todo => (
<TodoCard
key={todo.id}
todo={todo}
deleteTodoItem={deleteTodoItem}
updateTodoItems={updateTodoItems}
todoLoading={todoLoading}
/>
))}

{tempTodo && (
<TodoCard
todo={tempTodo}
updateTodoItems={updateTodoItems}
deleteTodoItem={deleteTodoItem}
todoLoading={todoLoading}
/>
)}
</section>

{todos.length > 0 && (
<Footer
filterType={filterType}
onFiltered={setFilterType}
todos={todos}
loadedDeleteTodo={loadedDeleteTodo}
setTodoLoading={setTodoLoading}
/>
)}
</div>

<Errors
errorType={errorType}
clearError={() => setErrorType(ErrorType.EmptyTitle)}
/>
</div>
);
};
20 changes: 20 additions & 0 deletions src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Todo } from '../types/Todo';
import { client } from '../utils/fetchClient';

export const USER_ID = 2134;

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

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

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

export const updateTodo = ({ id, ...todo }: Todo) => {
return client.patch<Todo>(`/todos/${id}`, todo);
};
39 changes: 39 additions & 0 deletions src/commponents/Errors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useEffect } from 'react';
import cn from 'classnames';

interface Props {
errorType: string;
clearError: () => void;
}

export const Errors: React.FC<Props> = props => {
const { errorType, clearError } = props;

useEffect(() => {
const timeOut = setTimeout(clearError, 3000);

return () => {
clearTimeout(timeOut);
};
}, [errorType]);

return (
<div
data-cy="ErrorNotification"
className={cn(
'notification',
'is-danger',
'is-light has-text-weight-normal',
{ hidden: !errorType },
)}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={clearError}
/>
{errorType}
</div>
);
};
Loading

0 comments on commit 7e2b32c

Please sign in to comment.