-
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
Develop #1535
base: master
Are you sure you want to change the base?
Develop #1535
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,26 +1,178 @@ | ||||||
/* eslint-disable max-len */ | ||||||
/* eslint-disable jsx-a11y/control-has-associated-label */ | ||||||
import React from 'react'; | ||||||
import { UserWarning } from './UserWarning'; | ||||||
import React, { useState, useEffect } from 'react'; | ||||||
import { addTodo, deleteTodo, getTodos, updateTodo } from './api/todos'; | ||||||
// import { todos } from './api/todos'; | ||||||
|
||||||
const USER_ID = 0; | ||||||
import { Header } from './components/Header'; | ||||||
import { TodoList } from './components/TodoList'; | ||||||
import { Footer } from './components/Footer'; | ||||||
import { Errors } from './components/Errors'; | ||||||
|
||||||
import { loadingObject } from './utils/loadingObject'; | ||||||
import { filteredTodos } from './utils/filteredTodos'; | ||||||
|
||||||
import { Todo } from './types/Todo'; | ||||||
import { ErrorMessage } from './types/Errors'; | ||||||
import { Loading } from './types/Loading'; | ||||||
import { Filters } from './types/Filters'; | ||||||
|
||||||
export const App: React.FC = () => { | ||||||
if (!USER_ID) { | ||||||
return <UserWarning />; | ||||||
} | ||||||
const [todos, setTodos] = useState<Todo[]>([]); | ||||||
const [errorMessage, setErrorMessage] = useState<string>(''); | ||||||
const [tempTodo, setTempTodo] = useState<Todo | null>(null); | ||||||
const [filter, setFilter] = useState<Filters>(Filters.All); | ||||||
const [loadingId, setLoadingId] = useState<Loading>({}); | ||||||
|
||||||
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. краще зберігати саму id, без об'єкта і судячи з назви там має бути id |
||||||
useEffect(() => { | ||||||
const timeoutId = setTimeout(() => setErrorMessage(''), 3000); | ||||||
|
||||||
getTodos() | ||||||
.then(setTodos) | ||||||
.catch(() => { | ||||||
setErrorMessage(ErrorMessage.UnableToLoad); | ||||||
clearTimeout(timeoutId); | ||||||
}); | ||||||
|
||||||
return () => clearTimeout(timeoutId); | ||||||
}, []); | ||||||
|
||||||
const handleAdd = (newTodo: Todo): Promise<Todo | void> => { | ||||||
setTempTodo(newTodo); | ||||||
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. why u need Todo in this place?
Suggested change
|
||||||
|
||||||
return addTodo(newTodo).then(newTodoRes => { | ||||||
setTodos(currentTodos => [...currentTodos, newTodoRes]); | ||||||
}); | ||||||
}; | ||||||
|
||||||
const updateCompleted = ( | ||||||
updatedTodo: Todo, | ||||||
key: keyof Todo, | ||||||
value: boolean | string, | ||||||
) => { | ||||||
return updateTodo({ ...updatedTodo, [key]: value }) | ||||||
.then((updatedTodoFromServer: Todo) => { | ||||||
setTodos(currentTodos => { | ||||||
return currentTodos.map(todo => | ||||||
todo.id === updatedTodo.id ? updatedTodoFromServer : todo, | ||||||
); | ||||||
}); | ||||||
|
||||||
return false; | ||||||
}) | ||||||
.catch(() => { | ||||||
setErrorMessage(ErrorMessage.UnableToUpdate); | ||||||
|
||||||
return true; | ||||||
}); | ||||||
}; | ||||||
|
||||||
const handleToggleAll = () => { | ||||||
const activeTodos = todos.filter(todo => !todo.completed); | ||||||
const activeTodosIds = loadingObject(activeTodos); | ||||||
|
||||||
if (activeTodos.length) { | ||||||
setLoadingId(activeTodosIds); | ||||||
|
||||||
Promise.all( | ||||||
activeTodos.map(todo => updateTodo({ ...todo, completed: true })), | ||||||
) | ||||||
.then(() => | ||||||
setTodos(currentTodos => { | ||||||
return currentTodos.map(todo => { | ||||||
if (Object.hasOwn(activeTodosIds, todo.id)) { | ||||||
return { ...todo, completed: true }; | ||||||
} else { | ||||||
return todo; | ||||||
} | ||||||
}); | ||||||
}), | ||||||
) | ||||||
.catch(() => setErrorMessage(ErrorMessage.UnableToUpdate)) | ||||||
.finally(() => setLoadingId({})); | ||||||
|
||||||
return; | ||||||
} | ||||||
|
||||||
setLoadingId(loadingObject(todos)); | ||||||
Promise.all(todos.map(todo => updateTodo({ ...todo, completed: false }))) | ||||||
.then(() => | ||||||
setTodos(prevTodos => { | ||||||
return prevTodos.map(todo => ({ ...todo, completed: false })); | ||||||
}), | ||||||
) | ||||||
.catch(() => setErrorMessage(ErrorMessage.UnableToUpdate)) | ||||||
.finally(() => setLoadingId({})); | ||||||
}; | ||||||
|
||||||
const handleDeleteCompleted = () => { | ||||||
const completedTodos = todos.filter(todo => todo.completed); | ||||||
|
||||||
setLoadingId(loadingObject(completedTodos)); | ||||||
|
||||||
Promise.allSettled( | ||||||
completedTodos.map(todo => deleteTodo(todo.id).then(() => todo)), | ||||||
) | ||||||
.then(values => { | ||||||
values.map(val => { | ||||||
if (val.status === 'rejected') { | ||||||
setErrorMessage(ErrorMessage.UnableToDelete); | ||||||
} else { | ||||||
setTodos(currentTodos => { | ||||||
const todoId = val.value as Todo; | ||||||
|
||||||
return currentTodos.filter(todo => todo.id !== todoId.id); | ||||||
}); | ||||||
} | ||||||
}); | ||||||
}) | ||||||
.finally(() => setLoadingId({})); | ||||||
}; | ||||||
|
||||||
const handleDelete = (todoId: number): Promise<void> => { | ||||||
return deleteTodo(todoId) | ||||||
.then(() => { | ||||||
setTodos(currentTodos => | ||||||
currentTodos.filter(todo => todo.id !== todoId), | ||||||
); | ||||||
}) | ||||||
.catch(() => { | ||||||
setErrorMessage(ErrorMessage.UnableToDelete); | ||||||
}) | ||||||
.finally(() => setTempTodo(null)); | ||||||
}; | ||||||
|
||||||
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 | ||||||
tempTodo={tempTodo} | ||||||
todos={todos} | ||||||
onToggleAll={handleToggleAll} | ||||||
onChangeTempTodo={setTempTodo} | ||||||
setErrorMessage={setErrorMessage} | ||||||
onSubmit={handleAdd} | ||||||
/> | ||||||
|
||||||
<TodoList | ||||||
todos={filteredTodos(todos, filter)} | ||||||
tempTodo={tempTodo} | ||||||
loadingId={loadingId} | ||||||
onEdit={updateCompleted} | ||||||
onDelete={handleDelete} | ||||||
/> | ||||||
|
||||||
{todos.length > 0 && ( | ||||||
<Footer | ||||||
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. краще вказати перевірку !!todos.length 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. чому саме той варіант кращий? 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. @OkMoroz |
||||||
todos={todos} | ||||||
selectedFilter={filter} | ||||||
onFilteredStatus={setFilter} | ||||||
onDeleteCompleted={handleDeleteCompleted} | ||||||
/> | ||||||
)} | ||||||
</div> | ||||||
|
||||||
<Errors message={errorMessage} clearError={() => setErrorMessage('')} /> | ||||||
</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 = 2132; | ||
|
||
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'; | ||
|
||
type Props = { | ||
message: string; | ||
clearError: () => void; | ||
}; | ||
|
||
export const Errors: React.FC<Props> = props => { | ||
const { message, clearError } = props; | ||
|
||
useEffect(() => { | ||
const timeOut = setTimeout(clearError, 3000); | ||
|
||
return () => { | ||
clearTimeout(timeOut); | ||
}; | ||
}, [message]); | ||
|
||
return ( | ||
<div | ||
data-cy="ErrorNotification" | ||
className={cn( | ||
'notification', | ||
'is-danger', | ||
'is-light has-text-weight-normal', | ||
{ hidden: !message }, | ||
)} | ||
> | ||
<button | ||
data-cy="HideErrorButton" | ||
type="button" | ||
className="delete" | ||
onClick={clearError} | ||
/> | ||
{message} | ||
</div> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,59 @@ | ||||||
import React from 'react'; | ||||||
import cn from 'classnames'; | ||||||
import { Filters } from '../types/Filters'; | ||||||
import { Todo } from '../types/Todo'; | ||||||
|
||||||
type Props = { | ||||||
todos: Todo[]; | ||||||
selectedFilter: Filters; | ||||||
onFilteredStatus: (filter: Filters) => void; | ||||||
onDeleteCompleted: () => void; | ||||||
}; | ||||||
|
||||||
export const Footer: React.FC<Props> = props => { | ||||||
const { todos, selectedFilter, onFilteredStatus, onDeleteCompleted } = props; | ||||||
|
||||||
const filtersValue = Object.values(Filters); | ||||||
const activeTodosCount = todos.filter(todo => !todo.completed).length; | ||||||
const isCompleted = todos.some(todo => todo.completed); | ||||||
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. useMemo? |
||||||
let isDeleteCompleted = 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. loading state? |
||||||
|
||||||
const handleDeleteCompleted = () => { | ||||||
isDeleteCompleted = true; | ||||||
onDeleteCompleted(); | ||||||
}; | ||||||
|
||||||
return ( | ||||||
<footer className="todoapp__footer" data-cy="Footer"> | ||||||
<span className="todo-count" data-cy="TodosCounter"> | ||||||
{activeTodosCount} items left | ||||||
</span> | ||||||
<nav className="filter" data-cy="Filter"> | ||||||
{filtersValue.map(filter => ( | ||||||
<a | ||||||
key={filter} | ||||||
href={`#/${filter !== Filters.All ? filter.toLowerCase() : ''}`} | ||||||
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. will it be better?
Suggested change
|
||||||
className={cn('filter__link', { | ||||||
selected: filter === selectedFilter, | ||||||
})} | ||||||
data-cy={`FilterLink${filter}`} | ||||||
onClick={() => onFilteredStatus(filter)} | ||||||
> | ||||||
{filter} | ||||||
</a> | ||||||
))} | ||||||
</nav> | ||||||
|
||||||
<button | ||||||
type="button" | ||||||
className="todoapp__clear-completed" | ||||||
data-cy="ClearCompletedButton" | ||||||
disabled={isDeleteCompleted || !isCompleted} | ||||||
style={{ visibility: !isCompleted ? 'hidden' : 'visible' }} | ||||||
onClick={handleDeleteCompleted} | ||||||
> | ||||||
Clear completed | ||||||
</button> | ||||||
</footer> | ||||||
); | ||||||
}; |
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.
в тебе є enum по помилкам, типізуй useState типом Error
щоб не робити тут додаткової перевірки на пустий рядок, то можеж в enum прописати, наприклад Nothing: '', aбо Empty: ''
Бо в даному випадку в цей state можна покласти довільний рядок
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.
Дякую, візьму до уваги