diff --git a/README.md b/README.md index af7dae81f6..30cad51906 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,5 @@ # React Todo App with API (complete) -It is the third part of the React Todo App with API. +[Todo App](https://Koliras.github.io/react_todo-app-with-api/) -Take your code implemented for [Add and Delete](https://github.com/mate-academy/react_todo-app-add-and-delete) -and implement the ability to toggle and rename todos. - -> Here is [the working example](https://mate-academy.github.io/react_todo-app-with-api/) - -## Toggling a todo status - -Toggle the `completed` status on `TodoStatus` change: - -- covered the todo with a loader overlay while waiting for API response; -- the status should be changed on success; -- show the `Unable to update a todo` notification in case of API error. - -Add the ability to toggle the completed status of all the todos with the `toggleAll` checkbox: - -- `toggleAll` button should have `active` class only if all the todos are completed; -- `toggleAll` click changes its status to the opposite one, and sets this new status to all the todos; -- it should work the same as several individual updates of the todos which statuses were actually changed; -- do send requests for the todos that were not changed; - -## Renaming a todo - -Implement the ability to edit a todo title on double click: - -- show the edit form instead of the title and remove button; -- saves changes on the form submit (just press `Enter`); -- save changes when the field loses focus (`onBlur`); -- if the new title is the same as the old one just cancel editing; -- cancel editing on `Esс` key `keyup` event; -- if the new title is empty delete the todo the same way the `x` button does it; -- if the title was changed show the loader while waiting for the API response; -- update the todo title on success; -- show `Unable to update a todo` in case of API error; -- or the deletion error message if we tried to delete the todo. - -## Instructions - -- Implement a solution following the [React task guideline](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline). -- Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript). -- Replace `` with your Github username in the [DEMO LINK](https://.github.io/react_todo-app-with-api/) and add it to the PR description. +Used technologies: Javascript, Typescript, HTML, CSS, Sass, React.js, Bulma diff --git a/src/App.tsx b/src/App.tsx index 5749bdf784..98f48b0d72 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,14 @@ -/* eslint-disable max-len */ /* eslint-disable jsx-a11y/control-has-associated-label */ import React from 'react'; import { UserWarning } from './UserWarning'; - -const USER_ID = 0; +import { TodoApp } from './components/TodoApp/TodoApp'; +import { + TodosContextProvider, +} from './components/TodosContextProvider/TodosContextProvider'; +import { USER_ID } from './utils/UserId'; +import { + ErrorContextProvider, +} from './components/ErrorContextProvider/ErrorContextProvider'; export const App: React.FC = () => { if (!USER_ID) { @@ -11,14 +16,10 @@ export const App: React.FC = () => { } return ( -
-

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

- -

Styles are already copied

-
+ + + + + ); }; diff --git a/src/api/todos.ts b/src/api/todos.ts new file mode 100644 index 0000000000..d243734a2e --- /dev/null +++ b/src/api/todos.ts @@ -0,0 +1,18 @@ +import { Todo } from '../types/Todo'; +import { client } from '../utils/fetchClient'; + +export const getTodos = (userId: number) => { + return client.get(`/todos?userId=${userId}`); +}; + +export const postTodo = (data: Omit) => { + return client.post('/todos', data); +}; + +export const deleteTodo = (todoId: number) => { + return client.delete(`/todos/${todoId}`); +}; + +export const updateTodo = (todoId: number, changes: object) => { + return client.patch(`/todos/${todoId}`, changes); +}; diff --git a/src/components/ErrorContextProvider/ErrorContextProvider.tsx b/src/components/ErrorContextProvider/ErrorContextProvider.tsx new file mode 100644 index 0000000000..47f6f34070 --- /dev/null +++ b/src/components/ErrorContextProvider/ErrorContextProvider.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import { ErrorMessage } from '../../types/ErrorMessage'; + +export const ErrorContext = React.createContext({ + errorMessage: ErrorMessage.None, + setErrorMessage: () => {}, + onNewError: () => {}, +} as ErrorContextProps); + +type ErrorContextProps = { + errorMessage: ErrorMessage, + setErrorMessage: React.Dispatch>, + onNewError: (error: ErrorMessage) => void, +}; + +type Props = { + children: React.ReactNode, +}; + +export const ErrorContextProvider: React.FC = ({ children }) => { + const [errorMessage, setErrorMessage] = useState( + ErrorMessage.None, + ); + + const onNewError = (error: ErrorMessage) => { + setErrorMessage(error); + + setTimeout(() => { + setErrorMessage(ErrorMessage.None); + }, 3000); + }; + + const initialValue = { + errorMessage, + setErrorMessage, + onNewError, + }; + + return ( + + {children} + + ); +}; diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx new file mode 100644 index 0000000000..83ccfd37c6 --- /dev/null +++ b/src/components/Filter/Filter.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import classNames from 'classnames'; +import { FilterKey } from '../../types/FilterKey'; + +type Props = { + filterKey: FilterKey, + onClick: (key: FilterKey) => void +}; + +export const Filter: React.FC = ({ filterKey, onClick }) => { + return ( + + ); +}; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000000..287781b936 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,65 @@ +import React, { useContext } from 'react'; +import classNames from 'classnames'; +import { TodosContext } from '../TodosContextProvider/TodosContextProvider'; +import { ErrorMessage } from '../../types/ErrorMessage'; +import { deleteTodo } from '../../api/todos'; +import { ErrorContext } from '../ErrorContextProvider/ErrorContextProvider'; +import { Filter } from '../Filter/Filter'; +import { FilterKey } from '../../types/FilterKey'; + +type Props = { + filterKey: FilterKey, + setFilterKey: React.Dispatch>, +}; + +export const Footer: React.FC = ({ filterKey, setFilterKey }) => { + const { todos, setTodos, setTodoIdsWithLoader } = useContext(TodosContext); + const { onNewError, setErrorMessage } = useContext(ErrorContext); + const activeTodos = todos.filter(({ completed }) => !completed); + const hasCompletedTodo = todos.some(({ completed }) => completed); + + const handleCompletedTodosDelete = () => { + const completedTodos = todos.filter(({ completed }) => completed); + + setTodoIdsWithLoader(prevTodoIds => { + return [...prevTodoIds, ...completedTodos.map(({ id }) => id)]; + }); + setErrorMessage(ErrorMessage.None); + + Promise.all(completedTodos.map(todo => { + return deleteTodo(todo.id) + .then(() => { + setTodos(prevTodos => prevTodos.filter(({ id }) => id !== todo.id)); + }) + .catch(() => onNewError(ErrorMessage.UnableDelete)) + .finally(() => setTodoIdsWithLoader( + prevTodoIds => prevTodoIds.filter((id) => todo.id !== id), + )); + })); + }; + + return ( +
+ + {activeTodos.length === 1 + ? '1 item left' + : `${activeTodos.length} items left`} + + + +
+ ); +}; diff --git a/src/components/NewTodo/NewTodo.tsx b/src/components/NewTodo/NewTodo.tsx new file mode 100644 index 0000000000..8ef5bd2f2f --- /dev/null +++ b/src/components/NewTodo/NewTodo.tsx @@ -0,0 +1,78 @@ +import React, { + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { Todo } from '../../types/Todo'; +import { postTodo } from '../../api/todos'; +import { TodosContext } from '../TodosContextProvider/TodosContextProvider'; +import { USER_ID } from '../../utils/UserId'; +import { ErrorContext } from '../ErrorContextProvider/ErrorContextProvider'; +import { ErrorMessage } from '../../types/ErrorMessage'; + +type Props = { + setTempTodo: (todo: Todo | null) => void, + tempTodo: Todo | null, +}; + +export const NewTodo: React.FC = ({ + setTempTodo, tempTodo, +}) => { + const { onNewError, setErrorMessage } = useContext(ErrorContext); + const { setTodos, todos } = useContext(TodosContext); + const [newTodoTitle, setNewTodoTitle] = useState(''); + const titleInput = useRef(null); + + useEffect(() => { + if (titleInput.current) { + titleInput.current.focus(); + } + }, [todos, tempTodo]); + + const handleAddingTodo = (event: React.FormEvent) => { + event.preventDefault(); + + if (!newTodoTitle.trim()) { + onNewError(ErrorMessage.EmptyTitleRecieved); + + return; + } + + const newTodo: Omit = { + userId: USER_ID, + title: newTodoTitle.trim(), + completed: false, + }; + + setErrorMessage(ErrorMessage.None); + + postTodo(newTodo) + .then((response) => { + setNewTodoTitle(''); + setTodos((prevTodos) => [...prevTodos, response]); + }) + .catch(() => onNewError(ErrorMessage.UnableAdd)) + .finally(() => setTempTodo(null)); + + setTempTodo({ + ...newTodo, + id: 0, + }); + }; + + return ( +
+ setNewTodoTitle(target.value)} + disabled={!!tempTodo} + /> +
+ ); +}; diff --git a/src/components/TodoApp/TodoApp.tsx b/src/components/TodoApp/TodoApp.tsx new file mode 100644 index 0000000000..e02a6cf8c9 --- /dev/null +++ b/src/components/TodoApp/TodoApp.tsx @@ -0,0 +1,106 @@ +import { + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import classNames from 'classnames'; +import { NewTodo } from '../NewTodo/NewTodo'; +import { TodoList } from '../TodoList/TodoList'; +import { + TodosContext, +} from '../TodosContextProvider/TodosContextProvider'; +import { TodoError } from '../TodoError/TodoError'; +import { getTodos, updateTodo } from '../../api/todos'; +import { FilterKey } from '../../types/FilterKey'; +import { Todo } from '../../types/Todo'; +import { USER_ID } from '../../utils/UserId'; +import { ErrorContext } from '../ErrorContextProvider/ErrorContextProvider'; +import { ErrorMessage } from '../../types/ErrorMessage'; +import { getFilteredTodos } from '../../utils/getFilteredTodos'; +import { Footer } from '../Footer/Footer'; + +export const TodoApp = () => { + const { onNewError, setErrorMessage } = useContext(ErrorContext); + const { todos, setTodos, setTodoIdsWithLoader } = useContext(TodosContext); + const [filterKey, setFilterKey] = useState(FilterKey.All); + const [tempTodo, setTempTodo] = useState(null); + + const isAllTodosCompleted = todos.every(({ completed }) => completed); + const visibleTodos = useMemo( + () => getFilteredTodos(filterKey, todos), + [todos, filterKey], + ); + + useEffect(() => { + getTodos(USER_ID) + .then(setTodos) + .catch(() => onNewError(ErrorMessage.UnableLoad)); + }, []); + + const handleAllTodosToggle = () => { + const todosToUpdate = todos.filter( + ({ completed }) => completed === isAllTodosCompleted, + ); + + setTodoIdsWithLoader( + prevTodoIds => [...prevTodoIds, ...todosToUpdate.map(({ id }) => id)], + ); + setErrorMessage(ErrorMessage.None); + + Promise.all(todosToUpdate.map(todo => { + return updateTodo(todo.id, { completed: !todo.completed }) + .then(() => { + const todosCopy = [...todos]; + const searchedTodo = todos.find( + ({ id }) => todo.id === id, + ) as Todo; + + searchedTodo.completed = !searchedTodo.completed; + + setTodos(todosCopy); + }) + .catch(() => onNewError(ErrorMessage.UnableUpdate)) + .finally(() => setTodoIdsWithLoader( + prevTodoIds => prevTodoIds.filter((todoId) => todoId !== todo.id), + )); + })); + }; + + return ( +
+

todos

+
+
+ {!!todos.length && ( +
+ + + + {!!todos.length && ( +
+ )} + + +
+
+ ); +}; diff --git a/src/components/TodoError/TodoError.tsx b/src/components/TodoError/TodoError.tsx new file mode 100644 index 0000000000..6ce32cdd92 --- /dev/null +++ b/src/components/TodoError/TodoError.tsx @@ -0,0 +1,30 @@ +import classNames from 'classnames'; +import { useContext } from 'react'; +import { ErrorContext } from '../ErrorContextProvider/ErrorContextProvider'; +import { ErrorMessage } from '../../types/ErrorMessage'; + +export const TodoError = () => { + const { setErrorMessage, errorMessage } = useContext(ErrorContext); + + return ( +
+
+ ); +}; diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx new file mode 100644 index 0000000000..82431c8a56 --- /dev/null +++ b/src/components/TodoItem/TodoItem.tsx @@ -0,0 +1,191 @@ +import React, { + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import classNames from 'classnames'; +import { Todo } from '../../types/Todo'; +import { deleteTodo, updateTodo } from '../../api/todos'; +import { TodosContext } from '../TodosContextProvider/TodosContextProvider'; +import { ErrorContext } from '../ErrorContextProvider/ErrorContextProvider'; +import { ErrorMessage } from '../../types/ErrorMessage'; + +type Props = { + todo: Todo, +}; + +export const TodoItem: React.FC = ({ todo }) => { + const { onNewError, setErrorMessage } = useContext(ErrorContext); + const { + todos, + setTodos, + todoIdsWithLoader, + setTodoIdsWithLoader, + } = useContext(TodosContext); + const { title, completed, id } = todo; + const [isBeingEdited, setIsBeingEdited] = useState(false); + const [newTitle, setNewTitle] = useState(title); + const editedInputRef = useRef(null); + + useEffect(() => { + if (editedInputRef.current) { + editedInputRef.current.focus(); + } + }, [isBeingEdited]); + + const handleTodoDelete = () => { + setTodoIdsWithLoader(prevTodoIds => [...prevTodoIds, id]); + setErrorMessage(ErrorMessage.None); + deleteTodo(id) + .then(() => { + setTodos(prevTodos => prevTodos + .filter(({ id: todoId }) => id !== todoId)); + setIsBeingEdited(false); + }) + .catch(() => onNewError(ErrorMessage.UnableDelete)) + .finally(() => setTodoIdsWithLoader( + prevTodoIds => prevTodoIds.filter((todoId) => todoId !== id), + )); + }; + + const handleChangeOfTitle = (( + event: React.ChangeEvent, + ) => { + setNewTitle(event.target.value); + }); + + const handleNewTitleSubmit = ( + event?: React.FormEvent + | React.FocusEvent, + ) => { + event?.preventDefault(); + const trimmedTitle = newTitle.trim(); + + if (!trimmedTitle) { + handleTodoDelete(); + + return; + } + + if (trimmedTitle === title) { + setIsBeingEdited(false); + + return; + } + + setTodoIdsWithLoader(prevTodoIds => [...prevTodoIds, id]); + setErrorMessage(ErrorMessage.None); + + updateTodo(id, { title: trimmedTitle }) + .then(() => { + const todosCopy = [...todos]; + const searchedTodo = todos.find( + ({ id: todoId }) => todoId === id, + ) as Todo; + + searchedTodo.title = trimmedTitle; + + setTodos(todosCopy); + setIsBeingEdited(false); + }) + .catch(() => onNewError(ErrorMessage.UnableUpdate)) + .finally(() => { + setTodoIdsWithLoader( + prevTodoIds => prevTodoIds.filter((todoId) => todoId !== id), + ); + }); + }; + + const handleKeyUp = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setIsBeingEdited(false); + setNewTitle(title); + } + }; + + const handleTodoToggle = (event: React.ChangeEvent) => { + setTodoIdsWithLoader(prevTodoIds => [...prevTodoIds, id]); + setErrorMessage(ErrorMessage.None); + updateTodo(id, { completed: !completed }) + .then(() => { + const todosCopy = [...todos]; + const searchedTodo = todos.find( + ({ id: todoId }) => todoId === id, + ) as Todo; + + searchedTodo.completed = !event.target.checked; + + setTodos(todosCopy); + }) + .catch(() => onNewError(ErrorMessage.UnableUpdate)) + .finally(() => setTodoIdsWithLoader( + prevTodoIds => prevTodoIds.filter((todoId) => todoId !== id), + )); + }; + + return ( +
+ + + {isBeingEdited + ? ( +
+ +
+ ) : ( + <> + setIsBeingEdited(true)} + > + {title} + + + + )} + +
+
+
+
+
+ ); +}; diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx new file mode 100644 index 0000000000..6428851358 --- /dev/null +++ b/src/components/TodoList/TodoList.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { TodoItem } from '../TodoItem/TodoItem'; +import { Todo } from '../../types/Todo'; + +type Props = { + todos: Todo[] + tempTodo: Todo | null, +}; + +export const TodoList: React.FC = ({ todos, tempTodo }) => { + return ( +
+ {todos.map(todo => ( + + ))} + + {tempTodo && } +
+ ); +}; diff --git a/src/components/TodosContextProvider/TodosContextProvider.tsx b/src/components/TodosContextProvider/TodosContextProvider.tsx new file mode 100644 index 0000000000..6d68b57391 --- /dev/null +++ b/src/components/TodosContextProvider/TodosContextProvider.tsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react'; +import { Todo } from '../../types/Todo'; + +export const TodosContext = React.createContext({ + todos: [], + setTodos: () => {}, + todoIdsWithLoader: [], + setTodoIdsWithLoader: () => {}, +} as TodosContextProps); + +type TodosContextProps = { + todos: Todo[], + setTodos: React.Dispatch>, + todoIdsWithLoader: number[], + setTodoIdsWithLoader: React.Dispatch>, +}; + +type Props = { + children: React.ReactNode, +}; + +export const TodosContextProvider: React.FC = ({ children }) => { + const [todos, setTodos] = useState([]); + const [todoIdsWithLoader, setTodoIdsWithLoader] = useState([]); + + const initialValue = { + todos, + setTodos, + todoIdsWithLoader, + setTodoIdsWithLoader, + }; + + return ( + + {children} + + ); +}; diff --git a/src/styles/index.scss b/src/styles/index.scss index bccd80c8bc..ef04476260 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -18,6 +18,7 @@ body { min-height: 0; opacity: 0; pointer-events: none; + padding: 0; } @import "./todoapp"; diff --git a/src/styles/todo.scss b/src/styles/todo.scss index c7f93ff6b9..fc99714ef2 100644 --- a/src/styles/todo.scss +++ b/src/styles/todo.scss @@ -8,7 +8,7 @@ font-size: 24px; line-height: 1.4em; border-bottom: 1px solid #ededed; - + &:last-child { border-bottom: 0; } @@ -30,7 +30,7 @@ &__title { padding: 12px 15px; - + word-break: break-all; transition: color 0.4s; } @@ -56,7 +56,7 @@ border: 0; background: none; cursor: pointer; - + transform: translateY(-2px); opacity: 0; transition: color 0.2s ease-out; @@ -65,7 +65,7 @@ color: #af5b5e; } } - + &:hover &__remove { opacity: 1; } @@ -73,13 +73,13 @@ &__title-field { width: 100%; padding: 11px 14px; - + font-size: inherit; line-height: inherit; font-family: inherit; font-weight: inherit; color: inherit; - + border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); @@ -92,58 +92,11 @@ .overlay { position: absolute; - top: 0; left: 0; right: 0; - height: 58px; + bottom: 0; + top: 0; opacity: 0.5; } } - -.item-enter { - max-height: 0; -} - -.item-enter-active { - overflow: hidden; - max-height: 58px; - transition: max-height 0.3s ease-in-out; -} - -.item-exit { - max-height: 58px; -} - -.item-exit-active { - overflow: hidden; - max-height: 0; - transition: max-height 0.3s ease-in-out; -} - -.temp-item-enter { - max-height: 0; -} - -.temp-item-enter-active { - overflow: hidden; - max-height: 58px; - transition: max-height 0.3s ease-in-out; -} - -.temp-item-exit { - max-height: 58px; -} - -.temp-item-exit-active { - transform: translateY(-58px); - max-height: 0; - opacity: 0; - transition: 0.3s ease-in-out; - transition-property: opacity, max-height, transform; -} - -.has-error .temp-item-exit-active { - transform: translateY(0); - overflow: hidden; -} diff --git a/src/styles/todoapp.scss b/src/styles/todoapp.scss index 9095f1847f..836166156b 100644 --- a/src/styles/todoapp.scss +++ b/src/styles/todoapp.scss @@ -121,7 +121,6 @@ appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - transition: opacity 0.3s; &:hover { text-decoration: underline; @@ -130,9 +129,5 @@ &:active { text-decoration: none; } - - &:disabled { - visibility: hidden; - } } } diff --git a/src/types/ErrorMessage.ts b/src/types/ErrorMessage.ts new file mode 100644 index 0000000000..d071a408a7 --- /dev/null +++ b/src/types/ErrorMessage.ts @@ -0,0 +1,8 @@ +export enum ErrorMessage { + None = '', + EmptyTitleRecieved = 'Title should not be empty', + UnableLoad = 'Unable to load todos', + UnableAdd = 'Unable to add a todo', + UnableDelete = 'Unable to delete a todo', + UnableUpdate = 'Unable to update a todo', +} diff --git a/src/types/FilterKey.ts b/src/types/FilterKey.ts new file mode 100644 index 0000000000..1e42321ab7 --- /dev/null +++ b/src/types/FilterKey.ts @@ -0,0 +1,5 @@ +export enum FilterKey { + All = '', + Active = 'active', + Completed = 'completed', +} 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/UserId.js b/src/utils/UserId.js new file mode 100644 index 0000000000..09999070d0 --- /dev/null +++ b/src/utils/UserId.js @@ -0,0 +1 @@ +export const USER_ID = 11492; diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts new file mode 100644 index 0000000000..42421feae0 --- /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', + }; + } + + // we wait for testing purpose to see loaders + return wait(300) + .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/getFilteredTodos.ts b/src/utils/getFilteredTodos.ts new file mode 100644 index 0000000000..7aa2ef0d9d --- /dev/null +++ b/src/utils/getFilteredTodos.ts @@ -0,0 +1,15 @@ +import { FilterKey } from '../types/FilterKey'; +import { Todo } from '../types/Todo'; + +export function getFilteredTodos(key: FilterKey, todos: Todo[]) { + switch (key) { + case FilterKey.All: + return todos; + case FilterKey.Active: + return todos.filter(({ completed }) => !completed); + case FilterKey.Completed: + return todos.filter(({ completed }) => completed); + default: + return todos; + } +}