-
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 2 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,150 @@ | ||||||||||||||||||
/* 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, | ||||||||||||||||||
setErrorMessage, | ||||||||||||||||||
isErrorHidden, | ||||||||||||||||||
setIsErrorHidden, | ||||||||||||||||||
} = useErrorMessage(); | ||||||||||||||||||
|
||||||||||||||||||
const [filterParam, setFilterParam] = useState(Filters.All); | ||||||||||||||||||
|
||||||||||||||||||
const clearCompletedTodos = () => { | ||||||||||||||||||
const completedTodos = countTodos(todos, true); | ||||||||||||||||||
|
||||||||||||||||||
completedTodos.forEach(({ id }) => { | ||||||||||||||||||
setLoadingTodos(prev => [...prev, { todoId: id, isLoading: true }]); | ||||||||||||||||||
deleteTodo(id) | ||||||||||||||||||
.then(() => { | ||||||||||||||||||
setTodos(prev => prev.filter((todo) => id !== todo.id)); | ||||||||||||||||||
}) | ||||||||||||||||||
.catch((error) => { | ||||||||||||||||||
setErrorMessage(JSON.parse(error.message).error); | ||||||||||||||||||
setIsErrorHidden(false); | ||||||||||||||||||
|
||||||||||||||||||
setTimeout(() => { | ||||||||||||||||||
setIsErrorHidden(true); | ||||||||||||||||||
}, 3000); | ||||||||||||||||||
}) | ||||||||||||||||||
.finally(() => { | ||||||||||||||||||
setLoadingTodos(prev => prev.filter(todo => todo.todoId !== id)); | ||||||||||||||||||
}); | ||||||||||||||||||
}); | ||||||||||||||||||
}; | ||||||||||||||||||
|
||||||||||||||||||
useEffect(() => { | ||||||||||||||||||
getTodos(USER_ID) | ||||||||||||||||||
.then(setTodos) | ||||||||||||||||||
.catch((error) => { | ||||||||||||||||||
setErrorMessage(JSON.parse(error.message).error); | ||||||||||||||||||
setIsErrorHidden(false); | ||||||||||||||||||
|
||||||||||||||||||
setTimeout(() => { | ||||||||||||||||||
setIsErrorHidden(true); | ||||||||||||||||||
}, 3000); | ||||||||||||||||||
}); | ||||||||||||||||||
}, []); | ||||||||||||||||||
|
||||||||||||||||||
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} | ||||||||||||||||||
/> | ||||||||||||||||||
|
||||||||||||||||||
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
Better write it in one line if there are < 3 props |
||||||||||||||||||
<section className="todoapp__main"> | ||||||||||||||||||
<TodoList | ||||||||||||||||||
todos={visibleTodos} | ||||||||||||||||||
/> | ||||||||||||||||||
|
||||||||||||||||||
{tempTodo && ( | ||||||||||||||||||
<div className="todo"> | ||||||||||||||||||
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? |
||||||||||||||||||
<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={(newFilter) => setFilterParam(newFilter)} | ||||||||||||||||||
clearCompleted={clearCompletedTodos} | ||||||||||||||||||
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
|
||||||||||||||||||
/> | ||||||||||||||||||
)} | ||||||||||||||||||
</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" | ||||||||||||||||||
/> | ||||||||||||||||||
|
||||||||||||||||||
{errorMessage} | ||||||||||||||||||
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
|
||||||||||||||||||
<br /> | ||||||||||||||||||
</div> | ||||||||||||||||||
</div> | ||||||||||||||||||
); | ||||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Todo } from '../types/Todo'; | ||
import { UpdateCompleted } from '../types/UpdateCompleted'; | ||
import { UpdateTitle } from '../types/UpdateTitle'; | ||
import { client } from '../utils/fetchClient'; | ||
|
||
export const getTodos = (userId: number) => { | ||
return client.get<Todo[]>(`/todos?userId=${userId}`); | ||
}; | ||
|
||
// Add more methods here | ||
|
||
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,38 @@ | ||||||||||||||
import React, { useContext, useState } from 'react'; | ||||||||||||||
|
||||||||||||||
interface ErrorContextType { | ||||||||||||||
errorMessage: string, | ||||||||||||||
setErrorMessage: React.Dispatch<React.SetStateAction<string>> | ||||||||||||||
isErrorHidden: boolean, | ||||||||||||||
setIsErrorHidden: React.Dispatch<React.SetStateAction<boolean>>, | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
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, | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
return ( | ||||||||||||||
<ErrorMessageContext.Provider value={value}> | ||||||||||||||
{children} | ||||||||||||||
</ErrorMessageContext.Provider> | ||||||||||||||
); | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
export const useErrorMessage = () => { | ||||||||||||||
const errorMessage = useContext(ErrorMessageContext); | ||||||||||||||
|
||||||||||||||
return errorMessage; | ||||||||||||||
}; | ||||||||||||||
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
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import React, { useContext, useState } from 'react'; | ||
import { LoadingTodo } from '../../types/LoadingTodo'; | ||
|
||
interface LoadingTodosContextType { | ||
loadingTodos: LoadingTodo[], | ||
setLoadingTodos: React.Dispatch<React.SetStateAction<LoadingTodo[]>> | ||
} | ||
|
||
export const LoadingTodosContext = React.createContext( | ||
{} as LoadingTodosContextType, | ||
); | ||
|
||
type Props = { | ||
children: React.ReactNode, | ||
}; | ||
|
||
export const LoadingTodosContextProvider: React.FC<Props> = ({ children }) => { | ||
const [loadingTodos, setLoadingTodos] = useState<LoadingTodo[]>([]); | ||
|
||
const value = { | ||
loadingTodos, | ||
setLoadingTodos, | ||
}; | ||
|
||
return ( | ||
<LoadingTodosContext.Provider value={value}> | ||
{children} | ||
</LoadingTodosContext.Provider> | ||
); | ||
}; | ||
|
||
export const useLoadingTodos = () => { | ||
const loadingTodos = useContext(LoadingTodosContext); | ||
|
||
return loadingTodos; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
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 = () => { | ||
const todos = useContext(TodosContext); | ||
|
||
return todos; | ||
}; |
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.
You execute similar logic several times, you can move it to a separate handler: