-
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
solution #1541
base: master
Are you sure you want to change the base?
solution #1541
Changes from 5 commits
6d55969
c10a67c
b39ea1b
5a1fdd3
5af2f6b
bc1054b
f04c272
c5cce3c
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,26 +1,336 @@ | ||||||
/* 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 { UserWarning } from './UserWarning'; | ||||||
import React, { | ||||||
useEffect, | ||||||
useRef, | ||||||
useState, | ||||||
Dispatch, | ||||||
SetStateAction, | ||||||
FormEvent, | ||||||
useMemo, | ||||||
} from 'react'; | ||||||
import { USER_ID } from './api/todos'; | ||||||
import { Todo } from './types/Todo'; | ||||||
import { client } from './utils/fetchClient'; | ||||||
import cN from 'classnames'; | ||||||
import { TodoItem } from './components/TodoItem'; | ||||||
import { Filter } from './types/Filter'; | ||||||
import { Error } from './types/Error'; | ||||||
import { ErrorNotification } from './components/ErrorNotification'; | ||||||
import { filterTodosByStatus } from './utils/filterTodosByStatus'; | ||||||
import { removeTodoById } from './utils/removeTodoById'; | ||||||
|
||||||
const USER_ID = 0; | ||||||
const filterValues = Object.values(Filter); | ||||||
|
||||||
export const App: React.FC = () => { | ||||||
if (!USER_ID) { | ||||||
return <UserWarning />; | ||||||
const [todos, setTodos] = useState<Todo[]>([]); | ||||||
const [errorMessage, setErrorMessage] = useState<Error>(Error.Default); | ||||||
const [activeFilter, setActiveFilter] = useState<Filter>(Filter.All); | ||||||
const [tempTodo, setTempTodo] = useState<Todo | null>(null); | ||||||
const [todoToDeleteIds, setTodoToDeleteIds] = useState<number[]>([]); | ||||||
|
||||||
const [inputText, setInputText] = useState(''); | ||||||
const [filteredTodos, setFilteredTodos] = useState<Todo[]>([]); | ||||||
const [statusChangeId, setStatusChangeId] = useState<number[]>([]); | ||||||
|
||||||
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
One state for todo ids is enough |
||||||
const addTodoField = useRef<HTMLInputElement>(null); | ||||||
const errorTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||||||
|
||||||
const isAllCompleted = useMemo(() => { | ||||||
return !todos.some(todo => todo.completed === false); | ||||||
}, [todos]); | ||||||
|
||||||
const hasCompleted = useMemo(() => { | ||||||
return todos.some(todo => todo.completed === true); | ||||||
}, [todos]); | ||||||
|
||||||
const activeCount: number = useMemo(() => { | ||||||
return todos.reduce((acc, todo) => { | ||||||
if (todo.completed === false) { | ||||||
return acc + 1; | ||||||
} | ||||||
|
||||||
return acc; | ||||||
}, 0); | ||||||
}, [todos]); | ||||||
|
||||||
function trimTitle(text: string) { | ||||||
return text.replace(/\s+/g, ' ').trim(); | ||||||
} | ||||||
|
||||||
function showError(message: Error) { | ||||||
if (errorTimeoutRef.current) { | ||||||
clearTimeout(errorTimeoutRef.current); | ||||||
} | ||||||
|
||||||
setErrorMessage(message); | ||||||
errorTimeoutRef.current = setTimeout( | ||||||
() => setErrorMessage(Error.Default), | ||||||
3000, | ||||||
); | ||||||
} | ||||||
|
||||||
function filterToBool(filter: Filter) { | ||||||
let boolFilter = null; | ||||||
|
||||||
if (filter === Filter.Active) { | ||||||
boolFilter = false; | ||||||
} else if (filter === Filter.Completed) { | ||||||
boolFilter = 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. Use switch case instead. And you can simplify this condition |
||||||
return boolFilter; | ||||||
} | ||||||
|
||||||
function changeState( | ||||||
id: number, | ||||||
todosState: Dispatch<SetStateAction<Todo[]>>, | ||||||
updatedTodo: Todo, | ||||||
) { | ||||||
const filter = filterToBool(activeFilter); | ||||||
|
||||||
todosState(prev => { | ||||||
let changed = prev.map(todo => (todo.id === id ? updatedTodo : todo)); | ||||||
|
||||||
if (todosState === setFilteredTodos) { | ||||||
changed = filterTodosByStatus(changed, filter); | ||||||
} | ||||||
|
||||||
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 have no idea what is happening here. Try to simplify |
||||||
return changed; | ||||||
}); | ||||||
} | ||||||
|
||||||
function handleTitleChange(newTitle: string, editingTodoId: number | null) { | ||||||
const updateStatus = { title: newTitle }; | ||||||
|
||||||
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. Why do you call it status when actually store a title? |
||||||
return client | ||||||
.patch<Todo>(`/todos/${editingTodoId}`, updateStatus) | ||||||
.then(fetchedTodo => { | ||||||
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. Create a function to do it. Your component shouldn't know anything about URL where you are sending the request |
||||||
changeState(editingTodoId as number, setFilteredTodos, fetchedTodo); | ||||||
changeState(editingTodoId as number, setTodos, fetchedTodo); | ||||||
}) | ||||||
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. ??? setTodos(prevTodos => prevTodos.map(todo => todo.id === fetchedTodo.id ? fetchedTodo : todo)); |
||||||
.catch(error => { | ||||||
showError(Error.UpdateError); | ||||||
throw error; | ||||||
}); | ||||||
} | ||||||
|
||||||
function handleTodoStatusChange(id: number, newStatus: boolean) { | ||||||
setStatusChangeId(prev => [...prev, id]); | ||||||
const updateStatus = { completed: newStatus }; | ||||||
|
||||||
return client | ||||||
.patch<Todo>(`/todos/${id}`, updateStatus) | ||||||
.then(fetchedTodo => { | ||||||
changeState(id, setFilteredTodos, fetchedTodo); | ||||||
changeState(id, setTodos, fetchedTodo); | ||||||
}) | ||||||
.catch(() => showError(Error.UpdateError)) | ||||||
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. The same, fix in all places |
||||||
.finally(() => { | ||||||
setStatusChangeId(prev => prev.filter(idParametr => idParametr !== id)); | ||||||
}); | ||||||
} | ||||||
|
||||||
function setFocusOnAddInput() { | ||||||
if (addTodoField.current !== null) { | ||||||
addTodoField.current.focus(); | ||||||
} | ||||||
} | ||||||
|
||||||
useEffect(() => { | ||||||
client | ||||||
.get<Todo[]>(`/todos?userId=${USER_ID}`) | ||||||
.then(fetchedTodos => { | ||||||
setTodos(fetchedTodos); | ||||||
setFilteredTodos(fetchedTodos); | ||||||
setFocusOnAddInput(); | ||||||
}) | ||||||
.catch(() => showError(Error.LoadError)); | ||||||
}, []); | ||||||
|
||||||
useEffect(() => { | ||||||
if (tempTodo === null) { | ||||||
/*tempTodo === null для того, не виконувати це два рази (бо стейт tempTodo спочатку змінюється на об'єкт а потім змінюється на null)*/ | ||||||
setFocusOnAddInput(); | ||||||
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. Remove all comments. If you simplify your code and provide good names everyone understand what your code is doing without any comments |
||||||
} | ||||||
}, [tempTodo]); | ||||||
|
||||||
function handleAddTodoOnEnter(event: FormEvent<HTMLFormElement>) { | ||||||
event.preventDefault(); | ||||||
const title = trimTitle(inputText); | ||||||
|
||||||
if (title === '') { | ||||||
showError(Error.EmptyTitleError); | ||||||
} else { | ||||||
const newTodo = { | ||||||
id: 0, | ||||||
title: title, | ||||||
userId: USER_ID, | ||||||
completed: false, | ||||||
}; | ||||||
|
||||||
setTempTodo(newTodo); | ||||||
|
||||||
client | ||||||
.post<Todo>(`/todos`, newTodo) | ||||||
.then(fetchedTodo => { | ||||||
setInputText(''); | ||||||
setTodos(prevTodos => [...prevTodos, fetchedTodo]); | ||||||
setFilteredTodos(prevTodos => [...prevTodos, fetchedTodo]); | ||||||
}) | ||||||
.catch(() => showError(Error.AddError)) | ||||||
.finally(() => { | ||||||
setTempTodo(null); | ||||||
}); | ||||||
} | ||||||
} | ||||||
|
||||||
function onDelete(id: number): Promise<void> { | ||||||
return client | ||||||
.delete(`/todos/${id}`) | ||||||
.then(() => { | ||||||
setTodos(prevTodos => removeTodoById(prevTodos, id)); | ||||||
setFilteredTodos(prevTodos => removeTodoById(prevTodos, id)); | ||||||
}) | ||||||
.catch(() => showError(Error.DeleteError)); | ||||||
} | ||||||
|
||||||
function handleClearCompleted() { | ||||||
const completedTodoIds = todos.reduce( | ||||||
(acc, todo) => (todo.completed ? [...acc, todo.id] : acc), | ||||||
[] as number[], | ||||||
); | ||||||
|
||||||
setTodoToDeleteIds(completedTodoIds); | ||||||
|
||||||
const promises: Promise<void>[] = []; | ||||||
|
||||||
completedTodoIds.forEach(id => { | ||||||
promises.push(onDelete(id)); | ||||||
}); | ||||||
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. Use |
||||||
|
||||||
Promise.all(promises).then(() => { | ||||||
setFocusOnAddInput(); | ||||||
}); | ||||||
} | ||||||
|
||||||
//96 221 | ||||||
function handleFilter(filterParam: Filter) { | ||||||
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. ??? |
||||||
const filter = filterToBool(filterParam); | ||||||
|
||||||
setFilteredTodos(filterTodosByStatus(todos, filter)); | ||||||
setActiveFilter(filterParam); | ||||||
} | ||||||
|
||||||
useEffect(() => { | ||||||
handleFilter(activeFilter); //&&& | ||||||
}); | ||||||
|
||||||
function changeStatusAll() { | ||||||
const status = isAllCompleted ? false : 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. It's already boolean |
||||||
todos.forEach(todo => { | ||||||
if (todo.completed !== status) { | ||||||
handleTodoStatusChange(todo.id, status); | ||||||
} | ||||||
}); | ||||||
} | ||||||
|
||||||
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"> | ||||||
{/* this button should have `active` class only if all todos are completed +++*/} | ||||||
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. Create a header component |
||||||
{todos.length !== 0 && ( | ||||||
<button | ||||||
type="button" | ||||||
className={cN('todoapp__toggle-all', { active: isAllCompleted })} | ||||||
data-cy="ToggleAllButton" | ||||||
onClick={changeStatusAll} | ||||||
/> | ||||||
)} | ||||||
|
||||||
{/* Add a todo on form submit +*/} | ||||||
<form onSubmit={handleAddTodoOnEnter}> | ||||||
<input | ||||||
ref={addTodoField} | ||||||
data-cy="NewTodoField" | ||||||
type="text" | ||||||
value={inputText} | ||||||
className="todoapp__new-todo" | ||||||
placeholder="What needs to be done?" | ||||||
onChange={event => setInputText(event.target.value)} | ||||||
disabled={tempTodo !== null} | ||||||
/> | ||||||
</form> | ||||||
</header> | ||||||
<section className="todoapp__main" data-cy="TodoList"> | ||||||
{filteredTodos.map(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. Create a TodoList component |
||||||
<TodoItem | ||||||
key={todo.id} | ||||||
todo={todo} | ||||||
handleTodoStatusChange={handleTodoStatusChange} | ||||||
onDelete={onDelete} | ||||||
todoToDeleteIds={todoToDeleteIds} | ||||||
setTodoToDeleteIds={setTodoToDeleteIds} | ||||||
addTodoField={addTodoField} | ||||||
statusChangeId={statusChangeId} | ||||||
handleTitleChange={handleTitleChange} | ||||||
/> | ||||||
))} | ||||||
{tempTodo !== null && ( | ||||||
<TodoItem key={tempTodo.id} todo={tempTodo} isTemp={true} /> | ||||||
)} | ||||||
</section> | ||||||
|
||||||
{/* Hide the footer if there are no todos +++*/} | ||||||
{todos.length !== 0 && ( | ||||||
<footer className="todoapp__footer" data-cy="Footer"> | ||||||
<span className="todo-count" data-cy="TodosCounter"> | ||||||
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. Create a Footer component |
||||||
{activeCount} items left | ||||||
</span> | ||||||
|
||||||
{/* Active link should have the 'selected' class +++*/} | ||||||
<nav className="filter" data-cy="Filter"> | ||||||
{filterValues.map(filter => { | ||||||
return ( | ||||||
<a | ||||||
key={filter} | ||||||
href={`#/${filter === 'All' ? '' : filter}`} | ||||||
className={cN('filter__link', { | ||||||
selected: activeFilter === filter, | ||||||
})} | ||||||
data-cy={`FilterLink${filter}`} | ||||||
onClick={() => handleFilter(filter)} | ||||||
> | ||||||
{filter} | ||||||
</a> | ||||||
); | ||||||
})} | ||||||
</nav> | ||||||
|
||||||
{/* this button should be disabled if there are no completed todos +++*/} | ||||||
<button | ||||||
type="button" | ||||||
className="todoapp__clear-completed" | ||||||
data-cy="ClearCompletedButton" | ||||||
disabled={!hasCompleted} | ||||||
onClick={handleClearCompleted} | ||||||
> | ||||||
Clear completed | ||||||
</button> | ||||||
</footer> | ||||||
)} | ||||||
</div> | ||||||
|
||||||
{/* DON'T use conditional rendering to hide the notification +++*/} | ||||||
{/* Add the 'hidden' class to hide the message smoothly +++*/} | ||||||
<ErrorNotification | ||||||
errorMessage={errorMessage} | ||||||
setErrorMessage={setErrorMessage} | ||||||
/> | ||||||
</div> | ||||||
); | ||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Todo } from '../types/Todo'; | ||
import { client } from '../utils/fetchClient'; | ||
|
||
export const USER_ID = 2148; | ||
|
||
export const getTodos = () => { | ||
return client.get<Todo[]>(`/todos?userId=${USER_ID}`); | ||
}; | ||
|
||
// Add more methods here | ||
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. // Add more methods here |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* eslint-disable jsx-a11y/label-has-associated-control */ | ||
/* eslint-disable jsx-a11y/control-has-associated-label */ | ||
import React, { Dispatch, SetStateAction } from 'react'; | ||
import cN from 'classnames'; | ||
import { Error } from '../../types/Error'; | ||
|
||
type Props = { | ||
errorMessage: Error; | ||
setErrorMessage: Dispatch<SetStateAction<Error>>; | ||
}; | ||
|
||
export const ErrorNotification: React.FC<Props> = ({ | ||
errorMessage, | ||
setErrorMessage, | ||
}) => { | ||
return ( | ||
<div | ||
data-cy="ErrorNotification" | ||
className={cN('notification is-danger is-light has-text-weight-normal', { | ||
hidden: errorMessage === Error.Default, | ||
})} | ||
> | ||
<button | ||
onClick={() => setErrorMessage(Error.Default)} | ||
data-cy="HideErrorButton" | ||
type="button" | ||
className="delete" | ||
/> | ||
{/* show only one message at a time +++*/} | ||
{errorMessage} | ||
</div> | ||
); | ||
}; |
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 don't need one more state for todos.
filteredTodos
are dynamic and depends onactiveFilter
so just store in the variable