-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Create todo app #1557
base: master
Are you sure you want to change the base?
Create todo app #1557
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move this function to separate component or use useMemo for it |
||||||
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); | ||||||
} | ||||||
}; | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
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} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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> | ||||||
); | ||||||
}; |
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); | ||
}; |
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, clearError]); | ||
|
||
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> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move it outside the function