diff --git a/src/App.tsx b/src/App.tsx index a6a8bee5be..8c336a3638 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,365 +1,177 @@ -/*eslint-disable*/ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/control-has-associated-label */ - -import React, { useEffect, useRef } from 'react'; -import { UserWarning } from './UserWarning'; -import { addTodo, changeTodoCompleted, changeTodoTitle, deleteTodo, USER_ID } from './api/todos'; -import { getTodos } from './api/todos'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + addTodo, + deleteTodo, + getTodos, + updateTodo, + USER_ID, +} from './api/todos'; import { Todo } from './types/Todo'; -import classNames from 'classnames'; -import { Footer } from './components/Footer/Footer'; -import { TodoList } from './components/TodoList/TodoList'; -import { ErrorObject, FilterEnum } from './utils/types'; +import { TodoHeader } from './components/TodoHeader'; +import { TodoFooter } from './components/TodoFooter'; +import { ErrorNotification } from './components/ErrorNotification'; +import { ErrorType } from './types/ErrorTypes'; +import { FilterStatus } from './types/FilterStatus'; +import { TodoList } from './components/TodoList'; export const App: React.FC = () => { + const [todos, setTodos] = useState([]); + const [errorMessage, setErrorMessage] = useState(ErrorType.Empty); + const [filterStatus, setFilterStatus] = useState( + FilterStatus.All, + ); + const [tempTodo, setTempTodo] = useState(null); + const [loadingTodoIds, setLoadingTodoIds] = useState([]); - const inputRef = useRef(null); - const [todosData, setTodosData] = React.useState([]); - const [error, setError] = React.useState(''); - const [filter, setFilter] = React.useState(FilterEnum.All); - const [title, setTitle] = React.useState(''); - const [tempTodo, setTempTodo] = React.useState(null); - const [deletingTodo, setDeletingTodo] = React.useState(null); - const [isDeletingCompleted, setIsDeletingCompleted] = React.useState(false); - const [updatingTodos, setUpdatingTodos] = React.useState([]); - const [updatingTodoId, setUpdatingTodoId] = React.useState(null); - const [tempTitle, setTempTitle] = React.useState(''); - const [isServerRequest, setIsServerRequest] = React.useState(false); - - if (!USER_ID) { - return ; - } - - useEffect(() => { -console.log(updatingTodos) }, [updatingTodos]); - - useEffect(() => { - inputRef.current?.focus(); - }, []); - - useEffect(() => { - const fetchTodos = async () => { - try { - - const currentTodos = await getTodos(); - - setTodosData(currentTodos); - - } catch (err) { - setError('Load'); - } finally { - } - }; - - fetchTodos(); - }, []); - - const filteredTodos = todosData.filter(todo => { - switch (filter) { - case 'Active': - return !todo.completed; - case 'Completed': - return todo.completed; - default: - return true; - } - }); - - useEffect(() => { - if (!error) { - return; - } - - const timer = setTimeout(() => setError(''), 3000); - - return () => clearTimeout(timer); - }, [error]); + const inputAddRef = useRef(null); - useEffect(() => { - if (!tempTodo && !isDeletingCompleted && !deletingTodo) { - inputRef.current?.focus(); - } - }, [tempTodo, isDeletingCompleted, deletingTodo]); + const filteredTodos = useMemo( + () => + todos.filter(todo => { + if (filterStatus === FilterStatus.All) { + return true; + } - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + return filterStatus === FilterStatus.Completed + ? todo.completed + : !todo.completed; + }), + [todos, filterStatus], + ); - if(!title.trim()) { - setError('Title'); - return; - } + const todosActiveNum = useMemo( + () => todos.filter(todo => !todo.completed).length, + [todos], + ); - const newTempTodo = { - id: 0, - title: title.trim(), - completed: false, - userId: USER_ID, - }; + const todosCompleted = useMemo( + () => todos.filter(todo => todo.completed).length, + [todos], + ); - setTempTodo(newTempTodo); + const areAllTodosCompleted = useMemo( + () => todos.every(todo => todo.completed), + [todos], + ); + const onAddTodo = async (todoTitle: string) => { + setTempTodo({ id: 0, title: todoTitle, completed: false, userId: USER_ID }); try { - const newTodo = await addTodo({title: title.trim(), completed: false}); - setTodosData((prevTodos)=>[...prevTodos, newTodo]); - setTitle(''); + const newTodo = await addTodo({ title: todoTitle, completed: false }); + + setTodos(prev => [...prev, newTodo]); } catch (err) { - setError('Add'); + setErrorMessage(ErrorType.AddTodo); + inputAddRef?.current?.focus(); + throw err; } finally { setTempTodo(null); } }; - const handleDeleteOneTodo = async (id:number) => { - - setDeletingTodo(id); - + const onRemoveTodo = async (todoId: number) => { + setLoadingTodoIds(prev => [...prev, todoId]); try { - await deleteTodo(id); - setTodosData((prevState)=>prevState.filter((todo) => todo.id !== id)); + await deleteTodo(todoId); + setTodos(prev => prev.filter(todo => todo.id !== todoId)); } catch (err) { - setError('Delete'); + setErrorMessage(ErrorType.DeleteTodo); + inputAddRef?.current?.focus(); + throw err; } finally { - setDeletingTodo(null); + setLoadingTodoIds(prev => prev.filter(id => id !== todoId)); } }; - - const handleDeleteCompletedTodos = async () => { - const completedTodos = todosData.filter(todo => todo.completed); - const remainingTodos = [...todosData]; - - setIsDeletingCompleted(true); - - await Promise.all( - completedTodos.map(async todo => { - try { - await deleteTodo(todo.id); - const index = remainingTodos.findIndex(t => t.id === todo.id); - if (index !== -1) { - remainingTodos.splice(index, 1); - } - } catch { - setError('Delete'); - } - finally { - setIsDeletingCompleted(false); - - } - }) - ); - - setTodosData(remainingTodos); - }; - - const handleToggleTodos = async () => { - const completedTodos = todosData.filter(todo => todo.completed); - const isAllCompleted = completedTodos.length === todosData.length; - + const onUpdateTodo = async (todoToUpdate: Todo) => { + setLoadingTodoIds(prev => [...prev, todoToUpdate.id]); try { - const todosToUpdate = todosData.filter(todo => todo.completed === isAllCompleted); - - setUpdatingTodos(todosToUpdate.map(todo => todo.id)); + const updatedTodo = await updateTodo(todoToUpdate); - await Promise.all( - todosToUpdate.map(async todo => { - await changeTodoCompleted({ id: todo.id, completed: !isAllCompleted }); - }) + setTodos(prev => + prev.map(todo => (todo.id === updatedTodo.id ? updatedTodo : todo)), ); - - setTodosData(prevTodos => - prevTodos.map(todo => - todosToUpdate.some(t => t.id === todo.id) - ? { ...todo, completed: !isAllCompleted } - : todo - ) - ); - } catch { - setError('Update'); + } catch (err) { + setErrorMessage(ErrorType.UpdateTodo); + throw err; } finally { - setUpdatingTodos([]); + setLoadingTodoIds(prev => prev.filter(id => id !== todoToUpdate.id)); } }; - const toggleTodo = async (id: number) => { - const todo = todosData.find(todo => todo.id === id); - if (!todo) return; - - const updatedTodo = { ...todo, completed: !todo.completed }; + const onClearCompleted = async () => { + const completedTodos = todos.filter(todo => todo.completed); - setUpdatingTodos(prev => [...prev, id]); - - try { - await changeTodoCompleted({ id, completed: updatedTodo.completed }); - - setTodosData(prevTodos => - prevTodos.map(t => (t.id === id ? updatedTodo : t)) - ); - } catch { - setError('Update'); - } finally { - setUpdatingTodos(prev => prev.filter(todoId => todoId !== id)); - } + completedTodos.forEach(todo => { + onRemoveTodo(todo.id); + }); }; - const handleEditTodoTitle = async (id: number) => { - const todo = todosData.find(todo => todo.id === id); - if (!todo) return; - if (tempTitle.trim() === '') { - await handleDeleteOneTodo(id); - } else if (tempTitle.trim() === todo.title) { - setUpdatingTodoId(null); - return; - } - setIsServerRequest(true) - try { - await changeTodoTitle({ id, title: tempTitle.trim() }); - setTodosData(prevTodos => - prevTodos.map(t => (t.id === id ? { ...t, title: tempTitle.trim() } : t)) - ); - setUpdatingTodoId(null); - - } catch { - setError('Update'); - } finally { - setIsServerRequest(false) + const onToggleAll = async () => { + if (todosActiveNum > 0) { + const activeTodos = todos.filter(todo => !todo.completed); + + activeTodos.forEach(todo => { + onUpdateTodo({ ...todo, completed: true }); + }); + } else { + todos.forEach(todo => { + onUpdateTodo({ ...todo, completed: false }); + }); } }; + useEffect(() => { + (async () => { + try { + const data = await getTodos(); - + setTodos(data); + } catch (err) { + setErrorMessage(ErrorType.LoadTodos); + } + })(); + }, []); return (

todos

-
- {/* this button should have `active` class only if all todos are completed */} - {todosData.length>0 && (
- -
- -
- {/* - - This todo is being edited -
- - - This form is shown instead of the title and remove button -
- -
- -
-
-
-
-
- - This todo is in loadind state -
- - - - Todo is being saved now - - - - - 'is-active' class puts this modal on top of the todo -
-
-
-
-
- */} - -
-
- - {/* DON'T use conditional rendering to hide the notification */} - {/* Add the 'hidden' class to hide the message smoothly */} -
+ )} - > -
+ +
); }; diff --git a/src/api/todos.ts b/src/api/todos.ts index 74be44fb16..b519829a76 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,30 +1,20 @@ import { Todo } from '../types/Todo'; import { client } from '../utils/fetchClient'; -export const USER_ID = 2161; +export const USER_ID = 2164; export const getTodos = () => { return client.get(`/todos?userId=${USER_ID}`); }; -export const addTodo = ({ title, completed }: Omit) => { - return client.post('/todos', { title, completed, userId: USER_ID }); +export const addTodo = (newTodo: Omit) => { + return client.post(`/todos`, { ...newTodo, userId: USER_ID }); }; -export const deleteTodo = (id: number) => { - return client.delete(`/todos/${id}`); +export const deleteTodo = (todoId: number) => { + return client.delete(`/todos/${todoId}`); }; -export const changeTodoCompleted = ({ - id, - completed, -}: Omit) => { - return client.patch(`/todos/${id}`, { completed }); -}; - -export const changeTodoTitle = ({ - id, - title, -}: Omit) => { - return client.patch(`/todos/${id}`, { title }); +export const updateTodo = (todo: Todo) => { + return client.patch(`/todos/${todo.id}`, todo); }; diff --git a/src/components/ErrorNotification.tsx b/src/components/ErrorNotification.tsx new file mode 100644 index 0000000000..3c5dc467aa --- /dev/null +++ b/src/components/ErrorNotification.tsx @@ -0,0 +1,44 @@ +import React, { Dispatch, SetStateAction, useEffect } from 'react'; +import { ErrorType } from '../types/ErrorTypes'; +import classNames from 'classnames'; + +type Props = { + error: ErrorType; + setError: Dispatch>; +}; + +export const ErrorNotification: React.FC = props => { + const { error, setError } = props; + + useEffect(() => { + if (error === ErrorType.Empty) { + return; + } + + const timerId = setTimeout(() => { + setError(ErrorType.Empty); + }, 3000); + + return () => { + clearTimeout(timerId); + }; + }, [error, setError]); + + return ( +
+
+ ); +}; diff --git a/src/components/Filter.tsx b/src/components/Filter.tsx new file mode 100644 index 0000000000..e4d5a40e78 --- /dev/null +++ b/src/components/Filter.tsx @@ -0,0 +1,34 @@ +import classNames from 'classnames'; +import { FILTER_BY } from '../constants/constants'; +import React from 'react'; + +interface Props { + filter: string; + onFilter: (v: string) => void; +} + +export const Filter: React.FC = ({ + filter: currentFilter, + onFilter, +}) => { + const getNameFilter = (str: string) => + str.slice(0, 1).toUpperCase() + str.slice(1); + + return ( + + ); +}; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000000..e3f700ba5f --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Filter } from './Filter'; +import { Todo } from '../types/Todo'; +import { getCompletedTodos } from '../utils/methods'; + +interface Props { + sizeLeft: number; + filter: string; + onFilter: (v: string) => void; + onClear: () => void; + todos: Todo[]; +} + +export const Footer: React.FC = ({ + sizeLeft, + filter, + onFilter, + onClear, + todos, +}) => { + return ( +
+ + {`${sizeLeft} items left`} + + + + + {/* this button should be disabled if there are no completed todos */} + +
+ ); +}; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx deleted file mode 100644 index 291bee4e83..0000000000 --- a/src/components/Footer/Footer.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Todo } from '../../types/Todo'; -import classNames from 'classnames'; -import React from 'react'; -import { FilterEnum } from '../../utils/types'; - -type Props = { - todosData: Todo[]; - filter: FilterEnum; - setFilter: (filter: FilterEnum) => void; - onClearClick: () => void; - isDeleting: boolean; -}; - -export const Footer: React.FC = ({ - todosData, - filter, - setFilter, - onClearClick, - isDeleting, -}) => { - return ( - <> - {/* Hide the footer if there are no todos */} - {todosData.length > 0 && ( -
- - {todosData.filter(todo => !todo.completed).length} items left - - - {/* Active link should have the 'selected' class */} - - - {/* this button should be disabled if there are no completed todos */} - - -
- )} - - ); -}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000000..813805aadc --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,90 @@ +import classNames from 'classnames'; +import React from 'react'; +import { updateTodoCompleted } from '../api/todos'; +import { Todo } from '../types/Todo'; +import { pause } from '../utils/methods'; +import { NewTodoField } from './NewTodoField'; + +interface Props { + todos: Todo[]; + sizeLeft: number; + onTodos: React.Dispatch>; + onErrorMessage: (v: string) => void; + onMassLoader: (v: boolean) => void; + onLoader: (v: boolean) => void; + onTempTodo: (v: Todo | null) => void; +} + +export const Header: React.FC = ({ + todos, + sizeLeft, + onTodos, + onErrorMessage, + onMassLoader, + onLoader, + onTempTodo, +}) => { + const handleChangeAll = async () => { + const notCompletedTodos = todos.filter(e => !e.completed); + + try { + onMassLoader(true); + await pause(); + const todosToUpdate = + notCompletedTodos.length > 0 ? notCompletedTodos : todos; + + const updatedTodos = await Promise.all( + todosToUpdate.map(async e => { + const newTodo = await updateTodoCompleted({ + id: e.id, + completed: notCompletedTodos.length > 0 || sizeLeft !== 0, + }); + + return { + ...e, + completed: newTodo.completed, + }; + }), + ); + + onTodos(prev => + prev.map(todo => { + const updated = updatedTodos.find( + updatedTodo => updatedTodo.id === todo.id, + ); + + return updated ? updated : todo; + }), + ); + } catch { + onErrorMessage('Unable to update a todo'); + } finally { + onMassLoader(false); + } + }; + + return ( +
+ {/* this button should have `active` class only if all todos are completed */} + {todos.length > 0 && ( +
+ ); +}; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/components/MyError.tsx b/src/components/MyError.tsx new file mode 100644 index 0000000000..d20bb2761d --- /dev/null +++ b/src/components/MyError.tsx @@ -0,0 +1,44 @@ +import React, { useEffect, useRef } from 'react'; + +interface Props { + errorMessage: string; + onErrorMessage: (v: string) => void; +} + +export const MyError: React.FC = ({ errorMessage, onErrorMessage }) => { + const errorDiv = useRef(null); + const timerId = useRef(0); + + useEffect(() => { + if (errorDiv.current && errorMessage) { + errorDiv.current.classList.remove('hidden'); + window.clearTimeout(timerId.current); + timerId.current = window.setTimeout(() => { + errorDiv.current?.classList.add('hidden'); + onErrorMessage(''); + }, 3000); + } + }, [errorMessage, onErrorMessage]); + + const closeError = () => { + window.clearTimeout(timerId.current); + errorDiv.current?.classList.add('hidden'); + }; + + return ( +
+
+ ); +}; diff --git a/src/components/NewTodoField.tsx b/src/components/NewTodoField.tsx new file mode 100644 index 0000000000..5581cedd54 --- /dev/null +++ b/src/components/NewTodoField.tsx @@ -0,0 +1,84 @@ +import { FormEvent, useEffect, useRef, useState } from 'react'; +import { addTodo, USER_ID } from '../api/todos'; +import { Todo } from '../types/Todo'; +import { pause } from '../utils/methods'; + +interface Props { + onTempTodo?: (v: Todo | null) => void; + onErrorMessage?: (v: string) => void; + onLoader?: (v: boolean) => void; + onTodos?: React.Dispatch>; + titleTodo?: string; +} + +export const NewTodoField: React.FC = ({ + onErrorMessage = () => {}, + onTempTodo = () => {}, + onLoader = () => {}, + onTodos = () => {}, +}) => { + const [title, setTitle] = useState(''); + const [disabled, setDisabled] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + if (title.trim()) { + const newTodo = { + title: title.trim(), + userId: USER_ID, + completed: false, + }; + + setDisabled(true); + + try { + const tempTodo = { ...newTodo, id: 0 }; + + onTempTodo(tempTodo); + onLoader(true); + await pause(); + + const response = await addTodo(newTodo); + + onTodos(prev => [...prev, response]); + + setTitle(''); + } catch { + onErrorMessage('Unable to add a todo'); + } finally { + onTempTodo(null); + onLoader(false); + setDisabled(false); + setTimeout(() => { + inputRef.current?.focus(); + }, 0); + } + } else { + onErrorMessage('Title should not be empty'); + } + }; + + return ( +
+ setTitle(e.target.value)} + /> +
+ ); +}; diff --git a/src/components/TempTodoItem/TempTodoItem.tsx b/src/components/TempTodoItem/TempTodoItem.tsx deleted file mode 100644 index f4e24b2bd8..0000000000 --- a/src/components/TempTodoItem/TempTodoItem.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { Todo } from '../../types/Todo'; - -type Props = { - tempTodo: Todo | null; -}; - -export const TempTodoItem: React.FC = ({ tempTodo }) => { - if (!tempTodo) { - return null; - } - - const { title, completed } = tempTodo; - - return ( -
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - - - {title} - - -
-
-
-
-
- ); -}; diff --git a/src/components/TodoFooter.tsx b/src/components/TodoFooter.tsx new file mode 100644 index 0000000000..045ac864b3 --- /dev/null +++ b/src/components/TodoFooter.tsx @@ -0,0 +1,57 @@ +import React, { Dispatch, SetStateAction } from 'react'; +import { FilterStatus } from '../types/FilterStatus'; +import classNames from 'classnames'; + +type Props = { + filterStatus: FilterStatus; + setFilterStatus: Dispatch>; + todosLeft: number; + todosCompleted: number; + onClearCompleted: () => Promise; +}; + +export const TodoFooter: React.FC = props => { + const { + filterStatus, + setFilterStatus, + todosLeft, + todosCompleted, + onClearCompleted, + } = props; + + return ( +
+ + {todosLeft} items left + + + {/* Active link should have the 'selected' class */} + + + {/* this button should be disabled if there are no completed todos */} + +
+ ); +}; diff --git a/src/components/TodoHeader.tsx b/src/components/TodoHeader.tsx new file mode 100644 index 0000000000..d669209bb7 --- /dev/null +++ b/src/components/TodoHeader.tsx @@ -0,0 +1,80 @@ +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { ErrorType } from '../types/ErrorTypes'; +import classNames from 'classnames'; + +type Props = { + onAddTodo: (value: string) => Promise; + setErrorMessage: Dispatch>; + isInputDisabled: boolean; + todosLength: number; + areAllTodosCompleted: boolean; + onToggleAll: () => Promise; + inputRef: React.RefObject | null; +}; + +export const TodoHeader: React.FC = props => { + const { + onAddTodo, + setErrorMessage, + isInputDisabled, + onToggleAll, + todosLength, + areAllTodosCompleted, + inputRef, + } = props; + + const [inputValue, setInputValue] = useState(''); + + const onSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (inputValue.trim() === '') { + setErrorMessage(ErrorType.EmptyTitle); + + return; + } + + try { + await onAddTodo(inputValue.trim()); + setInputValue(''); + } catch (err) {} + }; + + useEffect(() => { + inputRef?.current?.focus(); + }, [todosLength, inputRef]); + + useEffect(() => { + if (!isInputDisabled) { + inputRef?.current?.focus(); + } + }, [isInputDisabled, inputRef]); + + return ( +
+ {todosLength !== 0 && ( +
+ ); +}; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 0000000000..f8a8397fe0 --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,133 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/*eslint-disable*/ +import React, { Dispatch, SetStateAction, useRef, useState } from 'react'; +import { Todo } from '../types/Todo'; +import classNames from 'classnames'; + +type Props = { + todo: Todo; + isLoading?: boolean; + isInEditMode?: boolean; + onRemoveTodo: (todoId: number) => Promise; + onUpdateTodo: (todo: Todo) => Promise; + setEditedTodoId: Dispatch>; +}; + +export const TodoItem: React.FC = props => { + const { + todo, + isLoading, + isInEditMode, + onRemoveTodo, + onUpdateTodo, + setEditedTodoId, + } = props; + + const [todoTitleValue, setTodoTitleValue] = useState(todo.title); + + const inputRef = useRef(null); + + const onCheckTodo = () => { + const todoToUpdate = { ...todo, completed: !todo.completed }; + + onUpdateTodo(todoToUpdate); + }; + + const onDoubleClick = () => { + setEditedTodoId(todo.id); + }; + + // eslint-disable-next-line max-len, prettier/prettier + const onBlur = async ( + // eslint-disable-next-line max-len + event: + | React.FocusEvent | React.FormEvent, + ) => { + event.preventDefault(); + const normalizedTitle = todoTitleValue.trim(); + + if (todo.title === normalizedTitle) { + setEditedTodoId(null); + + return; + } + + try { + if (normalizedTitle === '') { + await onRemoveTodo(todo.id); + } else { + await onUpdateTodo({ ...todo, title: normalizedTitle }); + } + + setEditedTodoId(null); + } catch (err) { + inputRef?.current?.focus(); + } + }; + + const onKeyUp = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setEditedTodoId(null); + setTodoTitleValue(todo.title); + } + }; + + return ( +
+ + + {isInEditMode ? ( +
+ setTodoTitleValue(e.target.value)} + onKeyUp={onKeyUp} + ref={inputRef} + /> +
+ ) : ( + <> + + {todo.title} + + + + )} + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx deleted file mode 100644 index 275e2890e4..0000000000 --- a/src/components/TodoItem/TodoItem.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import classNames from 'classnames'; -import { Todo } from '../../types/Todo'; - -type Props = { - todo: Todo | null; - onDelete: (id: number) => void; - isDeleting: boolean; - isUpdating: boolean; - isUpdatingTitle: boolean; - tempTitle: string; - setTempTitle: (newTitle: string) => void; - onDoubleClick: (id: number) => void; - setUpdatingTodoId: (newId: number | null) => void; - isServerRequest: boolean; - toggleTodo: (id: number) => void; -}; - -export const TodoItem: React.FC = ({ - todo, - onDelete, - isDeleting, - isUpdating, - isUpdatingTitle, - tempTitle, - setTempTitle, - onDoubleClick, - setUpdatingTodoId, - isServerRequest, - toggleTodo, -}) => { - if (!todo) { - return null; - } - - const { id, completed, title } = todo; - - const handleBlur = () => { - if (tempTitle.trim() === todo.title || tempTitle.trim() === '') { - setUpdatingTodoId(null); - - return; - } - - onDoubleClick(id); - }; - - const handleKeyUp = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setTempTitle(todo.title); - setUpdatingTodoId(null); - } - }; - - return ( -
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - - {isUpdatingTitle ? ( -
{ - e.preventDefault(); - onDoubleClick(id); - }} - > - setTempTitle(e.target.value)} - onBlur={() => handleBlur()} - onKeyUp={e => handleKeyUp(e)} - autoFocus - /> -
- ) : ( - <> - { - setTempTitle(title); - setUpdatingTodoId(id); - }} - > - {title} - - - - )} - -
-
-
-
-
- ); -}; diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx new file mode 100644 index 0000000000..0a7f40578e --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { TodoItem } from './TodoItem'; +import { Todo } from '../types/Todo'; + +type Props = { + filteredTodos: Todo[]; + loadingTodoIds: number[]; + tempTodo: Todo | null; + onRemoveTodo: (todoId: number) => Promise; + onUpdateTodo: (todo: Todo) => Promise; +}; + +export const TodoList: React.FC = props => { + const { + filteredTodos, + loadingTodoIds, + tempTodo, + onRemoveTodo, + onUpdateTodo, + } = props; + + const [editedTodoId, setEditedTodoId] = useState(null); + + return ( +
+ {filteredTodos.map(todo => ( + + ))} + {tempTodo && ( + + )} +
+ ); +}; diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx deleted file mode 100644 index 2a42b012e2..0000000000 --- a/src/components/TodoList/TodoList.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Todo } from '../../types/Todo'; -import { TodoItem } from '../TodoItem/TodoItem'; - -type Props = { - filteredTodos: Todo[]; - tempTodo: Todo | null; - onDelete: (id: number) => void; - deletingTodoId: number | null; - updatingTodos: number[]; - setTempTodoTitle: (newTitle: string) => void; - tempTodoTitle: string; - updatingTodo: number | null; - onDoubleClick: (id: number) => void; - setUpdatingTodoId: (newId: number | null) => void; - serverRequest: boolean; - toggleTodo: (id: number) => void; -}; - -export const TodoList: React.FC = ({ - filteredTodos, - tempTodo, - onDelete, - deletingTodoId, - updatingTodos, - setTempTodoTitle, - tempTodoTitle, - updatingTodo, - onDoubleClick, - setUpdatingTodoId, - serverRequest, - toggleTodo, -}) => { - return ( - <> - {filteredTodos.map(todo => ( - - ))} - - {tempTodo && ( - {}} - isDeleting={false} - isUpdating={false} - isUpdatingTitle={false} - tempTitle="" - setTempTitle={() => {}} - onDoubleClick={() => {}} - setUpdatingTodoId={() => {}} - isServerRequest={false} - toggleTodo={() => {}} - /> - )} - - ); -}; diff --git a/src/constants/constants.ts b/src/constants/constants.ts new file mode 100644 index 0000000000..fc97eecbe8 --- /dev/null +++ b/src/constants/constants.ts @@ -0,0 +1,5 @@ +export const FILTER_BY = { + ALL: 'all', + ACTIVE: 'active', + COMPLETED: 'completed', +}; diff --git a/src/types/ErrorTypes.ts b/src/types/ErrorTypes.ts new file mode 100644 index 0000000000..66f0a8d5ad --- /dev/null +++ b/src/types/ErrorTypes.ts @@ -0,0 +1,8 @@ +export enum ErrorType { + Empty = '', + LoadTodos = 'Unable to load todos', + EmptyTitle = 'Title should not be empty', + AddTodo = 'Unable to add a todo', + DeleteTodo = 'Unable to delete a todo', + UpdateTodo = 'Unable to update a todo', +} diff --git a/src/types/FilterStatus.ts b/src/types/FilterStatus.ts new file mode 100644 index 0000000000..7ca17f289b --- /dev/null +++ b/src/types/FilterStatus.ts @@ -0,0 +1,5 @@ +export enum FilterStatus { + All = 'All', + Active = 'Active', + Completed = 'Completed', +} diff --git a/src/utils/methods.tsx b/src/utils/methods.tsx new file mode 100644 index 0000000000..2c3c8e00af --- /dev/null +++ b/src/utils/methods.tsx @@ -0,0 +1,7 @@ +import { Todo } from '../types/Todo'; + +export const getCompletedTodos = (arr: Todo[]): Todo[] => { + return arr.filter(elem => elem.completed); +}; + +export const pause = () => new Promise(resolve => setTimeout(resolve, 300)); diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index 0dd69ce179..0000000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum FilterEnum { - All = 'All', - Active = 'Active', - Completed = 'Completed', -} - -export type ErrorObject = '' | 'Load' | 'Add' | 'Title' | 'Delete' | 'Update';