-
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 #708
base: master
Are you sure you want to change the base?
Develop #708
Changes from 2 commits
67075bd
a04699d
0f96ab1
cd36639
24654eb
18bc34d
4c14365
a52d504
66c5340
999790f
4a270f1
00a1b2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,24 +1,169 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
/* eslint-disable max-len */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
/* eslint-disable jsx-a11y/control-has-associated-label */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
import React from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import React, { useEffect, useMemo, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { UserWarning } from './UserWarning'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { TodoList } from './Components/ToDoList'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { Notification } from './Components/errorNotification'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { client } from './utils/fetchClient'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const USER_ID = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { FilterType, Todo } from './types/Todo'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
import { getTodos, deleteTodo } from './api/todos'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
import { Footer } from './Components/Footer'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const USER_ID = 6340; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
export const App: React.FC = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const [todos, setTodos] = useState<Todo[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const [filterBy, setFilterBy] = useState<FilterType>(FilterType.ALL); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const [isError, setIsError] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const [inputQuery, setInputQuery] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const loadTodosData = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const todosFromServer = await getTodos(USER_ID); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
setTodos(todosFromServer); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
loadTodosData(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const addNewTodo = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
if (inputQuery.trim() === '') { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const newTodo = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
userId: USER_ID, | ||||||||||||||||||||||||||||||||||||||||||||||||||
title: inputQuery.trim(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
completed: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const response = await client.post<Todo>('/todos', 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.
better to keep such functions in separated |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
setTodos([...todos, response]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
setInputQuery(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const deleteTodoItem = async (todoId: number) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
await deleteTodo(todoId); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
setTodos(todos.filter(todo => todo.id !== todoId)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const handleFormSubmit = (e: React.FormEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
addNewTodo(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const handleTodoDelete = (todoId: number) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
deleteTodoItem(todoId); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setInputQuery(e.target.value); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const handleToggleAll = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const areAllCompleted = todos.every(todo => todo.completed); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const updatedTodos = todos.map(todo => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
...todo, | ||||||||||||||||||||||||||||||||||||||||||||||||||
completed: !areAllCompleted, | ||||||||||||||||||||||||||||||||||||||||||||||||||
})); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
setTodos(updatedTodos); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const visibleTodos = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
return todos.filter((todo) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
switch (filterBy) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
case FilterType.ACTIVE: | ||||||||||||||||||||||||||||||||||||||||||||||||||
return !todo.completed; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
case FilterType.COMPLETED: | ||||||||||||||||||||||||||||||||||||||||||||||||||
return todo.completed; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
default: | ||||||||||||||||||||||||||||||||||||||||||||||||||
return 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. No need to filter in case FilterType is All
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
}, [filterBy, todos]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
if (!USER_ID) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
return <UserWarning />; | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const updateTodoItem = async (todoId: number, updatedTodo: Todo) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
await client.patch<Todo>(`/todos/${todoId}`, updatedTodo); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
setTodos(prevTodos => prevTodos.map( | ||||||||||||||||||||||||||||||||||||||||||||||||||
todo => (todo.id === todoId ? updatedTodo : todo), | ||||||||||||||||||||||||||||||||||||||||||||||||||
)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsError(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
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 className="todoapp__header"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<button | ||||||||||||||||||||||||||||||||||||||||||||||||||
type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||
className="todoapp__toggle-all active" | ||||||||||||||||||||||||||||||||||||||||||||||||||
onClick={handleToggleAll} | ||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<form onSubmit={handleFormSubmit}> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<input | ||||||||||||||||||||||||||||||||||||||||||||||||||
type="text" | ||||||||||||||||||||||||||||||||||||||||||||||||||
className="todoapp__new-todo" | ||||||||||||||||||||||||||||||||||||||||||||||||||
placeholder="What needs to be done?" | ||||||||||||||||||||||||||||||||||||||||||||||||||
value={inputQuery} | ||||||||||||||||||||||||||||||||||||||||||||||||||
onChange={handleInputChange} | ||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||
</form> | ||||||||||||||||||||||||||||||||||||||||||||||||||
</header> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
{todos.length > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
<> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<TodoList | ||||||||||||||||||||||||||||||||||||||||||||||||||
todos={visibleTodos} | ||||||||||||||||||||||||||||||||||||||||||||||||||
onDelete={handleTodoDelete} | ||||||||||||||||||||||||||||||||||||||||||||||||||
onUpdate={updateTodoItem} | ||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<Footer | ||||||||||||||||||||||||||||||||||||||||||||||||||
filterBy={filterBy} | ||||||||||||||||||||||||||||||||||||||||||||||||||
setFilterBy={setFilterBy} | ||||||||||||||||||||||||||||||||||||||||||||||||||
todos={visibleTodos} | ||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||
</> | ||||||||||||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
{isError && <Notification isError={Boolean(isError)} />} | ||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import classNames from 'classnames'; | ||
import React from 'react'; | ||
import { Todo, FilterType } from '../types/Todo'; | ||
|
||
type Props = { | ||
filterBy: FilterType, | ||
setFilterBy: (value: FilterType) => void, | ||
todos: Todo[] | ||
}; | ||
|
||
const filterOptions = Object.values(FilterType); | ||
|
||
export const Footer: React.FC<Props> = React.memo(({ | ||
filterBy, | ||
setFilterBy, | ||
todos, | ||
}) => { | ||
const itemsLeftLength = todos.filter((todo) => !todo.completed).length; | ||
|
||
return ( | ||
<footer className="todoapp__footer"> | ||
<span className="todo-count"> | ||
{`${itemsLeftLength} items left`} | ||
</span> | ||
|
||
<nav className="filter"> | ||
{filterOptions.map((option) => { | ||
return ( | ||
<a | ||
key={option} | ||
href={`#/${option}`} | ||
className={classNames( | ||
'filter__link', | ||
{ selected: filterBy === option }, | ||
)} | ||
onClick={() => setFilterBy(option)} | ||
> | ||
{option} | ||
</a> | ||
); | ||
})} | ||
</nav> | ||
|
||
<button type="button" className="todoapp__clear-completed"> | ||
Clear completed | ||
</button> | ||
</footer> | ||
); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,130 @@ | ||||||
import React, { useState } from 'react'; | ||||||
import classNames from 'classnames'; | ||||||
import { Todo } from '../types/Todo'; | ||||||
|
||||||
type Props = { | ||||||
todos: Todo[]; | ||||||
onDelete: (todoId: number) => void; | ||||||
onUpdate: (todoId: number, updatedTodo: Todo) => void; | ||||||
}; | ||||||
|
||||||
export const TodoList: React.FC<Props> = React.memo( | ||||||
({ todos, onDelete, onUpdate }) => { | ||||||
const [editingTodoId, setEditingTodoId] = useState<number | null>(null); | ||||||
const [newTitle, setNewTitle] = useState(''); | ||||||
|
||||||
const handleStartEditing = (todoId: number, title: string) => { | ||||||
setEditingTodoId(todoId); | ||||||
setNewTitle(title); | ||||||
}; | ||||||
|
||||||
const handleCancelEditing = () => { | ||||||
setEditingTodoId(null); | ||||||
setNewTitle(''); | ||||||
}; | ||||||
|
||||||
const findTodoById = (todoId: number): Todo | undefined => { | ||||||
return todos.find(todo => todo.id === todoId); | ||||||
}; | ||||||
|
||||||
const updateTodoTitle = (todoId: number, title: string) => { | ||||||
const todoToUpdate = findTodoById(todoId); | ||||||
|
||||||
if (todoToUpdate) { | ||||||
onUpdate(todoId, { ...todoToUpdate, title }); | ||||||
} | ||||||
}; | ||||||
|
||||||
const handleSaveEditing = (todoId: number, title: string) => { | ||||||
if (title.trim() === '') { | ||||||
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
|
||||||
return; | ||||||
} | ||||||
|
||||||
setEditingTodoId(null); | ||||||
updateTodoTitle(todoId, title); | ||||||
}; | ||||||
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, | ||||||
todoId: number) => { | ||||||
if (e.key === 'Enter') { | ||||||
handleSaveEditing(todoId, newTitle); | ||||||
} | ||||||
}; | ||||||
|
||||||
const toggleTodoStatus = (todoId: number, completed: boolean) => { | ||||||
const todoToUpdate = findTodoById(todoId); | ||||||
|
||||||
if (todoToUpdate) { | ||||||
onUpdate(todoId, { ...todoToUpdate, completed: !completed }); | ||||||
} | ||||||
}; | ||||||
|
||||||
return ( | ||||||
<section className="todoapp__main"> | ||||||
{todos.map(({ id, completed, title }) => { | ||||||
const isEditing = editingTodoId === id; | ||||||
|
||||||
return ( | ||||||
<div | ||||||
key={id} | ||||||
className={classNames( | ||||||
'todo', | ||||||
{ completed, editing: isEditing }, | ||||||
)} | ||||||
> | ||||||
<label className="todo__status-label"> | ||||||
<input | ||||||
type="checkbox" | ||||||
className="todo__status" | ||||||
checked={completed} | ||||||
onChange={() => toggleTodoStatus(id, completed)} | ||||||
/> | ||||||
</label> | ||||||
|
||||||
{isEditing ? ( | ||||||
<input | ||||||
type="text" | ||||||
className="todo__edit-input" | ||||||
value={newTitle} | ||||||
onChange={(e) => setNewTitle(e.target.value)} | ||||||
onBlur={() => handleSaveEditing(id, newTitle)} | ||||||
onKeyDown={(e) => handleKeyDown(e, id)} | ||||||
/> | ||||||
) : ( | ||||||
<span | ||||||
className="todo__title" | ||||||
onDoubleClick={() => handleStartEditing(id, title)} | ||||||
> | ||||||
{title} | ||||||
</span> | ||||||
)} | ||||||
|
||||||
{isEditing ? ( | ||||||
<button | ||||||
type="button" | ||||||
className="todo__cancel-edit" | ||||||
onClick={handleCancelEditing} | ||||||
> | ||||||
Cancel | ||||||
</button> | ||||||
) : ( | ||||||
<button | ||||||
type="button" | ||||||
className="todo__remove" | ||||||
onClick={() => onDelete(id)} | ||||||
> | ||||||
× | ||||||
</button> | ||||||
)} | ||||||
|
||||||
<div className="modal overlay"> | ||||||
<div className="modal-background has-background-white-ter" /> | ||||||
<div className="loader" /> | ||||||
</div> | ||||||
</div> | ||||||
); | ||||||
})} | ||||||
</section> | ||||||
); | ||||||
}, | ||||||
); |
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.