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 */} -
+
- {/* This is a completed todo */} -
- - - - Completed Todo - - - {/* Remove button appears only on hover */} - -
- - {/* 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 - - - -
+ {todos.map(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 */} - -
+ {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 + + + + + +
+ ); +}; + +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 */} -
); 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 && ( = ({ todo }) => { )} - + {!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 && (