-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
583 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,147 @@ | ||
/* eslint-disable max-len */ | ||
/* eslint-disable jsx-a11y/label-has-associated-control */ | ||
/* eslint-disable jsx-a11y/control-has-associated-label */ | ||
import React from 'react'; | ||
import React, { useEffect, useMemo, useState } from 'react'; | ||
import { UserWarning } from './UserWarning'; | ||
import { USER_ID } from './api/todos'; | ||
import * as servisesTodos from './api/todos'; | ||
import { Todo } from './types/Todo'; | ||
import { TodoInput } from './components/TodoInput/TodoInput'; | ||
import { TodoList } from './components/TodoList/TodoList'; | ||
import { ErrorNotification } from './components/ErrorNotification/ErrorNotification'; | ||
import { Footer } from './components/Footer/Footer'; | ||
import { Filter } from './types/Filter'; | ||
|
||
const USER_ID = 0; | ||
export enum FilterParam { | ||
All = 'All', | ||
Active = 'Active', | ||
Completed = 'Completed', | ||
} | ||
|
||
export const App: React.FC = () => { | ||
const [todos, setTodos] = useState<Todo[]>([]); | ||
const [errorMessage, setErrorMessage] = useState(''); | ||
const [filter, setFilter] = useState<Filter>(FilterParam.All); | ||
const [tempTodo, setTempTodo] = useState(null); | ||
const [loadingIds, setLoadingIds] = useState<number[]>([]); | ||
|
||
useEffect(() => { | ||
servisesTodos | ||
.getTodos() | ||
.then(setTodos) | ||
.catch(() => { | ||
setErrorMessage('Unable to load todos'); | ||
}); | ||
}, []); | ||
|
||
useEffect(() => { | ||
setTimeout(() => { | ||
setErrorMessage(''); | ||
}, 3000); | ||
}, [errorMessage]); | ||
|
||
const filteredTodos = useMemo(() => { | ||
if (filter === FilterParam.Active) { | ||
return todos.filter(todo => !todo.completed); | ||
} | ||
|
||
if (filter === FilterParam.Completed) { | ||
return todos.filter(todo => todo.completed); | ||
} | ||
|
||
return todos; | ||
}, [filter, todos]); | ||
|
||
const handleDeleteTodo = (todoId: number) => { | ||
setLoadingIds(current => [...current, todoId]); | ||
|
||
return servisesTodos | ||
.deleteTodo(todoId) | ||
.then(() => { | ||
setTodos(currentTodo => currentTodo.filter(todo => todo.id !== todoId)); | ||
}) | ||
.catch(error => { | ||
setErrorMessage('Unable to delete a todo'); | ||
throw error; | ||
}) | ||
.finally(() => { | ||
setLoadingIds(current => current.filter(id => id !== todoId)); | ||
}); | ||
}; | ||
|
||
const handleUpdateTodo = ( | ||
todoId: number, | ||
newTitle: string, | ||
completed: boolean, | ||
) => { | ||
setLoadingIds(current => [...current, todoId]); | ||
const todoToUpdate = todos.find(todo => todo.id === todoId); | ||
|
||
if (!todoToUpdate) { | ||
return; | ||
} | ||
|
||
const updatedTodo = { | ||
...todoToUpdate, | ||
title: newTitle.trim(), | ||
completed: completed, | ||
}; | ||
|
||
return servisesTodos | ||
.updateTodo(todoId, updatedTodo) | ||
.then(() => | ||
setTodos(currentTodo => | ||
currentTodo.map(todo => (todo.id === todoId ? updatedTodo : todo)), | ||
), | ||
) | ||
.catch(error => { | ||
setErrorMessage('Unable to update a todo'); | ||
throw error; | ||
}) | ||
.finally(() => { | ||
setLoadingIds(current => current.filter(id => id !== todoId)); | ||
}); | ||
}; | ||
|
||
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"> | ||
<TodoInput | ||
setErrorMessage={setErrorMessage} | ||
todos={todos} | ||
setTodos={setTodos} | ||
setTempTodo={setTempTodo} | ||
tempTodo={tempTodo} | ||
handleUpdateTodo={handleUpdateTodo} | ||
loadingIds={loadingIds} | ||
/> | ||
|
||
<TodoList | ||
filteredTodos={filteredTodos} | ||
tempTodo={tempTodo} | ||
handleDeleteTodo={handleDeleteTodo} | ||
handleUpdateTodo={handleUpdateTodo} | ||
loadingIds={loadingIds} | ||
/> | ||
{todos.length > 0 && ( | ||
<Footer | ||
filter={filter} | ||
setFilter={setFilter} | ||
todos={todos} | ||
handleDeleteTodo={handleDeleteTodo} | ||
/> | ||
)} | ||
</div> | ||
<ErrorNotification | ||
errorMessage={errorMessage} | ||
setErrorMessage={setErrorMessage} | ||
/> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Todo } from '../types/Todo'; | ||
import { client } from '../utils/fetchClient'; | ||
|
||
export const USER_ID = 1913; | ||
|
||
export const getTodos = () => { | ||
return client.get<Todo[]>(`/todos?userId=${USER_ID}`); | ||
}; | ||
|
||
export const postTodos = ({ title, userId, completed }: Omit<Todo, 'id'>) => { | ||
return client.post<Todo>(`/todos/`, { title, userId, completed }); | ||
}; | ||
|
||
export const deleteTodo = (todoId: number) => { | ||
return client.delete(`/todos/${todoId}`); | ||
}; | ||
|
||
export const updateTodo = (todoId: number, todo: Todo) => { | ||
return client.patch(`/todos/${todoId}`, todo); | ||
}; | ||
// Add more methods here |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import classNames from 'classnames'; | ||
import React from 'react'; | ||
|
||
type Props = { | ||
errorMessage: string; | ||
setErrorMessage: (a: string) => void; | ||
}; | ||
|
||
export const ErrorNotification: React.FC<Props> = ({ | ||
errorMessage, | ||
setErrorMessage, | ||
}) => { | ||
const handleCloseNotification = () => { | ||
setErrorMessage(''); | ||
}; | ||
|
||
return ( | ||
<div | ||
data-cy="ErrorNotification" | ||
className={classNames( | ||
'notification is-danger is-light has-text-weight-normal', | ||
// eslint-disable-next-line prettier/prettier | ||
{ hidden: !errorMessage }, | ||
)} | ||
> | ||
<button | ||
onClick={handleCloseNotification} | ||
data-cy="HideErrorButton" | ||
type="button" | ||
className="delete" | ||
/> | ||
{errorMessage} | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import classNames from 'classnames'; | ||
import { Todo } from '../../types/Todo'; | ||
import { Filter } from '../../types/Filter'; | ||
import { FilterParam } from '../../App'; | ||
|
||
type Props = { | ||
filter: string; | ||
setFilter: (a: Filter) => void; | ||
todos: Todo[]; | ||
handleDeleteTodo: (a: number) => void; | ||
}; | ||
|
||
export const Footer: React.FC<Props> = ({ | ||
filter, | ||
setFilter, | ||
todos, | ||
handleDeleteTodo, | ||
}) => { | ||
const handleFilter = (event: React.MouseEvent<HTMLElement>) => { | ||
const filterValue = event.currentTarget.textContent as Filter; | ||
|
||
setFilter(filterValue); | ||
}; | ||
|
||
const handleClearComplete = () => { | ||
const completedTodos = todos.filter(todo => todo.completed); | ||
const deletePromises = completedTodos.map(todo => | ||
handleDeleteTodo(todo.id), | ||
); | ||
|
||
Promise.allSettled(deletePromises); | ||
}; | ||
|
||
const amountActiveTodos = todos.filter(todo => !todo.completed).length; | ||
|
||
return ( | ||
<footer className="todoapp__footer" data-cy="Footer"> | ||
<span className="todo-count" data-cy="TodosCounter"> | ||
{amountActiveTodos} items left | ||
</span> | ||
|
||
<nav className="filter" data-cy="Filter"> | ||
{Object.values(FilterParam).map(param => ( | ||
<a | ||
key={param} | ||
href="#/" | ||
className={classNames('filter__link', { | ||
selected: filter === param, | ||
})} | ||
data-cy={`FilterLink${param}`} | ||
onClick={handleFilter} | ||
> | ||
{param} | ||
</a> | ||
))} | ||
</nav> | ||
<button | ||
type="button" | ||
className="todoapp__clear-completed" | ||
data-cy="ClearCompletedButton" | ||
disabled={todos.every(todo => !todo.completed)} | ||
onClick={handleClearComplete} | ||
> | ||
Clear completed | ||
</button> | ||
</footer> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { useEffect, useRef, useState } from 'react'; | ||
import * as servisesTodos from '../../api/todos'; | ||
import { Todo } from '../../types/Todo'; | ||
import classNames from 'classnames'; | ||
|
||
const userId = servisesTodos.USER_ID; | ||
|
||
type Props = { | ||
setErrorMessage: (a: string) => void; | ||
todos: Todo[]; | ||
setTodos: (a: Todo[]) => void; | ||
setTempTodo: (a: Todo[] | null) => void; | ||
tempTodo: Todo | null; | ||
handleUpdateTodo: (a: number, b: string, c: boolean) => void; | ||
loadingIds: number[]; | ||
}; | ||
|
||
export const TodoInput: React.FC<Props> = ({ | ||
setErrorMessage, | ||
todos, | ||
setTodos, | ||
setTempTodo, | ||
tempTodo, | ||
handleUpdateTodo, | ||
loadingIds, | ||
}) => { | ||
const [title, setTitle] = useState(''); | ||
const inputRef = useRef<HTMLInputElement>(null); | ||
const AllCompletedTodo = todos.every(todo => todo.completed); | ||
|
||
useEffect(() => { | ||
if (inputRef.current) { | ||
inputRef.current.focus(); | ||
} | ||
}, [todos, tempTodo]); | ||
|
||
const handleInputValue = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
setTitle(event.target.value); | ||
}; | ||
|
||
const handleAddTodo = (event: React.KeyboardEvent<HTMLInputElement>) => { | ||
if (event.key === 'Enter') { | ||
event.preventDefault(); | ||
setTempTodo({ id: 0, title: title.trim(), userId, completed: false }); | ||
if (title.trim().length > 0) { | ||
servisesTodos | ||
.postTodos({ title: title.trim(), userId, completed: false }) | ||
.then(newTodo => { | ||
setTodos([...todos, newTodo]); | ||
setTitle(''); | ||
setTempTodo(null); | ||
}) | ||
.catch(() => { | ||
setTempTodo(null); | ||
setErrorMessage('Unable to add a todo'); | ||
}); | ||
} else { | ||
setTempTodo(null); | ||
setErrorMessage('Title should not be empty'); | ||
} | ||
} | ||
}; | ||
|
||
const handleChangleAllStatus = () => { | ||
const newCompletedStatus = !AllCompletedTodo; | ||
const todosToUpdate = todos.filter( | ||
todo => todo.completed !== newCompletedStatus, | ||
); | ||
|
||
todosToUpdate.map(todo => | ||
handleUpdateTodo(todo.id, todo.title, newCompletedStatus), | ||
); | ||
}; | ||
|
||
return ( | ||
<header className="todoapp__header"> | ||
{todos.length > 0 && ( | ||
<button | ||
type="button" | ||
className={classNames('todoapp__toggle-all', { | ||
active: AllCompletedTodo, | ||
})} | ||
data-cy="ToggleAllButton" | ||
onClick={handleChangleAllStatus} | ||
disabled={loadingIds.length > 0} | ||
/> | ||
)} | ||
<form onKeyDown={handleAddTodo}> | ||
<input | ||
ref={inputRef} | ||
data-cy="NewTodoField" | ||
value={title} | ||
type="text" | ||
className="todoapp__new-todo" | ||
placeholder="What needs to be done?" | ||
onChange={handleInputValue} | ||
disabled={tempTodo} | ||
/> | ||
</form> | ||
</header> | ||
); | ||
}; |
Oops, something went wrong.