From e7a65a71b114ce0651bfadd79d8a6f66475f4c17 Mon Sep 17 00:00:00 2001
From: Igor <4430704@gmail.com>
Date: Wed, 4 Dec 2024 22:11:07 +0200
Subject: [PATCH 1/5] working solution
---
src/App.tsx | 154 +++----------------------------
src/Store.tsx | 129 ++++++++++++++++++++++++++
src/components/FilterButton.tsx | 42 +++++++++
src/components/Footer.tsx | 43 +++++++++
src/components/Form.tsx | 60 ++++++++++++
src/components/Header.tsx | 34 +++++++
src/components/TodoItem.tsx | 158 ++++++++++++++++++++++++++++++++
src/fakeTodos.ts | 12 +++
src/hooks/useLocalStorage.ts | 44 +++++++++
src/index.tsx | 11 ++-
src/styles/index.scss | 4 +
src/types/Todo.ts | 11 +++
src/utils/services.ts | 12 +++
13 files changed, 568 insertions(+), 146 deletions(-)
create mode 100644 src/Store.tsx
create mode 100644 src/components/FilterButton.tsx
create mode 100644 src/components/Footer.tsx
create mode 100644 src/components/Form.tsx
create mode 100644 src/components/Header.tsx
create mode 100644 src/components/TodoItem.tsx
create mode 100644 src/fakeTodos.ts
create mode 100644 src/hooks/useLocalStorage.ts
create mode 100644 src/types/Todo.ts
create mode 100644 src/utils/services.ts
diff --git a/src/App.tsx b/src/App.tsx
index a399287bd..3b5380376 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,156 +1,26 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
-import React from 'react';
+import React, { useContext } from 'react';
+import { StateContext } from './Store';
+import TodoItem from './components/TodoItem';
+import Footer from './components/Footer';
+import Header from './components/Header';
export const App: React.FC = () => {
+ const { todos, allTodos } = useContext(StateContext);
+
return (
todos
-
- {/* this button should have `active` class only if all todos are completed */}
-
-
- {/* Add a todo on form submit */}
-
-
+
-
- {/* 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
-
-
+ {allTodos.length > 0 &&
}
);
diff --git a/src/Store.tsx b/src/Store.tsx
new file mode 100644
index 000000000..01f59bec1
--- /dev/null
+++ b/src/Store.tsx
@@ -0,0 +1,129 @@
+import React, { useEffect, useReducer } from 'react';
+import { Filters, Todo } from './types/Todo';
+import { filterTodos } from './utils/services';
+import { useLocalStorage } from './hooks/useLocalStorage';
+
+type Action =
+ | { type: 'filter'; payload: Filters }
+ | { type: 'addTodo'; payload: Todo }
+ | { type: 'updateTodo'; payload: Todo }
+ | { type: 'toggleTodo'; payload: number }
+ | { type: 'deleteTodo'; payload: number }
+ | { type: 'toggleAllTodos' }
+ | { type: 'clearCompleted' }
+ | { type: 'renameTodo'; payload: number | null };
+
+interface State {
+ allTodos: Todo[];
+ todos: Todo[];
+ activeFilter: Filters;
+ renamingTodo: number | null;
+}
+
+const reducer = (state: State, action: Action): State => {
+ let updatedTodos = state.allTodos;
+
+ switch (action.type) {
+ case 'addTodo': {
+ updatedTodos = [...state.allTodos, action.payload];
+ break;
+ }
+
+ case 'updateTodo': {
+ updatedTodos = state.allTodos.map(todo =>
+ todo.id === action.payload.id
+ ? { ...todo, title: action.payload.title }
+ : todo,
+ );
+ break;
+ }
+
+ case 'deleteTodo': {
+ updatedTodos = state.allTodos.filter(todo => todo.id !== action.payload);
+ break;
+ }
+
+ case 'clearCompleted': {
+ updatedTodos = state.allTodos.filter(todo => !todo.completed);
+ break;
+ }
+
+ case 'toggleTodo': {
+ updatedTodos = state.allTodos.map(todo =>
+ todo.id === action.payload
+ ? { ...todo, completed: !todo.completed }
+ : todo,
+ );
+ break;
+ }
+
+ case 'toggleAllTodos': {
+ const isAllCompleted = state.allTodos.every(todo => todo.completed);
+
+ updatedTodos = state.allTodos.map(todo => ({
+ ...todo,
+ completed: !isAllCompleted,
+ }));
+ break;
+ }
+
+ case 'filter': {
+ return {
+ ...state,
+ activeFilter: action.payload,
+ todos: filterTodos(state.allTodos, action.payload),
+ };
+ }
+
+ case 'renameTodo': {
+ return {
+ ...state,
+ renamingTodo: action.payload,
+ };
+ }
+
+ default:
+ return state;
+ }
+
+ return {
+ ...state,
+ allTodos: updatedTodos,
+ todos: filterTodos(updatedTodos, state.activeFilter),
+ };
+};
+
+const initialState: State = {
+ allTodos: [],
+ todos: [],
+ activeFilter: Filters.All,
+ renamingTodo: null,
+};
+
+export const StateContext = React.createContext(initialState);
+export const DispatchContext =
+ React.createContext | null>(null);
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const GlobalStateProvider: React.FC = ({ children }) => {
+ const [local, setLocal] = useLocalStorage('todos', []);
+ const [state, dispatch] = useReducer(reducer, {
+ ...initialState,
+ allTodos: local,
+ todos: local,
+ renamingTodo: initialState.renamingTodo,
+ });
+
+ useEffect(() => {
+ setLocal(state.allTodos);
+ }, [state.allTodos, setLocal]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/FilterButton.tsx b/src/components/FilterButton.tsx
new file mode 100644
index 000000000..385d50c60
--- /dev/null
+++ b/src/components/FilterButton.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import cn from 'classnames';
+import { Filters } from '../types/Todo';
+import { DispatchContext, StateContext } from '../Store';
+
+type Props = {
+ filterItem: Filters;
+};
+
+const FilterButton: React.FC = ({ filterItem }) => {
+ const { activeFilter } = React.useContext(StateContext);
+ const dispatch = React.useContext(DispatchContext);
+
+ const isSelectedFilter = activeFilter === filterItem;
+
+ const filterName = filterItem.charAt(0).toUpperCase() + filterItem.slice(1);
+
+ const handleFilterClick = () => {
+ if (!isSelectedFilter) {
+ if (dispatch) {
+ dispatch({ type: 'filter', payload: filterItem });
+ }
+ }
+ };
+
+ return (
+
+ {filterName}
+
+ );
+};
+
+export default FilterButton;
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 000000000..23c5d938b
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+
+import { Filters } from '../types/Todo';
+import FilterButton from './FilterButton';
+import { DispatchContext, StateContext } from '../Store';
+
+const Footer: React.FC = ({}) => {
+ const { allTodos } = React.useContext(StateContext);
+ const activeTodos = allTodos.filter(todo => !todo.completed);
+ const dispatch = React.useContext(DispatchContext);
+
+ const handleClearCompleted = () => {
+ if (dispatch) {
+ dispatch({ type: 'clearCompleted' });
+ }
+ };
+
+ return (
+
+
+ {activeTodos.length} items left
+
+
+
+ {Object.values(Filters).map(filterItem => {
+ return ;
+ })}
+
+
+ !todo.completed)}
+ onClick={handleClearCompleted}
+ >
+ Clear completed
+
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/Form.tsx b/src/components/Form.tsx
new file mode 100644
index 000000000..3f41556fc
--- /dev/null
+++ b/src/components/Form.tsx
@@ -0,0 +1,60 @@
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import { DispatchContext, StateContext } from '../Store';
+import { Todo } from '../types/Todo';
+
+const Form: React.FC = () => {
+ const [todoTitle, setTodoTitle] = useState('');
+ const { renamingTodo } = React.useContext(StateContext);
+
+ const dispatch = useContext(DispatchContext);
+
+ const titleField = useRef(null);
+
+ useEffect(() => {
+ if (titleField.current && !renamingTodo) {
+ titleField.current.focus();
+ }
+ }, [renamingTodo]);
+
+ const addTodo = () => {
+ const newTodo: Todo = {
+ id: Date.now(),
+ title: todoTitle,
+ completed: false,
+ };
+
+ if (dispatch) {
+ dispatch({ type: 'addTodo', payload: newTodo });
+ }
+ };
+
+ const handleTitleInput = (event: React.ChangeEvent) => {
+ setTodoTitle(event.target.value);
+ };
+
+ const handleFormSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+ if (todoTitle.trim() === '') {
+ return;
+ }
+
+ addTodo();
+ setTodoTitle('');
+ };
+
+ return (
+
+ );
+};
+
+export default Form;
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 000000000..6ac08b3c7
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,34 @@
+import React, { useContext } from 'react';
+import cn from 'classnames';
+import Form from './Form';
+import { DispatchContext, StateContext } from '../Store';
+
+const Header: React.FC = () => {
+ const { allTodos } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ const isAllCompleted = allTodos.every(todo => todo.completed);
+
+ const handleToggleAll = () => {
+ if (dispatch) {
+ dispatch({ type: 'toggleAllTodos' });
+ }
+ };
+
+ return (
+
+ {/* this button should have `active` class only if all todos are completed */}
+
+
+
+ );
+};
+
+export default Header;
diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx
new file mode 100644
index 000000000..438434aa3
--- /dev/null
+++ b/src/components/TodoItem.tsx
@@ -0,0 +1,158 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+/* eslint-disable jsx-a11y/control-has-associated-label */
+
+import cn from 'classnames';
+
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import { Todo } from '../types/Todo';
+import { DispatchContext, StateContext } from '../Store';
+
+type Props = {
+ todo: Todo;
+};
+
+const TodoItem: React.FC = ({ todo }) => {
+ const [inputValue, setInputValue] = useState('');
+ const { renamingTodo } = React.useContext(StateContext);
+
+ const isRenaming = renamingTodo === todo.id;
+
+ // const [isRenaming, setIsRenaming] = useState(false);
+
+ const todoField = useRef(null);
+
+ const dispatch = useContext(DispatchContext);
+
+ const handleRenamingTodo = (id: number | null) => {
+ if (dispatch) {
+ dispatch({ type: 'renameTodo', payload: id });
+ }
+ };
+
+ const handleDelete = () => {
+ if (dispatch) {
+ dispatch({ type: 'deleteTodo', payload: todo.id });
+ }
+ };
+
+ const handleToggle = () => {
+ if (dispatch) {
+ dispatch({ type: 'toggleTodo', payload: todo.id });
+ }
+ };
+
+ const updateTodo = () => {
+ if (dispatch) {
+ dispatch({
+ type: 'updateTodo',
+ payload: {
+ ...todo,
+ title: inputValue,
+ },
+ });
+ }
+ };
+
+ const handleEditTodo = () => {
+ handleRenamingTodo(todo.id);
+ // setIsRenaming(true);
+ setInputValue(todo.title);
+ };
+
+ const handleChangeValue = (event: React.ChangeEvent) => {
+ setInputValue(event.target.value);
+ };
+
+ const handleSubmitChange = (event: React.FormEvent) => {
+ event.preventDefault();
+ handleRenamingTodo(null);
+ // setIsRenaming(false);
+ if (inputValue === '') {
+ handleDelete();
+
+ return;
+ }
+
+ if (inputValue !== todo.title) {
+ updateTodo();
+ handleRenamingTodo(null);
+ // setIsRenaming(false);
+
+ return;
+ }
+ };
+
+ useEffect(() => {
+ const handleEsc = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ handleRenamingTodo(null);
+ // setIsRenaming(false);
+
+ return;
+ }
+ };
+
+ window.addEventListener('keyup', handleEsc);
+
+ return () => {
+ window.removeEventListener('keyup', handleEsc);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (todoField.current) {
+ todoField.current.focus();
+ }
+ }, [isRenaming]);
+
+ return (
+
+
+
+
+
+ {!isRenaming && (
+
+ {todo.title}
+
+ )}
+
+ {!!isRenaming && (
+
+ )}
+
+
+ ×
+
+
+ );
+};
+
+export default TodoItem;
diff --git a/src/fakeTodos.ts b/src/fakeTodos.ts
new file mode 100644
index 000000000..44c0631e2
--- /dev/null
+++ b/src/fakeTodos.ts
@@ -0,0 +1,12 @@
+export const fakeTodos = [
+ { id: 1, title: 'Buy some milk', completed: false },
+ { id: 2, title: 'Call the doctor', completed: true },
+ { id: 3, title: 'Pay water bill', completed: false },
+ { id: 4, title: 'Buy a new phone', completed: true },
+ { id: 5, title: 'Order pizza', completed: false },
+ { id: 6, title: 'Finish the project', completed: false },
+ { id: 7, title: 'Clean the house', completed: false },
+ { id: 8, title: 'Walk the dog', completed: false },
+ { id: 9, title: 'Go to the gym', completed: false },
+ { id: 10, title: 'Read a book', completed: false },
+];
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 000000000..b663354f5
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,44 @@
+import { useState } from 'react';
+import { fakeTodos } from '../fakeTodos';
+
+type SerializableValue =
+ | string
+ | number
+ | boolean
+ | null
+ | SerializableObject
+ | SerializableArray;
+type SerializableObject = { [key: string]: SerializableValue };
+type SerializableArray = SerializableValue[];
+
+export const fakeLocalTodos = () => {
+ return fakeTodos;
+};
+
+export const useLocalStorage = (
+ key: string,
+ defaultValue: T,
+): [T, (value: T) => void] => {
+ const [storedValue, setStoredValue] = useState(() => {
+ try {
+ const item = window.localStorage.getItem(key);
+
+ return item ? JSON.parse(item) : defaultValue;
+ } catch (error) {
+ console.error(error);
+
+ return defaultValue;
+ }
+ });
+
+ const setValue = (value: T) => {
+ try {
+ setStoredValue(value);
+ window.localStorage.setItem(key, JSON.stringify(value));
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return [storedValue, setValue];
+};
diff --git a/src/index.tsx b/src/index.tsx
index a9689cb38..4912b470d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,11 +1,14 @@
import { createRoot } from 'react-dom/client';
-import './styles/index.css';
-import './styles/todo-list.css';
-import './styles/filters.css';
+import './styles/index.scss';
import { App } from './App';
+import { GlobalStateProvider } from './Store';
const container = document.getElementById('root') as HTMLDivElement;
-createRoot(container).render( );
+createRoot(container).render(
+
+
+ ,
+);
diff --git a/src/styles/index.scss b/src/styles/index.scss
index a34eec7c6..9d8c1901b 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -2,6 +2,10 @@ iframe {
display: none;
}
+* {
+ box-sizing: border-box;
+}
+
body {
min-width: 230px;
max-width: 550px;
diff --git a/src/types/Todo.ts b/src/types/Todo.ts
new file mode 100644
index 000000000..7378f22a1
--- /dev/null
+++ b/src/types/Todo.ts
@@ -0,0 +1,11 @@
+export interface Todo {
+ id: number;
+ title: string;
+ completed: boolean;
+}
+
+export enum Filters {
+ All = 'all',
+ Active = 'active',
+ Completed = 'completed',
+}
diff --git a/src/utils/services.ts b/src/utils/services.ts
new file mode 100644
index 000000000..38a27b457
--- /dev/null
+++ b/src/utils/services.ts
@@ -0,0 +1,12 @@
+import { Filters, Todo } from '../types/Todo';
+
+export const filterTodos = (todos: Todo[], status: Filters): Todo[] => {
+ switch (status) {
+ case Filters.Active:
+ return todos.filter(todo => !todo.completed);
+ case Filters.Completed:
+ return todos.filter(todo => todo.completed);
+ default:
+ return todos;
+ }
+};
From 01020fd4d872a4f4a8f7ab07fe2bc6d8c7b4fd44 Mon Sep 17 00:00:00 2001
From: Igor <4430704@gmail.com>
Date: Thu, 5 Dec 2024 10:23:21 +0200
Subject: [PATCH 2/5] working solution, tests passed
---
src/components/Form.tsx | 6 ++++--
src/components/Header.tsx | 18 ++++++++++--------
src/components/TodoItem.tsx | 29 +++++++++++++----------------
src/fakeTodos.ts | 12 ------------
src/hooks/useLocalStorage.ts | 25 +++++--------------------
5 files changed, 32 insertions(+), 58 deletions(-)
delete mode 100644 src/fakeTodos.ts
diff --git a/src/components/Form.tsx b/src/components/Form.tsx
index 3f41556fc..6cf834625 100644
--- a/src/components/Form.tsx
+++ b/src/components/Form.tsx
@@ -4,6 +4,8 @@ import { Todo } from '../types/Todo';
const Form: React.FC = () => {
const [todoTitle, setTodoTitle] = useState('');
+ const { allTodos } = React.useContext(StateContext);
+
const { renamingTodo } = React.useContext(StateContext);
const dispatch = useContext(DispatchContext);
@@ -14,12 +16,12 @@ const Form: React.FC = () => {
if (titleField.current && !renamingTodo) {
titleField.current.focus();
}
- }, [renamingTodo]);
+ }, [renamingTodo, allTodos]);
const addTodo = () => {
const newTodo: Todo = {
id: Date.now(),
- title: todoTitle,
+ title: todoTitle.trim(),
completed: false,
};
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 6ac08b3c7..4eb30b93c 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -18,14 +18,16 @@ const Header: React.FC = () => {
return (
{/* this button should have `active` class only if all todos are completed */}
-
+ {allTodos.length > 0 && (
+
+ )}
);
diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx
index 438434aa3..3e2aff7f7 100644
--- a/src/components/TodoItem.tsx
+++ b/src/components/TodoItem.tsx
@@ -17,8 +17,6 @@ const TodoItem: React.FC = ({ todo }) => {
const isRenaming = renamingTodo === todo.id;
- // const [isRenaming, setIsRenaming] = useState(false);
-
const todoField = useRef(null);
const dispatch = useContext(DispatchContext);
@@ -47,7 +45,7 @@ const TodoItem: React.FC = ({ todo }) => {
type: 'updateTodo',
payload: {
...todo,
- title: inputValue,
+ title: inputValue.trim(),
},
});
}
@@ -55,7 +53,6 @@ const TodoItem: React.FC = ({ todo }) => {
const handleEditTodo = () => {
handleRenamingTodo(todo.id);
- // setIsRenaming(true);
setInputValue(todo.title);
};
@@ -66,7 +63,6 @@ const TodoItem: React.FC = ({ todo }) => {
const handleSubmitChange = (event: React.FormEvent) => {
event.preventDefault();
handleRenamingTodo(null);
- // setIsRenaming(false);
if (inputValue === '') {
handleDelete();
@@ -76,7 +72,6 @@ const TodoItem: React.FC = ({ todo }) => {
if (inputValue !== todo.title) {
updateTodo();
handleRenamingTodo(null);
- // setIsRenaming(false);
return;
}
@@ -86,7 +81,6 @@ const TodoItem: React.FC = ({ todo }) => {
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handleRenamingTodo(null);
- // setIsRenaming(false);
return;
}
@@ -131,9 +125,10 @@ const TodoItem: React.FC = ({ todo }) => {
)}
- {!!isRenaming && (
+ {isRenaming && (
)}
-
- ×
-
+ {!isRenaming && (
+
+ ×
+
+ )}
);
};
diff --git a/src/fakeTodos.ts b/src/fakeTodos.ts
deleted file mode 100644
index 44c0631e2..000000000
--- a/src/fakeTodos.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export const fakeTodos = [
- { id: 1, title: 'Buy some milk', completed: false },
- { id: 2, title: 'Call the doctor', completed: true },
- { id: 3, title: 'Pay water bill', completed: false },
- { id: 4, title: 'Buy a new phone', completed: true },
- { id: 5, title: 'Order pizza', completed: false },
- { id: 6, title: 'Finish the project', completed: false },
- { id: 7, title: 'Clean the house', completed: false },
- { id: 8, title: 'Walk the dog', completed: false },
- { id: 9, title: 'Go to the gym', completed: false },
- { id: 10, title: 'Read a book', completed: false },
-];
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
index b663354f5..bb5cabc4f 100644
--- a/src/hooks/useLocalStorage.ts
+++ b/src/hooks/useLocalStorage.ts
@@ -1,31 +1,16 @@
import { useState } from 'react';
-import { fakeTodos } from '../fakeTodos';
-type SerializableValue =
- | string
- | number
- | boolean
- | null
- | SerializableObject
- | SerializableArray;
-type SerializableObject = { [key: string]: SerializableValue };
-type SerializableArray = SerializableValue[];
-
-export const fakeLocalTodos = () => {
- return fakeTodos;
-};
-
-export const useLocalStorage = (
+export const useLocalStorage = (
key: string,
defaultValue: T,
): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState(() => {
try {
- const item = window.localStorage.getItem(key);
+ const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
- console.error(error);
+ localStorage.removeItem(key);
return defaultValue;
}
@@ -34,9 +19,9 @@ export const useLocalStorage = (
const setValue = (value: T) => {
try {
setStoredValue(value);
- window.localStorage.setItem(key, JSON.stringify(value));
+ localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
- console.error(error);
+ alert('Unable to save todo');
}
};
From 4a78c3af962e125e21107ad4c4ad2e1113ee05a4 Mon Sep 17 00:00:00 2001
From: Igor <4430704@gmail.com>
Date: Thu, 5 Dec 2024 10:31:20 +0200
Subject: [PATCH 3/5] add useEffect dependency
---
src/components/TodoItem.tsx | 23 ++++++++++++++++-------
1 file changed, 16 insertions(+), 7 deletions(-)
diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx
index 3e2aff7f7..8b042a337 100644
--- a/src/components/TodoItem.tsx
+++ b/src/components/TodoItem.tsx
@@ -3,7 +3,13 @@
import cn from 'classnames';
-import React, { useContext, useEffect, useRef, useState } from 'react';
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
import { Todo } from '../types/Todo';
import { DispatchContext, StateContext } from '../Store';
@@ -21,11 +27,14 @@ const TodoItem: React.FC = ({ todo }) => {
const dispatch = useContext(DispatchContext);
- const handleRenamingTodo = (id: number | null) => {
- if (dispatch) {
- dispatch({ type: 'renameTodo', payload: id });
- }
- };
+ const handleRenamingTodo = useCallback(
+ (id: number | null) => {
+ if (dispatch) {
+ dispatch({ type: 'renameTodo', payload: id });
+ }
+ },
+ [dispatch],
+ );
const handleDelete = () => {
if (dispatch) {
@@ -91,7 +100,7 @@ const TodoItem: React.FC = ({ todo }) => {
return () => {
window.removeEventListener('keyup', handleEsc);
};
- }, []);
+ }, [handleRenamingTodo]);
useEffect(() => {
if (todoField.current) {
From 5f3a46cbac0b646fc8ed31e43502e7afe024894f Mon Sep 17 00:00:00 2001
From: Igor <4430704@gmail.com>
Date: Thu, 5 Dec 2024 10:35:23 +0200
Subject: [PATCH 4/5] add readme link
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 903c876f9..0cc7cfb09 100644
--- a/README.md
+++ b/README.md
@@ -33,4 +33,4 @@ Implement a simple [TODO app](https://mate-academy.github.io/react_todo-app/) th
- Implement a solution following the [React task guidelines](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).
- Open another terminal and run tests with `npm test` to ensure your solution is correct.
-- Replace `` with your GitHub username in the [DEMO LINK](https://.github.io/react_todo-app/) and add it to the PR description.
+- Replace `` with your GitHub username in the [DEMO LINK](https://Krykunov.github.io/react_todo-app/) and add it to the PR description.
From 56eb9a90cae7f6acce76e2edc6b9d6906a78ef1f Mon Sep 17 00:00:00 2001
From: Igor <4430704@gmail.com>
Date: Mon, 9 Dec 2024 17:12:19 +0200
Subject: [PATCH 5/5] fix
---
.eslintrc.cjs | 61 ++++++++++++++++++++-------------
src/Store.tsx | 34 +++++++++---------
src/components/FilterButton.tsx | 4 +--
src/components/Footer.tsx | 4 +--
src/components/Form.tsx | 6 ++--
src/components/Header.tsx | 4 +--
src/components/TodoItem.tsx | 13 +++----
src/types/Todo.ts | 11 ++++++
8 files changed, 80 insertions(+), 57 deletions(-)
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index b51149cf5..84477da1a 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -5,7 +5,7 @@ module.exports = {
},
extends: [
'plugin:react/recommended',
- "plugin:react-hooks/recommended",
+ 'plugin:react-hooks/recommended',
'airbnb-typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
@@ -14,11 +14,11 @@ module.exports = {
],
overrides: [
{
- 'files': ['**/*.spec.jsx'],
- 'rules': {
+ files: ['**/*.spec.jsx'],
+ rules: {
'react/jsx-filename-extension': ['off'],
- }
- }
+ },
+ },
],
parser: '@typescript-eslint/parser',
parserOptions: {
@@ -34,18 +34,21 @@ module.exports = {
'import',
'react-hooks',
'@typescript-eslint',
- 'prettier'
+ 'prettier',
],
rules: {
// JS
- 'semi': 'off',
+ semi: 'off',
'@typescript-eslint/semi': ['error', 'always'],
'prefer-const': 2,
curly: [2, 'all'],
- 'max-len': ['error', {
- ignoreTemplateLiterals: true,
- ignoreComments: true,
- }],
+ 'max-len': [
+ 'error',
+ {
+ ignoreTemplateLiterals: true,
+ ignoreComments: true,
+ },
+ ],
'no-redeclare': [2, { builtinGlobals: true }],
'no-console': 2,
'operator-linebreak': 0,
@@ -57,7 +60,11 @@ module.exports = {
2,
{ blankLine: 'always', prev: '*', next: 'return' },
{ blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
- { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] },
+ {
+ blankLine: 'any',
+ prev: ['const', 'let', 'var'],
+ next: ['const', 'let', 'var'],
+ },
{ blankLine: 'always', prev: 'directive', next: '*' },
{ blankLine: 'always', prev: 'block-like', next: '*' },
],
@@ -73,16 +80,16 @@ module.exports = {
'react/jsx-props-no-spreading': 0,
'react/state-in-constructor': [2, 'never'],
'react-hooks/rules-of-hooks': 2,
- 'jsx-a11y/label-has-associated-control': ["error", {
- assert: "either",
- }],
- 'jsx-a11y/label-has-for': [2, {
- components: ['Label'],
- required: {
- some: ['id', 'nesting'],
+ 'jsx-a11y/label-has-for': [
+ 2,
+ {
+ components: ['Label'],
+ required: {
+ some: ['id', 'nesting'],
+ },
+ allowChildren: true,
},
- allowChildren: true,
- }],
+ ],
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
@@ -91,7 +98,9 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
'@typescript-eslint/indent': ['error', 2],
- '@typescript-eslint/ban-types': ['error', {
+ '@typescript-eslint/ban-types': [
+ 'error',
+ {
extendDefaults: true,
types: {
'{}': false,
@@ -99,7 +108,13 @@ module.exports = {
},
],
},
- ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts', 'src/vite-env.d.ts', 'cypress'],
+ ignorePatterns: [
+ 'dist',
+ '.eslintrc.cjs',
+ 'vite.config.ts',
+ 'src/vite-env.d.ts',
+ 'cypress',
+ ],
settings: {
react: {
version: 'detect',
diff --git a/src/Store.tsx b/src/Store.tsx
index 01f59bec1..1faf30d69 100644
--- a/src/Store.tsx
+++ b/src/Store.tsx
@@ -1,17 +1,17 @@
import React, { useEffect, useReducer } from 'react';
-import { Filters, Todo } from './types/Todo';
+import { Actions, Filters, Todo } from './types/Todo';
import { filterTodos } from './utils/services';
import { useLocalStorage } from './hooks/useLocalStorage';
type Action =
- | { type: 'filter'; payload: Filters }
- | { type: 'addTodo'; payload: Todo }
- | { type: 'updateTodo'; payload: Todo }
- | { type: 'toggleTodo'; payload: number }
- | { type: 'deleteTodo'; payload: number }
- | { type: 'toggleAllTodos' }
- | { type: 'clearCompleted' }
- | { type: 'renameTodo'; payload: number | null };
+ | { type: Actions.Filter; payload: Filters }
+ | { type: Actions.AddTodo; payload: Todo }
+ | { type: Actions.UpdateTodo; payload: Todo }
+ | { type: Actions.ToggleTodo; payload: number }
+ | { type: Actions.DeleteTodo; payload: number }
+ | { type: Actions.ToggleAllTodos }
+ | { type: Actions.ClearCompleted }
+ | { type: Actions.RenameTodo; payload: number | null };
interface State {
allTodos: Todo[];
@@ -24,12 +24,12 @@ const reducer = (state: State, action: Action): State => {
let updatedTodos = state.allTodos;
switch (action.type) {
- case 'addTodo': {
+ case Actions.AddTodo: {
updatedTodos = [...state.allTodos, action.payload];
break;
}
- case 'updateTodo': {
+ case Actions.UpdateTodo: {
updatedTodos = state.allTodos.map(todo =>
todo.id === action.payload.id
? { ...todo, title: action.payload.title }
@@ -38,17 +38,17 @@ const reducer = (state: State, action: Action): State => {
break;
}
- case 'deleteTodo': {
+ case Actions.DeleteTodo: {
updatedTodos = state.allTodos.filter(todo => todo.id !== action.payload);
break;
}
- case 'clearCompleted': {
+ case Actions.ClearCompleted: {
updatedTodos = state.allTodos.filter(todo => !todo.completed);
break;
}
- case 'toggleTodo': {
+ case Actions.ToggleTodo: {
updatedTodos = state.allTodos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
@@ -57,7 +57,7 @@ const reducer = (state: State, action: Action): State => {
break;
}
- case 'toggleAllTodos': {
+ case Actions.ToggleAllTodos: {
const isAllCompleted = state.allTodos.every(todo => todo.completed);
updatedTodos = state.allTodos.map(todo => ({
@@ -67,7 +67,7 @@ const reducer = (state: State, action: Action): State => {
break;
}
- case 'filter': {
+ case Actions.Filter: {
return {
...state,
activeFilter: action.payload,
@@ -75,7 +75,7 @@ const reducer = (state: State, action: Action): State => {
};
}
- case 'renameTodo': {
+ case Actions.RenameTodo: {
return {
...state,
renamingTodo: action.payload,
diff --git a/src/components/FilterButton.tsx b/src/components/FilterButton.tsx
index 385d50c60..5e756fb2a 100644
--- a/src/components/FilterButton.tsx
+++ b/src/components/FilterButton.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
-import { Filters } from '../types/Todo';
+import { Actions, Filters } from '../types/Todo';
import { DispatchContext, StateContext } from '../Store';
type Props = {
@@ -19,7 +19,7 @@ const FilterButton: React.FC = ({ filterItem }) => {
const handleFilterClick = () => {
if (!isSelectedFilter) {
if (dispatch) {
- dispatch({ type: 'filter', payload: filterItem });
+ dispatch({ type: Actions.Filter, payload: filterItem });
}
}
};
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index 23c5d938b..db3ec198c 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { Filters } from '../types/Todo';
+import { Actions, Filters } from '../types/Todo';
import FilterButton from './FilterButton';
import { DispatchContext, StateContext } from '../Store';
@@ -11,7 +11,7 @@ const Footer: React.FC = ({}) => {
const handleClearCompleted = () => {
if (dispatch) {
- dispatch({ type: 'clearCompleted' });
+ dispatch({ type: Actions.ClearCompleted });
}
};
diff --git a/src/components/Form.tsx b/src/components/Form.tsx
index 6cf834625..83c819312 100644
--- a/src/components/Form.tsx
+++ b/src/components/Form.tsx
@@ -1,6 +1,6 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import { DispatchContext, StateContext } from '../Store';
-import { Todo } from '../types/Todo';
+import { Actions, Todo } from '../types/Todo';
const Form: React.FC = () => {
const [todoTitle, setTodoTitle] = useState('');
@@ -26,7 +26,7 @@ const Form: React.FC = () => {
};
if (dispatch) {
- dispatch({ type: 'addTodo', payload: newTodo });
+ dispatch({ type: Actions.AddTodo, payload: newTodo });
}
};
@@ -36,7 +36,7 @@ const Form: React.FC = () => {
const handleFormSubmit = (event: React.FormEvent) => {
event.preventDefault();
- if (todoTitle.trim() === '') {
+ if (!todoTitle.trim()) {
return;
}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 4eb30b93c..6621f18cb 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -2,6 +2,7 @@ import React, { useContext } from 'react';
import cn from 'classnames';
import Form from './Form';
import { DispatchContext, StateContext } from '../Store';
+import { Actions } from '../types/Todo';
const Header: React.FC = () => {
const { allTodos } = useContext(StateContext);
@@ -11,13 +12,12 @@ const Header: React.FC = () => {
const handleToggleAll = () => {
if (dispatch) {
- dispatch({ type: 'toggleAllTodos' });
+ dispatch({ type: Actions.ToggleAllTodos });
}
};
return (
- {/* this button should have `active` class only if all todos are completed */}
{allTodos.length > 0 && (
= ({ todo }) => {
const handleRenamingTodo = useCallback(
(id: number | null) => {
if (dispatch) {
- dispatch({ type: 'renameTodo', payload: id });
+ dispatch({ type: Actions.RenameTodo, payload: id });
}
},
[dispatch],
@@ -38,20 +35,20 @@ const TodoItem: React.FC = ({ todo }) => {
const handleDelete = () => {
if (dispatch) {
- dispatch({ type: 'deleteTodo', payload: todo.id });
+ dispatch({ type: Actions.DeleteTodo, payload: todo.id });
}
};
const handleToggle = () => {
if (dispatch) {
- dispatch({ type: 'toggleTodo', payload: todo.id });
+ dispatch({ type: Actions.ToggleTodo, payload: todo.id });
}
};
const updateTodo = () => {
if (dispatch) {
dispatch({
- type: 'updateTodo',
+ type: Actions.UpdateTodo,
payload: {
...todo,
title: inputValue.trim(),
diff --git a/src/types/Todo.ts b/src/types/Todo.ts
index 7378f22a1..64a0e73dc 100644
--- a/src/types/Todo.ts
+++ b/src/types/Todo.ts
@@ -9,3 +9,14 @@ export enum Filters {
Active = 'active',
Completed = 'completed',
}
+
+export enum Actions {
+ AddTodo,
+ UpdateTodo,
+ DeleteTodo,
+ ClearCompleted,
+ ToggleTodo,
+ ToggleAllTodos,
+ Filter,
+ RenameTodo,
+}