-
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 using API #767
base: master
Are you sure you want to change the base?
Changes from 9 commits
74d64c2
cfb8bcb
e8c4b7c
455b1f5
7d0ba51
8865bc7
79a37d1
02c204e
c022282
7d880c9
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,136 @@ | ||||||||||||||||||
/* 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 classNames from 'classnames'; | ||||||||||||||||||
import React, { useEffect, useMemo, useState } from 'react'; | ||||||||||||||||||
import { Todo } from './types/Todo'; | ||||||||||||||||||
import { Filters } from './utils/Filters'; | ||||||||||||||||||
import { NewTodo } from './components/NewTodo/NewTodo'; | ||||||||||||||||||
import { TodoList } from './components/TodoList/TodoList'; | ||||||||||||||||||
import { TodoFilter } from './components/TodoFilter/TodoFilter'; | ||||||||||||||||||
import { | ||||||||||||||||||
deleteTodo, | ||||||||||||||||||
getTodos, | ||||||||||||||||||
} from './api/todos'; | ||||||||||||||||||
import { countTodos } from './utils/countTodos'; | ||||||||||||||||||
import { | ||||||||||||||||||
useTodos, | ||||||||||||||||||
} from './components/Contexts/TodosContext'; | ||||||||||||||||||
import { USER_ID } from './utils/userToken'; | ||||||||||||||||||
import { | ||||||||||||||||||
useLoadingTodos, | ||||||||||||||||||
} from './components/Contexts/LoadingTodosContext'; | ||||||||||||||||||
import { | ||||||||||||||||||
useErrorMessage, | ||||||||||||||||||
} from './components/Contexts/ErrorMessageContext'; | ||||||||||||||||||
|
||||||||||||||||||
export const App: React.FC = () => { | ||||||||||||||||||
if (!USER_ID) { | ||||||||||||||||||
return <UserWarning />; | ||||||||||||||||||
} | ||||||||||||||||||
const { todos, setTodos } = useTodos(); | ||||||||||||||||||
|
||||||||||||||||||
const [tempTodo, setTempTodo] = useState<Todo | null>(null); | ||||||||||||||||||
const { setLoadingTodos } = useLoadingTodos(); | ||||||||||||||||||
const { | ||||||||||||||||||
errorMessage, | ||||||||||||||||||
isErrorHidden, | ||||||||||||||||||
setIsErrorHidden, | ||||||||||||||||||
handleShowError, | ||||||||||||||||||
} = useErrorMessage(); | ||||||||||||||||||
|
||||||||||||||||||
const [filterParam, setFilterParam] = useState(Filters.All); | ||||||||||||||||||
|
||||||||||||||||||
const clearCompletedTodos = async () => { | ||||||||||||||||||
const completedTodos = countTodos(todos, true).map(({ id }) => ( | ||||||||||||||||||
deleteTodo(id))); | ||||||||||||||||||
|
||||||||||||||||||
countTodos(todos, true).forEach(({ id }) => { | ||||||||||||||||||
setLoadingTodos(prev => [...prev, id]); | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
try { | ||||||||||||||||||
await Promise.all(completedTodos); | ||||||||||||||||||
setTodos(prev => prev.filter(({ completed }) => !completed)); | ||||||||||||||||||
} catch { | ||||||||||||||||||
handleShowError('Unable to delete todo'); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
setLoadingTodos([]); | ||||||||||||||||||
}; | ||||||||||||||||||
|
||||||||||||||||||
useEffect(() => { | ||||||||||||||||||
getTodos(USER_ID) | ||||||||||||||||||
.then(setTodos) | ||||||||||||||||||
.catch(() => { | ||||||||||||||||||
handleShowError('Unable to load todos'); | ||||||||||||||||||
}); | ||||||||||||||||||
}, []); | ||||||||||||||||||
Comment on lines
+37
to
+43
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. It's a good idea to show the loader while fetching something from a server 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.
I still don't see a loader, when user have bad internet connection they don't know is there any todos or no |
||||||||||||||||||
|
||||||||||||||||||
const visibleTodos = useMemo(() => { | ||||||||||||||||||
switch (filterParam) { | ||||||||||||||||||
case Filters.Active: | ||||||||||||||||||
return todos.filter(({ completed }) => !completed); | ||||||||||||||||||
|
||||||||||||||||||
case Filters.Completed: | ||||||||||||||||||
return todos.filter(({ completed }) => completed); | ||||||||||||||||||
|
||||||||||||||||||
case Filters.All: | ||||||||||||||||||
return todos; | ||||||||||||||||||
|
||||||||||||||||||
default: | ||||||||||||||||||
return todos; | ||||||||||||||||||
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
|
||||||||||||||||||
} | ||||||||||||||||||
}, [filterParam, todos]); | ||||||||||||||||||
|
||||||||||||||||||
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> | ||||||||||||||||||
<div className="todoapp"> | ||||||||||||||||||
<h1 className="todoapp__title">todos</h1> | ||||||||||||||||||
|
||||||||||||||||||
<div className="todoapp__content"> | ||||||||||||||||||
<NewTodo onWaiting={setTempTodo} /> | ||||||||||||||||||
|
||||||||||||||||||
<section className="todoapp__main"> | ||||||||||||||||||
<TodoList todos={visibleTodos} /> | ||||||||||||||||||
|
||||||||||||||||||
{tempTodo && ( | ||||||||||||||||||
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. Can this tempTodo be moved to a separate component? |
||||||||||||||||||
<div className="todo"> | ||||||||||||||||||
<label className="todo__status-label"> | ||||||||||||||||||
<input type="checkbox" className="todo__status" /> | ||||||||||||||||||
</label> | ||||||||||||||||||
|
||||||||||||||||||
<p className="subtitle">Styles are already copied</p> | ||||||||||||||||||
</section> | ||||||||||||||||||
<span className="todo__title">{tempTodo.title}</span> | ||||||||||||||||||
<button type="button" className="todo__remove">×</button> | ||||||||||||||||||
|
||||||||||||||||||
<div className="modal overlay is-active"> | ||||||||||||||||||
<div className={'modal-background' | ||||||||||||||||||
+ ' has-background-white-ter'} | ||||||||||||||||||
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
Why do you need to concat classes like this? 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. Linter |
||||||||||||||||||
/> | ||||||||||||||||||
<div className="loader" /> | ||||||||||||||||||
</div> | ||||||||||||||||||
</div> | ||||||||||||||||||
)} | ||||||||||||||||||
</section> | ||||||||||||||||||
|
||||||||||||||||||
{!!todos?.length && ( | ||||||||||||||||||
<TodoFilter | ||||||||||||||||||
todos={todos} | ||||||||||||||||||
filterParam={filterParam} | ||||||||||||||||||
onFilterChange={setFilterParam} | ||||||||||||||||||
clearCompleted={clearCompletedTodos} | ||||||||||||||||||
/> | ||||||||||||||||||
)} | ||||||||||||||||||
</div> | ||||||||||||||||||
|
||||||||||||||||||
<div className={classNames( | ||||||||||||||||||
'notification is-danger is-light has-text-weight-normal', | ||||||||||||||||||
{ hidden: isErrorHidden }, | ||||||||||||||||||
)} | ||||||||||||||||||
> | ||||||||||||||||||
<button | ||||||||||||||||||
type="button" | ||||||||||||||||||
className="delete" | ||||||||||||||||||
onClick={() => setIsErrorHidden(true)} | ||||||||||||||||||
aria-label="Delete Button" | ||||||||||||||||||
/> | ||||||||||||||||||
|
||||||||||||||||||
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
|
||||||||||||||||||
{errorMessage} | ||||||||||||||||||
<br /> | ||||||||||||||||||
</div> | ||||||||||||||||||
</div> | ||||||||||||||||||
); | ||||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Todo } from '../types/Todo'; | ||
import { client } from '../utils/fetchClient'; | ||
|
||
interface UpdateCompleted { | ||
completed: boolean, | ||
} | ||
|
||
interface UpdateTitle { | ||
title: string | ||
} | ||
|
||
export const getTodos = (userId: number) => { | ||
return client.get<Todo[]>(`/todos?userId=${userId}`); | ||
}; | ||
|
||
export const addTodo = (userId: number, newTodo: Todo) => { | ||
return client.post<Todo>(`/todos?userId=${userId}`, newTodo); | ||
}; | ||
|
||
export const deleteTodo = (todoId: number) => { | ||
return client.delete(`/todos/${todoId}`); | ||
}; | ||
|
||
export const updateTodo = (todoId: number, | ||
data: UpdateCompleted | UpdateTitle) => { | ||
return client.patch<Todo>(`/todos/${todoId}`, data); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import React, { useContext, useState } from 'react'; | ||
|
||
interface ErrorContextType { | ||
errorMessage: string, | ||
setErrorMessage: React.Dispatch<React.SetStateAction<string>> | ||
isErrorHidden: boolean, | ||
setIsErrorHidden: React.Dispatch<React.SetStateAction<boolean>>, | ||
handleShowError: (error: string) => void, | ||
} | ||
|
||
export const ErrorMessageContext = React.createContext({} as ErrorContextType); | ||
|
||
type Props = { | ||
children: React.ReactNode, | ||
}; | ||
|
||
export const ErrorMessageContextProvider: React.FC<Props> = ({ children }) => { | ||
const [errorMessage, setErrorMessage] = useState(''); | ||
const [isErrorHidden, setIsErrorHidden] = useState(true); | ||
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. You don't need 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. I want my error to disappear smoothly, if I just setErrorMessage('') it will remove text and after hides the block. Which seems incorrect for me. |
||
|
||
const value = { | ||
errorMessage, | ||
setErrorMessage, | ||
isErrorHidden, | ||
setIsErrorHidden, | ||
handleShowError(error: string) { | ||
setErrorMessage(error); | ||
|
||
setTimeout(() => { | ||
setErrorMessage(''); | ||
}, 3000); | ||
}, | ||
}; | ||
|
||
return ( | ||
<ErrorMessageContext.Provider value={value}> | ||
{children} | ||
</ErrorMessageContext.Provider> | ||
); | ||
}; | ||
|
||
export const useErrorMessage = () => useContext(ErrorMessageContext); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React, { useContext, useState } from 'react'; | ||
|
||
interface LoadingTodosContextType { | ||
loadingTodos: number[], | ||
setLoadingTodos: React.Dispatch<React.SetStateAction<number[]>> | ||
} | ||
|
||
export const LoadingTodosContext = React.createContext( | ||
{} as LoadingTodosContextType, | ||
); | ||
|
||
type Props = { | ||
children: React.ReactNode, | ||
}; | ||
|
||
export const LoadingTodosContextProvider: React.FC<Props> = ({ children }) => { | ||
const [loadingTodos, setLoadingTodos] = useState<number[]>([]); | ||
|
||
const value = { | ||
loadingTodos, | ||
setLoadingTodos, | ||
}; | ||
|
||
return ( | ||
<LoadingTodosContext.Provider value={value}> | ||
{children} | ||
</LoadingTodosContext.Provider> | ||
); | ||
}; | ||
|
||
export const useLoadingTodos = () => useContext(LoadingTodosContext); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import React, { useContext, useState } from 'react'; | ||
import { Todo } from '../../types/Todo'; | ||
|
||
interface TodosContextType { | ||
todos: Todo[], | ||
setTodos: React.Dispatch<React.SetStateAction<Todo[]>> | ||
} | ||
|
||
export const TodosContext = React.createContext({} as TodosContextType); | ||
|
||
type Props = { | ||
children: React.ReactNode, | ||
}; | ||
|
||
export const TodosContextProvider: React.FC<Props> = ({ children }) => { | ||
const [todos, setTodos] = useState<Todo[]>([]); | ||
|
||
const value = { | ||
todos, | ||
setTodos, | ||
}; | ||
|
||
return ( | ||
<TodosContext.Provider value={value}> | ||
{children} | ||
</TodosContext.Provider> | ||
); | ||
}; | ||
|
||
export const useTodos = () => useContext(TodosContext); |
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.
Better to keep all
state
s together, all your own hooks also together, all constants together and alluseEffect
s also together