diff --git a/src/App.tsx b/src/App.tsx index bf9c75980..c0d691160 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,203 +1,25 @@ -/* 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 { USER_ID } from './api/todos'; +import { TodosProvider } from './Context/TodoContext'; +import { TodoInput } from './Components/TodoInput'; +import { TodoList } from './Components/TodoList'; +import { TodoFilters } from './Components/TodoFilters'; +import { NotificationProvider } from './Context/NotificationContext'; +import { Notification } from './Components/Notification'; export const App: React.FC = () => { - if (!USER_ID) { - return ; - } - return (

todos

-
-
- {/* this button should have `active` class only if all todos are completed */} -
- -
- {/* This is a completed todo */} -
- - - - Completed Todo - - - {/* Remove button appears only on hover */} - - - {/* overlay will cover the todo while it is being deleted or updated */} -
-
-
-
-
- - {/* This todo is an active todo */} -
- - - - Not Completed Todo - - - -
-
-
-
-
- - {/* 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 */} -
-
-
-
-
-
- - {/* Hide the footer if there are no todos */} -
- - 3 items left - - - {/* Active link should have the 'selected' class */} - - - {/* this button should be disabled if there are no completed todos */} - -
-
- - {/* DON'T use conditional rendering to hide the notification */} - {/* Add the 'hidden' class to hide the message smoothly */} -
-
); diff --git a/src/Components/Loader/Loader.tsx b/src/Components/Loader/Loader.tsx new file mode 100644 index 000000000..65eba1723 --- /dev/null +++ b/src/Components/Loader/Loader.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import '../../styles/Loader.scss'; + +export const Loader: React.FC = () => ( +
+
+
+); diff --git a/src/Components/Loader/index.ts b/src/Components/Loader/index.ts new file mode 100644 index 000000000..d5ce98115 --- /dev/null +++ b/src/Components/Loader/index.ts @@ -0,0 +1 @@ +export * from './Loader'; diff --git a/src/Components/Notification/Notification.tsx b/src/Components/Notification/Notification.tsx new file mode 100644 index 000000000..446633ac6 --- /dev/null +++ b/src/Components/Notification/Notification.tsx @@ -0,0 +1,22 @@ +import classNames from 'classnames'; +import { useNotification } from '../../Context/NotificationContext'; + +export const Notification = () => { + const { message, isVisible, hideNotification } = useNotification(); + + return ( +
+ +
+ ); +}; diff --git a/src/Components/Notification/index.ts b/src/Components/Notification/index.ts new file mode 100644 index 000000000..ed80171ae --- /dev/null +++ b/src/Components/Notification/index.ts @@ -0,0 +1 @@ +export * from './Notification'; diff --git a/src/Components/TodoEdit/TodoEdit.tsx b/src/Components/TodoEdit/TodoEdit.tsx new file mode 100644 index 000000000..076acc7ff --- /dev/null +++ b/src/Components/TodoEdit/TodoEdit.tsx @@ -0,0 +1,64 @@ +import { useContext, useState } from 'react'; +import { TodosContext } from '../../Context/TodoContext'; +import { EditContext } from '../../Context/EditContext'; +import { ACTIONS } from '../../types/Actions'; + +type Props = { + title: string; + id: number; +}; + +export const TodoEdit: React.FC = ({ title, id }) => { + const [newTodoTitle, setNewTodo] = useState(title); + const { dispatch } = useContext(TodosContext); + const { setEditedTodoId } = useContext(EditContext); + + const checkNewValue = (newTitle: string) => { + if (newTitle.length === 0) { + dispatch({ type: ACTIONS.DELETE_TODO, payload: { id } }); + setEditedTodoId(null); + + return; + } + + if (newTitle !== title) { + dispatch({ type: ACTIONS.RENAME_TODO, payload: { id, title: newTitle } }); + } + + setEditedTodoId(null); + }; + + const handleChange = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setEditedTodoId(null); + + return; + } + + if (event.key !== 'Enter') { + return; + } + + checkNewValue(newTodoTitle.trim()); + }; + + const handleBlur = () => { + checkNewValue(newTodoTitle.trim()); + }; + + return ( +
e.preventDefault()}> + handleChange(event)} + onChange={event => setNewTodo(event.target.value)} + /> +
+ ); +}; diff --git a/src/Components/TodoEdit/index.ts b/src/Components/TodoEdit/index.ts new file mode 100644 index 000000000..2fcc032d8 --- /dev/null +++ b/src/Components/TodoEdit/index.ts @@ -0,0 +1 @@ +export * from './TodoEdit'; diff --git a/src/Components/TodoElement/TodoElement.tsx b/src/Components/TodoElement/TodoElement.tsx new file mode 100644 index 000000000..aa02f3162 --- /dev/null +++ b/src/Components/TodoElement/TodoElement.tsx @@ -0,0 +1,75 @@ +import classNames from 'classnames'; +import { TodosContext } from '../../Context/TodoContext'; +import { useContext } from 'react'; +import { TodoEdit } from '../TodoEdit'; +import { EditContext } from '../../Context/EditContext'; +import { ACTIONS } from '../../types/Actions'; +import { Todo } from '../../types/Todo'; + +type Props = { + todo: Todo; +}; + +export const TodoElement: React.FC = ({ todo }) => { + const { completed, title, id } = todo; + const { dispatch } = useContext(TodosContext); + const { editedTodoId, setEditedTodoId } = useContext(EditContext); + + const handleToggle = () => { + dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id, completed } }); + }; + + const handleDelete = () => { + dispatch({ type: ACTIONS.DELETE_TODO, payload: { id } }); + }; + + const completedTodoClass = classNames('todo', { + completed: completed, + }); + + return ( +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + + {editedTodoId === id ? ( + + ) : ( + <> + setEditedTodoId(id)} + > + {title} + + + + + )} +
+
+
+
+
+ ); +}; diff --git a/src/Components/TodoElement/index.ts b/src/Components/TodoElement/index.ts new file mode 100644 index 000000000..43979830c --- /dev/null +++ b/src/Components/TodoElement/index.ts @@ -0,0 +1 @@ +export * from './TodoElement'; diff --git a/src/Components/TodoFilters/TodoFilters.tsx b/src/Components/TodoFilters/TodoFilters.tsx new file mode 100644 index 000000000..14bbe9cf2 --- /dev/null +++ b/src/Components/TodoFilters/TodoFilters.tsx @@ -0,0 +1,76 @@ +import { useContext, useState } from 'react'; +import { TodosContext } from '../../Context/TodoContext'; +import classNames from 'classnames'; +import { ACTIONS } from '../../types/Actions'; +import { FilterType, FILTERS } from '../../types/Filters'; + +export const TodoFilters = () => { + const { state, dispatch } = useContext(TodosContext); + const { todos } = state; + const [currentFilter, setcurrentFilter] = useState(FILTERS.ALL); + + const todosLeft = todos.filter(todo => !todo.completed).length; + + const handleFilterChange = (type: FilterType) => { + dispatch({ type: ACTIONS.SET_FILTER, payload: `${type}` }); + setcurrentFilter(type); + }; + + const handleClearAll = () => { + dispatch({ type: ACTIONS.DELETE_COMPLETED, payload: todos }); + }; + + const getFilterClass = (filter: FilterType) => + classNames('filter__link', { selected: currentFilter === filter }); + + return ( + <> + {todos.length !== 0 && ( + + )} + + ); +}; diff --git a/src/Components/TodoFilters/index.ts b/src/Components/TodoFilters/index.ts new file mode 100644 index 000000000..3c37575a7 --- /dev/null +++ b/src/Components/TodoFilters/index.ts @@ -0,0 +1 @@ +export * from './TodoFilters'; diff --git a/src/Components/TodoInput/TodoInput.tsx b/src/Components/TodoInput/TodoInput.tsx new file mode 100644 index 000000000..6a956152b --- /dev/null +++ b/src/Components/TodoInput/TodoInput.tsx @@ -0,0 +1,118 @@ +import { useContext, useEffect, useRef, useState } from 'react'; +import { TodosContext } from '../../Context/TodoContext'; +import classNames from 'classnames'; +import { ACTIONS } from '../../types/Actions'; +import { USER_ID } from '../../api/todos'; +import '../../styles/notification.scss'; +import { Loader } from '../Loader'; +import { useNotification } from '../../Context/NotificationContext'; + +export const TodoInput = () => { + const { dispatch, state, handleAddTodo } = useContext(TodosContext); + const { showNotification, hideNotification } = useNotification(); + const [newTodo, setNewTodo] = useState(''); + const inputRef = useRef(null); + const [isNewTodoLoading, setIsNewTodoLoading] = useState(false); + const { isLoading, isError } = state; + + const createNewTodo = () => { + return { + title: newTodo.trim(), + completed: false, + userId: USER_ID, + }; + }; + + const handleChange = (event: React.ChangeEvent) => { + setNewTodo(event.target.value); + }; + + const handleAdd = async (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setNewTodo(''); + + return; + } + + if (event.key === 'Enter' && newTodo.trim() !== '') { + const newTodoItem = createNewTodo(); + + setIsNewTodoLoading(true); + hideNotification(); + + try { + await handleAddTodo(newTodoItem); + setNewTodo(''); + } catch (error) { + showNotification('Failed to add todo. Please try again!'); + } finally { + setIsNewTodoLoading(false); + } + } + }; + + const handleToggleAll = () => { + dispatch({ type: ACTIONS.TOGGLE_ALL, payload: state.todos }); + }; + + const toggleAllClass = classNames('todoapp__toggle-all', { + active: state.todos.every(todo => todo.completed), + }); + + useEffect(() => { + if (!isNewTodoLoading && inputRef.current) { + inputRef.current.focus(); + } + }, [state.todos, isNewTodoLoading]); + + return ( +
+
e.preventDefault()}> +
+ {state.todos.length !== 0 && ( +
+
+ + {isError && ( +
+

+ Failed to load todos. Please try again later. +

+
+ )} + + {isLoading && !isError && ( +
+ +
+ )} +
+ ); +}; diff --git a/src/Components/TodoInput/index.ts b/src/Components/TodoInput/index.ts new file mode 100644 index 000000000..a825475dc --- /dev/null +++ b/src/Components/TodoInput/index.ts @@ -0,0 +1 @@ +export * from './TodoInput'; diff --git a/src/Components/TodoList/TodoList.tsx b/src/Components/TodoList/TodoList.tsx new file mode 100644 index 000000000..73cd65d77 --- /dev/null +++ b/src/Components/TodoList/TodoList.tsx @@ -0,0 +1,32 @@ +import { useContext, useEffect, useState } from 'react'; +import { TodosContext } from '../../Context/TodoContext'; +import { TodoElement } from '../TodoElement'; +import { EditProvider } from '../../Context/EditContext'; + +export const TodoList = () => { + const { state } = useContext(TodosContext); + const { todos, filter } = state; + const [visibleTodos, setVisibleTodos] = useState(todos); + + useEffect(() => { + if (filter === 'ALL') { + setVisibleTodos(todos); + } else if (filter === 'COMPLETED') { + setVisibleTodos(todos.filter(todo => todo.completed)); + } else { + setVisibleTodos(todos.filter(todo => !todo.completed)); + } + }, [filter, todos]); + + return ( +
+ {todos.length !== 0 && ( + + {visibleTodos.map(todo => ( + + ))} + + )} +
+ ); +}; diff --git a/src/Components/TodoList/index.ts b/src/Components/TodoList/index.ts new file mode 100644 index 000000000..f239f4345 --- /dev/null +++ b/src/Components/TodoList/index.ts @@ -0,0 +1 @@ +export * from './TodoList'; diff --git a/src/Context/EditContext.tsx b/src/Context/EditContext.tsx new file mode 100644 index 000000000..5f20a1cc8 --- /dev/null +++ b/src/Context/EditContext.tsx @@ -0,0 +1,21 @@ +import { createContext, ReactNode, useState } from 'react'; + +type EditContextType = { + editedTodoId: number | null; + setEditedTodoId: (id: number | null) => void; +}; + +export const EditContext = createContext({ + editedTodoId: null, + setEditedTodoId: () => {}, +}); + +export const EditProvider = ({ children }: { children: ReactNode }) => { + const [editedTodoId, setEditedTodoId] = useState(null); + + return ( + + {children} + + ); +}; diff --git a/src/Context/NotificationContext.tsx b/src/Context/NotificationContext.tsx new file mode 100644 index 000000000..5a321376f --- /dev/null +++ b/src/Context/NotificationContext.tsx @@ -0,0 +1,64 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +type NotificationContextType = { + message: string; + isVisible: boolean; + showNotification: (msg: string) => void; + hideNotification: () => void; +}; + +const NotificationContext = createContext( + undefined, +); + +export const NotificationProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [message, setMessage] = useState(''); + const [isVisible, setIsVisible] = useState(false); + + const showNotification = (msg: string) => { + setMessage(msg); + setIsVisible(true); + + setTimeout(() => { + setIsVisible(false); + }, 3000); + }; + + const hideNotification = () => { + setIsVisible(false); + }; + + useEffect(() => { + let timer: NodeJS.Timeout; + + if (!isVisible && message) { + timer = setTimeout(() => setMessage(''), 300); + } + + return () => clearTimeout(timer); + }, [isVisible, message]); + + return ( + + {children} + + ); +}; + +export const useNotification = () => { + const context = useContext(NotificationContext); + + if (!context) { + throw new Error( + 'useNotification must be used within a NotificationProvider', + ); + } + + return context; +}; diff --git a/src/Context/TodoContext.tsx b/src/Context/TodoContext.tsx new file mode 100644 index 000000000..fe2784770 --- /dev/null +++ b/src/Context/TodoContext.tsx @@ -0,0 +1,272 @@ +import React, { createContext, ReactNode, useEffect, useReducer } from 'react'; +import { FILTERS, FilterType } from '../types/Filters'; +import { Todo, TodoBase } from '../types/Todo'; +import { Action, ACTIONS } from '../types/Actions'; +import { + addTodo, + renameTodo, + deleteTodo, + toggleTodo, + toggleAll, + deleteCompleted, + getTodos, +} from '../api/todos'; +import { useNotification } from './NotificationContext'; + +type State = { + todos: Todo[]; + filter: FilterType; + isLoading: boolean; + isError: boolean; +}; + +type Context = { + state: State; + dispatch: React.Dispatch; + handleAddTodo: (todo: TodoBase) => Promise; + handleRenameTodo: (id: number, newTitle: string) => void; + handleDeleteTodo: (id: number) => void; + handleToggleTodo: (id: number, completed: boolean) => void; + handleToggleAll: () => void; + handleDeleteCompleted: () => void; +}; + +export const TodosContext = createContext({ + state: { todos: [], filter: FILTERS.ALL, isLoading: false, isError: false }, + dispatch: () => {}, + handleAddTodo: async () => Promise.resolve(), + handleRenameTodo: () => {}, + handleDeleteTodo: () => {}, + handleToggleTodo: () => {}, + handleToggleAll: () => {}, + handleDeleteCompleted: () => {}, +}); + +export const todosReducer = (state: State, action: Action): State => { + const { todos } = state; + const { type, payload } = action; + + switch (type) { + case ACTIONS.SET_TODOS: + return { ...state, todos: payload }; + + case ACTIONS.SET_LOADING: + return { ...state, isLoading: payload }; + + case ACTIONS.SET_ERROR: + return { ...state, isError: payload, isLoading: false }; + + case ACTIONS.ADD_TODO: + return { ...state, todos: [...todos, payload] }; + + case ACTIONS.UPDATE_ID: { + const { tempId, id } = payload; + + return { + ...state, + todos: todos.map(todo => (todo.id === tempId ? { ...todo, id } : todo)), + }; + } + + case ACTIONS.DELETE_TODO: + return { ...state, todos: todos.filter(todo => todo.id !== payload.id) }; + + case ACTIONS.RENAME_TODO: { + const { id, title } = payload; + + return { + ...state, + todos: todos.map(todo => (todo.id === id ? { ...todo, title } : todo)), + }; + } + + case ACTIONS.DELETE_TODO: { + const { id } = payload; + + return { ...state, todos: todos.filter(todo => todo.id !== id) }; + } + + case ACTIONS.TOGGLE_TODO: + const { id } = payload; + + return { + ...state, + todos: todos.map(todo => + todo.id === id ? { ...todo, completed: !todo.completed } : todo, + ), + }; + + case ACTIONS.TOGGLE_ALL: + const isTodosCompleted = todos.every(todo => todo.completed); + + if (isTodosCompleted) { + return { + ...state, + todos: todos.map(todo => { + return { ...todo, completed: false }; + }), + }; + } + + return { + ...state, + todos: todos.map(todo => { + return { ...todo, completed: true }; + }), + }; + + case ACTIONS.DELETE_COMPLETED: + return { ...state, todos: todos.filter(todo => !todo.completed) }; + + case ACTIONS.SET_FILTER: + return { ...state, filter: payload }; + + default: + return state; + } +}; + +export const TodosProvider = ({ children }: { children: ReactNode }) => { + const [state, dispatch] = useReducer(todosReducer, { + todos: [], + filter: FILTERS.ALL, + isLoading: false, + isError: false, + }); + + const { showNotification } = useNotification(); + + useEffect(() => { + const fetchTodos = async () => { + dispatch({ type: ACTIONS.SET_LOADING, payload: true }); + + try { + const fetchedTodos = await getTodos(); + + dispatch({ type: ACTIONS.SET_TODOS, payload: fetchedTodos }); + } catch (error) { + dispatch({ type: ACTIONS.SET_ERROR, payload: true }); + showNotification('Unable to load todos'); + } finally { + dispatch({ type: ACTIONS.SET_LOADING, payload: false }); + } + }; + + fetchTodos(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleAddTodo = async (todo: TodoBase) => { + const tempId = +new Date(); + const tempTodo: Todo = { ...todo, id: tempId }; + + const createdTodo = await addTodo({ ...todo }); + + dispatch({ + type: ACTIONS.ADD_TODO, + payload: { ...tempTodo, id: createdTodo.id }, + }); + }; + + const handleDeleteTodo = async (id: number) => { + const prevTodos = state.todos; + + dispatch({ + type: ACTIONS.SET_TODOS, + payload: prevTodos.filter(todo => todo.id !== id), + }); + + try { + await deleteTodo(id); + } catch (error) { + dispatch({ type: ACTIONS.SET_TODOS, payload: prevTodos }); + } + }; + + const handleDeleteCompleted = async () => { + const prevTodos = state.todos; + const filteredTodos = prevTodos.filter(todo => !todo.completed); + + dispatch({ + type: ACTIONS.SET_TODOS, + payload: filteredTodos, + }); + + try { + await deleteCompleted(prevTodos); + } catch (error) { + dispatch({ type: ACTIONS.SET_TODOS, payload: prevTodos }); + } + }; + + const handleRenameTodo = async (id: number, newTitle: string) => { + const prevTodos = state.todos; + + dispatch({ + type: ACTIONS.SET_TODOS, + payload: prevTodos.map(todo => + todo.id === id ? { ...todo, title: newTitle } : todo, + ), + }); + + try { + await renameTodo(id, newTitle); + } catch (error) { + dispatch({ type: ACTIONS.SET_TODOS, payload: prevTodos }); + } + }; + + const handleToggleTodo = async (id: number, completed: boolean) => { + const prevTodos = state.todos; + + dispatch({ + type: ACTIONS.SET_TODOS, + payload: prevTodos.map(todo => + todo.id === id ? { ...todo, completed: !completed } : todo, + ), + }); + + try { + await toggleTodo(id, completed); + } catch (error) { + dispatch({ type: ACTIONS.SET_TODOS, payload: prevTodos }); + } + }; + + const handleToggleAll = async () => { + const prevTodos = state.todos; + const allCompleted = prevTodos.every(todo => todo.completed); + const updatedTodos = prevTodos.map(todo => ({ + ...todo, + completed: !allCompleted, + })); + + dispatch({ + type: ACTIONS.SET_TODOS, + payload: updatedTodos, + }); + + try { + await toggleAll(prevTodos); + } catch (error) { + dispatch({ type: ACTIONS.SET_TODOS, payload: prevTodos }); + } + }; + + return ( + + {children} + + ); +}; diff --git a/src/api/todos.ts b/src/api/todos.ts index 8a0b90aa0..79ac7e9a5 100644 --- a/src/api/todos.ts +++ b/src/api/todos.ts @@ -1,10 +1,46 @@ -import { Todo } from '../types/Todo'; +import { Todo, TodoBase } from '../types/Todo'; import { client } from '../utils/fetchClient'; -export const USER_ID = 0; +export const USER_ID = 2358; export const getTodos = () => { return client.get(`/todos?userId=${USER_ID}`); }; // Add more methods here + +export const addTodo = (newTodo: TodoBase) => { + return client.post(`/todos`, { + ...newTodo, + }); +}; + +export const renameTodo = (id: number, newName: string) => { + return client.patch(`/todos/${id}`, { title: newName }); +}; + +export const deleteTodo = (id: number) => { + return client.delete(`/todos/${id}`); +}; + +export const toggleTodo = (id: number, completed: boolean) => { + return client.patch(`/todos/${id}`, { completed: !completed }); +}; + +export const toggleAll = (todos: Todo[]) => { + const allCompleted = todos.every(todo => todo.completed); + + return Promise.all( + todos.map(todo => { + return client.patch(`/todos/${todo.id}`, { completed: !allCompleted }); + }), + ); +}; + +export const deleteCompleted = (todos: Todo[]) => { + const completedTodos = todos.filter(todo => todo.completed); + + return Promise.all( + completedTodos.map(todo => client.delete(`/todos/${todo.id}`)), + ); +}; diff --git a/src/styles/Loader.scss b/src/styles/Loader.scss new file mode 100644 index 000000000..040c96200 --- /dev/null +++ b/src/styles/Loader.scss @@ -0,0 +1,25 @@ +.Loader { + display: flex; + width: 100%; + justify-content: center; + align-items: center; + + &__content { + border-radius: 50%; + width: 2em; + height: 2em; + margin: 1em auto; + border: 0.3em solid #ddd; + border-left-color: #000; + animation: load8 1.2s infinite linear; + } +} + +@keyframes load8 { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/styles/notification.scss b/src/styles/notification.scss new file mode 100644 index 000000000..edf590ff0 --- /dev/null +++ b/src/styles/notification.scss @@ -0,0 +1,45 @@ +.error-notification { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background-color: #f8d7da; + color: #721c24; + padding: 15px 25px; + border-radius: 5px; + font-size: 16px; + display: flex; + align-items: center; + gap: 16px; + z-index: 1000; + opacity: 1; + transition: opacity 0.3s ease-in-out; + + &.fade-out { + opacity: 0; + } + + &.hidden { + opacity: 0; + } + + .notification-message { + flex: 1; + } + + .close-btn { + cursor: pointer; + padding: 5px 10px; + font-size: 20px; + font-weight: bold; + color: #721c24; + background: transparent; + border: none; + transition: transform 0.2s ease, color 0.2s ease; + + &:hover { + transform: scale(1.2); + color: #a94442; + } + } +} diff --git a/src/styles/todoapp.scss b/src/styles/todoapp.scss index ad28bcb2f..fde48cb8a 100644 --- a/src/styles/todoapp.scss +++ b/src/styles/todoapp.scss @@ -135,4 +135,27 @@ visibility: hidden; } } + + &__input-wrapper { + position: relative; + width: 100%; + } + + &__input-loader { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + } + + &__error-wrapper { + display: flex; + justify-content: center; + align-items: center; + height: 100px; + } + + &__error-message { + color: rgb(175, 47, 47); + } } diff --git a/src/types/Actions.ts b/src/types/Actions.ts new file mode 100644 index 000000000..c0b8d0d36 --- /dev/null +++ b/src/types/Actions.ts @@ -0,0 +1,35 @@ +/* eslint-disable prettier/prettier */ +import { FilterType } from './Filters'; +import { Todo } from './Todo'; + +export const ACTIONS = { + SET_TODOS: 'SET_TODOS', + ADD_TODO: 'ADD_TODO', + RENAME_TODO: 'RENAME_TODO', + DELETE_TODO: 'DELETE_TODO', + SET_FILTER: 'SET_FILTER', + TOGGLE_TODO: 'TOGGLE_TODO', + TOGGLE_ALL: 'TOGGLE_ALL', + DELETE_COMPLETED: 'DELETE_COMPLETED', + UPDATE_ID: 'UPDATE_ID', + SET_LOADING: 'SET_LOADING', + SET_ERROR: 'SET_ERROR', +} as const; + +export type Action = + | { type: typeof ACTIONS.ADD_TODO; payload: Todo } + | { type: typeof ACTIONS.RENAME_TODO; payload: { id: number; title: string } } + | { type: typeof ACTIONS.DELETE_TODO; payload: { id: number } } + | { type: typeof ACTIONS.SET_FILTER; payload: FilterType } + | { type: typeof ACTIONS.TOGGLE_TODO; + payload: { + id: number, + completed: boolean + } + } + | { type: typeof ACTIONS.TOGGLE_ALL; payload: Todo[] } + | { type: typeof ACTIONS.DELETE_COMPLETED; payload: Todo[] } + | { type: typeof ACTIONS.SET_TODOS; payload: Todo[]} + | { type: typeof ACTIONS.UPDATE_ID; payload: { id: number; tempId: number}} + | { type: typeof ACTIONS.SET_LOADING; payload: boolean } + | { type: typeof ACTIONS.SET_ERROR; payload: boolean }; diff --git a/src/types/Filters.ts b/src/types/Filters.ts new file mode 100644 index 000000000..c5a93f521 --- /dev/null +++ b/src/types/Filters.ts @@ -0,0 +1,7 @@ +export const FILTERS = { + ALL: 'ALL', + ACTIVE: 'ACTIVE', + COMPLETED: 'COMPLETED', +} as const; + +export type FilterType = (typeof FILTERS)[keyof typeof FILTERS]; diff --git a/src/types/Todo.ts b/src/types/Todo.ts index 3f52a5fdd..cf48645c8 100644 --- a/src/types/Todo.ts +++ b/src/types/Todo.ts @@ -1,6 +1,9 @@ -export interface Todo { - id: number; - userId: number; +export type TodoBase = { title: string; completed: boolean; + userId: number; +}; + +export interface Todo extends TodoBase { + id: number; }