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 */}
-
-
- {/* Add a todo on form submit */}
-
-
-
-
- {/* 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 */}
-
-
- All
-
-
-
- Active
-
-
-
- Completed
-
-
-
- {/* this button should be disabled if there are no completed todos */}
-
- Clear completed
-
-
-
-
- {/* DON'T use conditional rendering to hide the notification */}
- {/* Add the 'hidden' class to hide the message smoothly */}
-
-
- {/* show only one message at a time */}
- Unable to load todos
-
- Title should not be empty
-
- Unable to add a todo
-
- Unable to delete a todo
-
- Unable to update a todo
+
+
+
+
+
+
+
+
);
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 (
+
+
+ {message}
+
+
+ );
+};
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 (
+
+ );
+};
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 (
+
+ );
+};
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;
}