diff --git a/src/App.tsx b/src/App.tsx index 81e011f432..534fe049c0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,16 @@ -/* eslint-disable max-len */ -/* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; -import { UserWarning } from './UserWarning'; - -const USER_ID = 0; +// /* eslint-disable jsx-a11y/control-has-associated-label */ +// /* eslint-disable jsx-a11y/label-has-associated-control */ -export const App: React.FC = () => { - if (!USER_ID) { - return ; - } - - return ( -
-

- Copy all you need from the prev task: -
- - React Todo App - Add and Delete - -

+import { USER_ID } from './api/todos'; +import { Todos } from './components/Todo/Todos'; +import { TodosProvider } from './context/TodoContext'; +import { UserWarning } from './UserWarning'; -

Styles are already copied

-
+export const App = () => + !USER_ID ? ( + + ) : ( + + + ); -}; diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 0000000000..1adc65d312 --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,24 @@ +import { TODOS_API_PATH } from '../constants/api'; +import { Todo } from '../types/Todo'; +import { client } from '../utils/fetchClient'; + +export const USER_ID = 1826; + +export const getTodos = () => { + return client.get(`${TODOS_API_PATH}?userId=${USER_ID}`); +}; + +export const getTodoById = (id: number) => client.get(`/todos/${id}`); + +export const getAllCompletedTodoByUserId = (id: number) => { + client.get(`${TODOS_API_PATH}?userId=${id}&completed=true`); +}; + +export const addTodo = ({ title, completed, userId }: Omit) => + client.post(TODOS_API_PATH, { title, completed, userId }); + +export const updateTodo = (todo: Todo, id: number): Promise => + client.patch(`${TODOS_API_PATH}/${id}`, todo); + +export const deleteTodo = (id: number) => + client.delete(`${TODOS_API_PATH}/${id}`); diff --git a/src/components/ErrorNotification/ErrorNotification.tsx b/src/components/ErrorNotification/ErrorNotification.tsx new file mode 100644 index 0000000000..41d53e7453 --- /dev/null +++ b/src/components/ErrorNotification/ErrorNotification.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react'; +import { getTodoErrorsMessage } from '../../utils/todos/getTodoErrorsMessage'; +import cn from 'classnames'; +import { TodoErrors } from '../../utils/enums/TodoErrors'; + +interface ErrorNotificationProps { + error: TodoErrors | null; +} + +export const ErrorNotification: FC = ({ error }) => ( +
+
+); diff --git a/src/components/Todo/TodoFooter/TodoFooter.tsx b/src/components/Todo/TodoFooter/TodoFooter.tsx new file mode 100644 index 0000000000..6a02e2eea9 --- /dev/null +++ b/src/components/Todo/TodoFooter/TodoFooter.tsx @@ -0,0 +1,61 @@ +import { Dispatch, FC, SetStateAction } from 'react'; +import cn from 'classnames'; + +import { Todo } from '../../../types/Todo'; +import { TODO_FILTER_OPTIONS } from '../../../constants/TodoFilter'; + +import { FilterStatuses } from '../../../utils/enums/FilterStatuses'; +import { + getInCompletedTodos, + hasCompletedTodos, +} from '../../../utils/todos/getTodos'; +import { useDeleteTodo } from '../../../hooks/useDeleteTodo'; + +interface TodoFooterProps { + todos: Todo[]; + setStatus: Dispatch>; + status: FilterStatuses; +} + +export const TodoFooter: FC = ({ + todos, + setStatus, + status, +}) => { + const isCompletedTodoCounter = getInCompletedTodos(todos).length; + const { handleDeleteCompletedTodos } = useDeleteTodo(); + + return ( +
+ + {isCompletedTodoCounter} + {isCompletedTodoCounter === 1 ? ' item ' : ' items '} + left + + + + + +
+ ); +}; diff --git a/src/components/Todo/TodoForm/TodoForm.tsx b/src/components/Todo/TodoForm/TodoForm.tsx new file mode 100644 index 0000000000..2745fe7ae2 --- /dev/null +++ b/src/components/Todo/TodoForm/TodoForm.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import { TodosContext } from '../../../context/TodoContext'; +import { useTodoFormManager } from '../../../hooks/useTodoFormManager'; + +export const TodoForm = () => { + const { inputRef } = useContext(TodosContext); + const { title, handleSubmit, handleChangeTitle, isInputDisabled } = + useTodoFormManager(); + + return ( +
+ +
+ ); +}; diff --git a/src/components/Todo/TodoHeader/TodoHeader.tsx b/src/components/Todo/TodoHeader/TodoHeader.tsx new file mode 100644 index 0000000000..f7644b8a8b --- /dev/null +++ b/src/components/Todo/TodoHeader/TodoHeader.tsx @@ -0,0 +1,32 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import cn from 'classnames'; + +import { Todo } from '../../../types/Todo'; +import { isAllTodosCompleted } from '../../../utils/todos/getTodos'; +import { TodoForm } from '../TodoForm/TodoForm'; +import { useTodoFormManager } from '../../../hooks/useTodoFormManager'; + +interface TodoHeaderProps { + todos: Todo[]; +} + +export const TodoHeader: FC = ({ todos }) => { + const { handleToogleAllTodoStatus } = useTodoFormManager(); + + return ( +
+ {!!todos.length && ( +
+ ); +}; diff --git a/src/components/Todo/TodoItem/TodoItem.tsx b/src/components/Todo/TodoItem/TodoItem.tsx new file mode 100644 index 0000000000..0df082de5f --- /dev/null +++ b/src/components/Todo/TodoItem/TodoItem.tsx @@ -0,0 +1,100 @@ +import { FormEvent, forwardRef } from 'react'; +import { Todo } from '../../../types/Todo'; +import cn from 'classnames'; +import { useDeleteTodo } from '../../../hooks/useDeleteTodo'; +import { useTodoFormManager } from '../../../hooks/useTodoFormManager'; +import { useSelectedTodo } from '../../../hooks/useSelectedTodo'; + +interface TodoItemProps { + todo: Todo; + isLoading?: boolean; +} + +export const TodoItem = forwardRef( + ({ todo, isLoading = false }, ref) => { + const { completed, id, title } = todo; + const { isDeleting, handleDeleteTodo } = useDeleteTodo(); + const { selectedTodo, setSelectedTodo } = useSelectedTodo(); + const { + title: updatingTitle, + isUpdating, + setTitle, + handleUpdateTodo, + } = useTodoFormManager(todo.title); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + + if (title == updatingTitle) { + return setSelectedTodo(null); + } + + const res = await handleUpdateTodo({ ...todo, title: updatingTitle }); + + if (res) { + setSelectedTodo(null); + } + }; + + return ( +
+ {/* eslint-disable jsx-a11y/label-has-associated-control */} + + + {selectedTodo ? ( +
+ setTitle(e.target.value)} + autoFocus + /> +
+ ) : ( + { + setSelectedTodo(todo); + }} + > + {title.trim()} + + )} + {!selectedTodo && ( + + )} + +
+
+
+
+
+ ); + }, +); diff --git a/src/components/Todo/TodoList/TodoList.tsx b/src/components/Todo/TodoList/TodoList.tsx new file mode 100644 index 0000000000..5d4367f367 --- /dev/null +++ b/src/components/Todo/TodoList/TodoList.tsx @@ -0,0 +1,56 @@ +import React, { FC, useContext, useRef } from 'react'; +import { Todo } from '../../../types/Todo'; +import { TodoItem } from '../TodoItem/TodoItem'; +import { TodosContext } from '../../../context/TodoContext'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; + +interface TodoListProps { + todos: Todo[]; +} + +export const TodoList: FC = ({ todos }) => { + const { tempTodo } = useContext(TodosContext); + + const refs = useRef>>({}); + + todos.forEach(todo => { + if (!refs.current[todo.id]) { + refs.current[todo.id] = React.createRef(); + } + }); + + if (tempTodo && !refs.current[tempTodo.id]) { + refs.current[tempTodo.id] = React.createRef(); + } + + return ( +
+ + {todos.map(todo => ( + + + + ))} + {tempTodo && ( + + + + )} + +
+ ); +}; diff --git a/src/components/Todo/Todos.tsx b/src/components/Todo/Todos.tsx new file mode 100644 index 0000000000..473cad9d59 --- /dev/null +++ b/src/components/Todo/Todos.tsx @@ -0,0 +1,35 @@ +import { useContext } from 'react'; + +import { TodoHeader } from './TodoHeader/TodoHeader'; +import { TodoList } from './TodoList/TodoList'; +import { TodoFooter } from './TodoFooter/TodoFooter'; +import { ErrorNotification } from '../ErrorNotification/ErrorNotification'; +import { TodosContext } from '../../context/TodoContext'; +import { useTodoFilter } from '../../hooks/useTodoFilter'; + +export const Todos = () => { + const { todos, error } = useContext(TodosContext); + const { filtredTodos, setTodoStatus, todoStatus } = useTodoFilter(todos); + + return ( +
+

todos

+ +
+ + + + + {!!todos.length && ( + + )} +
+ + +
+ ); +}; diff --git a/src/constants/TodoFilter.ts b/src/constants/TodoFilter.ts new file mode 100644 index 0000000000..6f0c78d15d --- /dev/null +++ b/src/constants/TodoFilter.ts @@ -0,0 +1,12 @@ +import { FilterStatuses } from '../utils/enums/FilterStatuses'; + +export const TODO_FILTER_OPTIONS = [ + { value: FilterStatuses.All, title: 'All', href: '#/', id: 1 }, + { value: FilterStatuses.Active, title: 'Active', href: '#/active', id: 2 }, + { + value: FilterStatuses.Completed, + title: 'Completed', + href: '#/completed', + id: 3, + }, +]; diff --git a/src/constants/api.ts b/src/constants/api.ts new file mode 100644 index 0000000000..113936873c --- /dev/null +++ b/src/constants/api.ts @@ -0,0 +1 @@ +export const TODOS_API_PATH = '/todos'; diff --git a/src/context/TodoContext.tsx b/src/context/TodoContext.tsx new file mode 100644 index 0000000000..3984ec9d69 --- /dev/null +++ b/src/context/TodoContext.tsx @@ -0,0 +1,80 @@ +import { createContext, ReactNode, useMemo, RefObject } from 'react'; +import { TodoErrors } from '../utils/enums/TodoErrors'; +import { Todo } from '../types/Todo'; +import { useTodoInput } from '../hooks/useTodoInput'; +import { useTodoErrors } from '../hooks/useTodoErrors'; +import { useTodos } from '../hooks/useTodos'; + +interface ITodosContext { + todos: Todo[]; + error: TodoErrors | null; + tempTodo: Todo | null; + inputRef: RefObject | null; + + onFocus: () => void; + fetchTodos: () => void; + addTodo: (title: string) => Promise; + deleteTodo: (todoId: number) => Promise; + deleteCompletedTodos: () => Promise; + updateTodo: (todo: Todo) => Promise; + updatedAllTodo: () => Promise; + showError: (err: TodoErrors) => void; +} + +export const TodosContext = createContext({ + todos: [], + error: null, + tempTodo: null, + inputRef: null, + + fetchTodos: () => {}, + addTodo: async () => {}, + deleteTodo: async () => {}, + deleteCompletedTodos: async () => {}, + updateTodo: async () => {}, + updatedAllTodo: async () => {}, + showError: () => {}, + onFocus: () => {}, +}); + +export const TodosProvider = ({ + children, +}: { + children: ReactNode; +}): ReactNode => { + const { error, showError } = useTodoErrors(); + const { inputRef, onFocus } = useTodoInput(); + const { + todos, + tempTodo, + fetchTodos, + addTodo, + deleteTodo, + deleteCompletedTodos, + updateTodo, + updatedAllTodo, + } = useTodos(showError); + + const store = useMemo( + () => ({ + todos, + tempTodo, + error, + inputRef, + + fetchTodos, + addTodo, + deleteTodo, + showError, + deleteCompletedTodos, + updateTodo, + updatedAllTodo, + onFocus, + }), + [todos, error, tempTodo, inputRef], + ); + + return ( + {children} + ); +}; diff --git a/src/hooks/useDeleteTodo.ts b/src/hooks/useDeleteTodo.ts new file mode 100644 index 0000000000..dd9dcef2d3 --- /dev/null +++ b/src/hooks/useDeleteTodo.ts @@ -0,0 +1,26 @@ +import { useContext, useState } from 'react'; + +import { TodosContext } from '../context/TodoContext'; + +export const useDeleteTodo = () => { + const [isDeleting, setDeleting] = useState(false); + const { deleteCompletedTodos, onFocus, deleteTodo } = + useContext(TodosContext); + + const handleDeleteTodo = async (id: number) => { + setDeleting(true); + + await deleteTodo(id); + + setDeleting(false); + onFocus(); + }; + + const handleDeleteCompletedTodos = async () => { + await deleteCompletedTodos(); + + onFocus(); + }; + + return { handleDeleteTodo, isDeleting, handleDeleteCompletedTodos }; +}; diff --git a/src/hooks/useSelectedTodo.ts b/src/hooks/useSelectedTodo.ts new file mode 100644 index 0000000000..d31dc9a963 --- /dev/null +++ b/src/hooks/useSelectedTodo.ts @@ -0,0 +1,21 @@ +import { useLayoutEffect, useState } from 'react'; +import { Todo } from '../types/Todo'; + +export const useSelectedTodo = () => { + const [selectedTodo, setSelectedTodo] = useState(null); + + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Escape' && selectedTodo) { + setSelectedTodo(null); + } + }; + useLayoutEffect(() => { + window.addEventListener('keydown', handleKeyPress); + + return () => { + window.removeEventListener('keydown', handleKeyPress); + }; + }, [selectedTodo]); + + return { selectedTodo, setSelectedTodo }; +}; diff --git a/src/hooks/useTodoErrors.ts b/src/hooks/useTodoErrors.ts new file mode 100644 index 0000000000..a698a4ff81 --- /dev/null +++ b/src/hooks/useTodoErrors.ts @@ -0,0 +1,13 @@ +import { useState } from 'react'; +import { TodoErrors } from '../utils/enums/TodoErrors'; + +export const useTodoErrors = () => { + const [error, setError] = useState(null); + + const showError = (err: TodoErrors) => { + setError(err); + setTimeout(() => setError(null), 3000); + }; + + return { error, showError }; +}; diff --git a/src/hooks/useTodoFilter.ts b/src/hooks/useTodoFilter.ts new file mode 100644 index 0000000000..fb5b5b6640 --- /dev/null +++ b/src/hooks/useTodoFilter.ts @@ -0,0 +1,17 @@ +import { useMemo, useState } from 'react'; +import { FilterStatuses } from '../utils/enums/FilterStatuses'; +import { getFiltredTodo } from '../utils/todos/filterTodo'; +import { Todo } from '../types/Todo'; + +export const useTodoFilter = (todos: Todo[]) => { + const [todoStatus, setTodoStatus] = useState( + FilterStatuses.All, + ); + + const filtredTodos = useMemo( + () => getFiltredTodo(todos, todoStatus), + [todoStatus, todos], + ); + + return { filtredTodos, setTodoStatus, todoStatus }; +}; diff --git a/src/hooks/useTodoFormManager.ts b/src/hooks/useTodoFormManager.ts new file mode 100644 index 0000000000..cdb0b4e119 --- /dev/null +++ b/src/hooks/useTodoFormManager.ts @@ -0,0 +1,88 @@ +import { useState, useContext, useLayoutEffect } from 'react'; +import { TodosContext } from '../context/TodoContext'; +import { Todo } from '../types/Todo'; +import { isOnlyWhiteSpace } from '../utils/string/isOnlyWhiteSpace'; +import { TodoErrors } from '../utils/enums/TodoErrors'; + +export const useTodoFormManager = (initialTitle = '') => { + const { + addTodo, + updateTodo, + updatedAllTodo, + showError, + onFocus, + deleteTodo, + } = useContext(TodosContext); + const [title, setTitle] = useState(initialTitle); + const [isInputDisabled, setInputDisabled] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + + useLayoutEffect(() => { + if (!isInputDisabled) { + onFocus(); + } + }, [isInputDisabled]); + + const handleAddTodo = async () => { + setInputDisabled(true); + + try { + await addTodo(title.trim()); + setTitle(''); + } catch { + showError(TodoErrors.add); + } finally { + setInputDisabled(false); + } + }; + + const handleUpdateTodo = async (todo: Todo) => { + setIsUpdating(true); + + try { + if (todo.title.trim()) { + const updatedTodo = await updateTodo(todo); + setIsUpdating(false); + + return updatedTodo; + } else { + await deleteTodo(todo.id); + } + } catch (err) { + showError(TodoErrors.update); + } + + setIsUpdating(false); + }; + + const handleToogleAllTodoStatus = async () => { + await updatedAllTodo(); + }; + + const handleChangeTitle = (e: React.ChangeEvent) => { + setTitle(e.target.value); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (!title || isOnlyWhiteSpace(title)) { + showError(TodoErrors.title); + return; + } + + handleAddTodo(); + }; + + return { + title, + isInputDisabled, + isUpdating, + setTitle, + handleSubmit, + handleChangeTitle, + handleAddTodo, + handleUpdateTodo, + handleToogleAllTodoStatus, + }; +}; diff --git a/src/hooks/useTodoInput.ts b/src/hooks/useTodoInput.ts new file mode 100644 index 0000000000..e38feff344 --- /dev/null +++ b/src/hooks/useTodoInput.ts @@ -0,0 +1,11 @@ +import { useRef } from 'react'; + +export const useTodoInput = () => { + const inputRef = useRef(null); + + const onFocus = () => { + inputRef.current?.focus(); + }; + + return { inputRef, onFocus }; +}; diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts new file mode 100644 index 0000000000..4981d78ca1 --- /dev/null +++ b/src/hooks/useTodos.ts @@ -0,0 +1,126 @@ +import { useLayoutEffect, useState } from 'react'; +import { Todo } from '../types/Todo'; +import * as todoApi from '../api/todos'; +import { TodoErrors } from '../utils/enums/TodoErrors'; +import { + getCompletedTodos, + getInCompletedTodos, + hasInCompletedTodos, +} from '../utils/todos/getTodos'; +import { revomesTodosById } from '../utils/todos/removeTodos'; +import { validTodoIds, validUpdatedTodos } from '../utils/todos/validationTodo'; +import { updateTodosCompleted } from '../utils/todos/updateTodo'; + +export const useTodos = (showError: (err: TodoErrors) => void) => { + const [todos, setTodos] = useState([]); + const [tempTodo, setTempTodo] = useState(null); + + const fetchTodos = async () => { + try { + const data = await todoApi.getTodos(); + setTodos(data); + } catch { + showError(TodoErrors.load); + } + }; + + const addTodo = async (title: string): Promise => { + try { + const todo = { title, userId: todoApi.USER_ID, completed: false }; + + setTempTodo({ ...todo, id: 0 }); + + const newTodo = await todoApi.addTodo(todo); + + setTodos(prevState => [...prevState, newTodo]); + setTempTodo(null); + } catch (err) { + showError(TodoErrors.add); + setTempTodo(null); + throw err; + } + }; + + const deleteTodo = async (todoId: number): Promise => { + try { + await todoApi.deleteTodo(todoId); + + setTodos(current => current.filter(({ id }) => id !== todoId)); + + return todoId; + } catch { + showError(TodoErrors.delete); + } + }; + + const deleteCompletedTodos = async () => { + const completedTodos = getCompletedTodos(todos); + + const res = await Promise.all( + completedTodos.map(({ id }) => deleteTodo(id)), + ); + + const todoIds = validTodoIds(res); + + if (!!validTodoIds.length) { + setTodos(revomesTodosById(todos, todoIds)); + } + }; + + const updateTodo = async (todo: Todo): Promise => { + try { + const updatedTodo: Todo = await todoApi.updateTodo(todo, todo.id); + + setTodos(prevState => + prevState.map(existingTodo => + existingTodo.id === todo.id ? todo : existingTodo, + ), + ); + + return updatedTodo; + } catch { + showError(TodoErrors.update); + } + }; + + const updatedAllTodo = async (): Promise => { + const isIncompletedTodo = hasInCompletedTodos(todos); + + const newTodos = isIncompletedTodo + ? updateTodosCompleted(getInCompletedTodos(todos)) + : updateTodosCompleted(todos); + + const res = await Promise.all(newTodos.map(todo => updateTodo(todo))); + + const updatedTodos = validUpdatedTodos(res); + + if (!!updatedTodos.length) { + setTodos(prevState => { + const updatedState = prevState.map(todo => { + const updatedTodo = updatedTodos.find( + updated => updated.id === todo.id, + ); + + return updatedTodo ? updatedTodo : todo; + }); + + return updatedState; + }); + } + }; + + useLayoutEffect(() => { + fetchTodos(); + }, []); + + return { + todos, + tempTodo, + fetchTodos, + addTodo, + deleteTodo, + deleteCompletedTodos, + updateTodo, + updatedAllTodo, + }; +}; diff --git a/src/styles/index.scss b/src/styles/index.scss index bccd80c8bc..a34eec7c6c 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -20,6 +20,6 @@ body { pointer-events: none; } -@import "./todoapp"; -@import "./todo"; -@import "./filter"; +@import './todoapp'; +@import './todo'; +@import './filter'; diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 0000000000..3f52a5fdde --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,6 @@ +export interface Todo { + id: number; + userId: number; + title: string; + completed: boolean; +} diff --git a/src/utils/enums/FilterStatuses.ts b/src/utils/enums/FilterStatuses.ts new file mode 100644 index 0000000000..bb9a642ef7 --- /dev/null +++ b/src/utils/enums/FilterStatuses.ts @@ -0,0 +1,5 @@ +export enum FilterStatuses { + All = 'all', + Active = 'active', + Completed = 'completed', +} diff --git a/src/utils/enums/TodoErrors.ts b/src/utils/enums/TodoErrors.ts new file mode 100644 index 0000000000..5b0735cff6 --- /dev/null +++ b/src/utils/enums/TodoErrors.ts @@ -0,0 +1,7 @@ +export enum TodoErrors { + load = 'load', + title = 'title', + add = 'add', + delete = 'delete', + update = 'update', +} diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts new file mode 100644 index 0000000000..708ac4c17b --- /dev/null +++ b/src/utils/fetchClient.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +const BASE_URL = 'https://mate.academy/students-api'; + +// returns a promise resolved after a given delay +function wait(delay: number) { + return new Promise(resolve => { + setTimeout(resolve, delay); + }); +} + +// To have autocompletion and avoid mistypes +type RequestMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'; + +function request( + url: string, + method: RequestMethod = 'GET', + data: any = null, // we can send any data to the server +): Promise { + const options: RequestInit = { method }; + + if (data) { + // We add body and Content-Type only for the requests with data + options.body = JSON.stringify(data); + options.headers = { + 'Content-Type': 'application/json; charset=UTF-8', + }; + } + + // DON'T change the delay it is required for tests + return wait(100) + .then(() => fetch(BASE_URL + url, options)) + .then(response => { + if (!response.ok) { + throw new Error(); + } + + return response.json(); + }); +} + +export const client = { + get: (url: string) => request(url), + post: (url: string, data: any) => request(url, 'POST', data), + patch: (url: string, data: any) => request(url, 'PATCH', data), + delete: (url: string) => request(url, 'DELETE'), +}; diff --git a/src/utils/string/isOnlyWhiteSpace.ts b/src/utils/string/isOnlyWhiteSpace.ts new file mode 100644 index 0000000000..5ff1d96102 --- /dev/null +++ b/src/utils/string/isOnlyWhiteSpace.ts @@ -0,0 +1,2 @@ +export const isOnlyWhiteSpace = (string: string): boolean => + /^\s*$/.test(string); diff --git a/src/utils/todos/filterTodo.ts b/src/utils/todos/filterTodo.ts new file mode 100644 index 0000000000..ba4d0b3646 --- /dev/null +++ b/src/utils/todos/filterTodo.ts @@ -0,0 +1,14 @@ +import { Todo } from '../../types/Todo'; +import { FilterStatuses } from '../enums/FilterStatuses'; + +export const getFiltredTodo = (todos: Todo[], status: FilterStatuses) => + todos.filter(({ completed }) => { + switch (status) { + case FilterStatuses.Active: + return !completed; + case FilterStatuses.Completed: + return completed; + default: + return true; + } + }); diff --git a/src/utils/todos/getTodoErrorsMessage.ts b/src/utils/todos/getTodoErrorsMessage.ts new file mode 100644 index 0000000000..33cf878bcb --- /dev/null +++ b/src/utils/todos/getTodoErrorsMessage.ts @@ -0,0 +1,18 @@ +import { TodoErrors } from '../enums/TodoErrors'; + +export const getTodoErrorsMessage = (error: TodoErrors) => { + switch (error) { + case TodoErrors.load: + return 'Unable to load todos'; + case TodoErrors.title: + return 'Title should not be empty'; + case TodoErrors.add: + return 'Unable to add a todo'; + case TodoErrors.delete: + return 'Unable to delete a todo'; + case TodoErrors.update: + return 'Unable to update a todo'; + default: + return 'An unexpected error'; + } +}; diff --git a/src/utils/todos/getTodos.ts b/src/utils/todos/getTodos.ts new file mode 100644 index 0000000000..e2e8d485e3 --- /dev/null +++ b/src/utils/todos/getTodos.ts @@ -0,0 +1,16 @@ +import { Todo } from '../../types/Todo'; + +export const getInCompletedTodos = (todos: Todo[]) => + todos.filter(({ completed }) => !completed); + +export const getCompletedTodos = (todos: Todo[]) => + todos.filter(({ completed }) => completed); + +export const isAllTodosCompleted = (todos: Todo[]) => + todos.every(({ completed }) => completed); + +export const hasCompletedTodos = (todos: Todo[]) => + todos.some(({ completed }) => completed); + +export const hasInCompletedTodos = (todos: Todo[]) => + todos.some(({ completed }) => completed === false); diff --git a/src/utils/todos/removeTodos.ts b/src/utils/todos/removeTodos.ts new file mode 100644 index 0000000000..e17c15da69 --- /dev/null +++ b/src/utils/todos/removeTodos.ts @@ -0,0 +1,4 @@ +import { Todo } from '../../types/Todo'; + +export const revomesTodosById = (todos: Todo[], ids: number[]) => + todos.filter(({ id }) => !ids.includes(id)); diff --git a/src/utils/todos/updateTodo.ts b/src/utils/todos/updateTodo.ts new file mode 100644 index 0000000000..bd43eb2cdf --- /dev/null +++ b/src/utils/todos/updateTodo.ts @@ -0,0 +1,4 @@ +import { Todo } from '../../types/Todo'; + +export const updateTodosCompleted = (todos: Todo[]) => + todos.map(todo => ({ ...todo, completed: !todo.completed })); diff --git a/src/utils/todos/validationTodo.ts b/src/utils/todos/validationTodo.ts new file mode 100644 index 0000000000..57b7ba18db --- /dev/null +++ b/src/utils/todos/validationTodo.ts @@ -0,0 +1,7 @@ +import { Todo } from '../../types/Todo'; + +export const validUpdatedTodos = (updatedTodos: (Todo | void)[]): Todo[] => + updatedTodos.filter((todo): todo is Todo => todo !== undefined); + +export const validTodoIds = (todoIds: (number | void)[]): number[] => + todoIds.filter((id): id is number => id !== undefined);