-
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
almost Fully functional Todo App🎀 #803
base: master
Are you sure you want to change the base?
Changes from 11 commits
488b346
0e6e58e
26e82df
45eb0bd
a49405f
7e87b24
e56d05b
fda98ee
26d433e
42f4762
df7495a
551a730
27bd278
e36cddb
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,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> | ||
</div> | ||
); | ||
}; |
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 | ||
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. Don't disable eslint, I think in this case you actually don't need to return 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. 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); |
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); | ||
}; |
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.
Create a component
ErrorNotification
and move the logic for errors to it too