From 8ea49d47217577ae8359929c68694331d6283f48 Mon Sep 17 00:00:00 2001 From: Dmytro Loboda Date: Sat, 1 Jun 2024 03:31:11 +0800 Subject: [PATCH 1/5] todo app solution --- src/App.tsx | 176 ++++++------------------------ src/Storage/storageFiles.tsx | 200 +++++++++++++++++++++++++++++++++++ src/components/Footer.tsx | 66 ++++++++++++ src/components/Header.tsx | 68 ++++++++++++ src/components/Todos.tsx | 97 +++++++++++++++++ src/index.tsx | 11 +- src/styles/index.scss | 4 + src/types/Todo.ts | 6 ++ 8 files changed, 480 insertions(+), 148 deletions(-) create mode 100644 src/Storage/storageFiles.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/Todos.tsx create mode 100644 src/types/Todo.ts diff --git a/src/App.tsx b/src/App.tsx index a399287bd..9de3c734e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,156 +1,44 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; +import React, { useContext, useEffect, useRef } from 'react'; +import { Todos } from './components/Todos'; +import { Header } from './components/Header'; +import { Footer } from './components/Footer'; +import { DispatchContext, StateContext } from './Storage/storageFiles'; +import { Todo } from './types/Todo'; -export const App: React.FC = () => { - 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 */} -
- +const getTodosFromLocalStorage: Todo[] = JSON.parse( + localStorage.getItem('todos') || '[]', +); - - 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 - +export const App: React.FC = () => { + const { todos } = useContext(StateContext); + const dispatch = useContext(DispatchContext); - -
-
+ useEffect(() => { + if (todos.length === 0) { + dispatch({ type: 'setTodos', tod: getTodosFromLocalStorage }); + } + }, []); + const temper = useRef(true); - {/* Hide the footer if there are no todos */} -
- - 3 items left - + useEffect(() => { + if (temper.current) { + temper.current = false; - {/* Active link should have the 'selected' class */} - + return ( +
+

todos

- {/* this button should be disabled if there are no completed todos */} - -
+
+
+ +
); diff --git a/src/Storage/storageFiles.tsx b/src/Storage/storageFiles.tsx new file mode 100644 index 000000000..740199045 --- /dev/null +++ b/src/Storage/storageFiles.tsx @@ -0,0 +1,200 @@ +import React, { useReducer } from 'react'; +import { Todo } from '../types/Todo'; + +interface State { + todos: Todo[]; + newTodo: string; + focusNewTodo: boolean; + useTodos: 'All' | 'Active' | 'Completed'; + changerTodo: string; +} + +type Action = + | { type: 'add' } + | { type: 'changeTodo'; text: string } + | { type: 'remove'; id: number } + | { type: 'checked'; id: number } + | { type: 'setChanged'; id: number } + | { type: 'changed'; id: number; text: string } + | { type: 'setAllCompleate'; use: boolean } + | { type: 'setUseTodos'; name: 'All' | 'Active' | 'Completed' } + | { type: 'clearAll' } + | { type: 'setTodos'; tod: Todo[] } + | { type: 'setFocudNewTodo' } + | { type: 'escapeChangedText'; id: number }; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'changeTodo': + return { + ...state, + newTodo: action.text, + }; + + case 'add': + return { + ...state, + todos: [ + ...state.todos, + { + id: +new Date(), + title: state.newTodo.trim(), + completed: false, + changed: false, + }, + ], + }; + + case 'remove': + return { + ...state, + todos: [...state.todos.filter(todo => todo.id !== action.id)], + focusNewTodo: !state.focusNewTodo, + }; + + case 'checked': + return { + ...state, + todos: [ + ...state.todos.map(todo => { + if (todo.id === action.id) { + return { + ...todo, + completed: !todo.completed, + }; + } else { + return todo; + } + }), + ], + }; + + case 'setChanged': + return { + ...state, + todos: [ + ...state.todos + .map(todo => { + if (todo.id === action.id) { + return { + ...todo, + changed: !todo.changed, + title: todo.title.trim(), + }; + } else { + return todo; + } + }) + .filter(todo => todo.title), + ], + changerTodo: + state.todos.find(todo => todo.id === action.id)?.title || '', + }; + + case 'changed': + return { + ...state, + todos: [ + ...state.todos.map(todo => { + if (todo.id === action.id) { + return { + ...todo, + title: action.text, + }; + } else { + return todo; + } + }), + ], + }; + + case 'setAllCompleate': + return { + ...state, + todos: [ + ...state.todos.map(todo => { + return { + ...todo, + completed: !action.use, + }; + }), + ], + }; + + case 'setUseTodos': + return { + ...state, + useTodos: action.name, + }; + + case 'clearAll': + return { + ...state, + todos: [ + ...state.todos.filter(todo => { + return !todo.completed; + }), + ], + focusNewTodo: !state.focusNewTodo, + }; + + case 'setTodos': + return { + ...state, + todos: [...action.tod], + }; + + case 'setFocudNewTodo': + return { + ...state, + focusNewTodo: !state.focusNewTodo, + }; + + case 'escapeChangedText': + return { + ...state, + todos: [ + ...state.todos.map(todo => { + if (todo.id === action.id) { + return { + ...todo, + title: state.changerTodo, + }; + } else { + return todo; + } + }), + ], + }; + + default: + return state; + } +}; + +const initialState: State = { + todos: [], + newTodo: '', + useTodos: 'All', + focusNewTodo: true, + changerTodo: '', +}; + +const defaultDispatch: React.Dispatch = () => {}; + +export const StateContext = React.createContext(initialState); +export const DispatchContext = React.createContext(defaultDispatch); + +type Props = { + children: React.ReactNode; +}; + +export const GlobalstateProvider: React.FC = ({ children }) => { + const [state, dispatch] = useReducer(reducer, initialState); + + return ( + + {children} + + ); +}; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 000000000..2f297bc4c --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,66 @@ +import { useContext } from 'react'; +import { DispatchContext, StateContext } from '../Storage/storageFiles'; +import cn from 'classnames'; + +export const Footer = () => { + const { todos, useTodos } = useContext(StateContext); + const dispatch = useContext(DispatchContext); + const todosLeft = todos.filter(todo => !todo.completed).length; + + return ( + <> + {todos.length !== 0 && ( + + )} + + ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 000000000..ab77ab5cc --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,68 @@ +import React, { useContext, useRef } from 'react'; +import { DispatchContext, StateContext } from '../Storage/storageFiles'; +import cn from 'classnames'; + +export const Header = () => { + const dispatch = useContext(DispatchContext); + const { todos, newTodo, focusNewTodo } = useContext(StateContext); + + const allTodosComplete = todos.reduce((prev, todo) => { + return prev && todo.completed; + }, true); + + const addNewTodo = (e: React.FormEvent) => { + e.preventDefault(); + + if (newTodo.trim()) { + dispatch({ type: 'add' }); + dispatch({ + type: 'changeTodo', + text: '', + }); + } + }; + + const inputRef = useRef(null); + + if (focusNewTodo) { + inputRef.current?.focus(); + } else { + inputRef.current?.blur(); + } + + return ( +
+ {todos.length > 0 && ( +
+ ); +}; diff --git a/src/components/Todos.tsx b/src/components/Todos.tsx new file mode 100644 index 000000000..8c3f47782 --- /dev/null +++ b/src/components/Todos.tsx @@ -0,0 +1,97 @@ +import { useContext } from 'react'; +import { DispatchContext, StateContext } from '../Storage/storageFiles'; +import cn from 'classnames'; + +export const Todos = () => { + const dispatch = useContext(DispatchContext); + const { todos, useTodos } = useContext(StateContext); + + const todosFilter = todos.filter(todo => { + if (useTodos === 'Active') { + return !todo.completed; + } + + if (useTodos === 'Completed') { + return todo.completed; + } + + return true; + }); + + return ( +
+ {todosFilter.map(todo => ( +
+ + + {!todo.changed && ( + + dispatch({ type: 'setChanged', id: todo.id }) + } + > + {todo.title} + + )} + + {todo.changed && ( +
{ + e.preventDefault(); + dispatch({ type: 'setChanged', id: todo.id }); + }} + > + { + if (e.key === 'Escape') { + dispatch({ type: 'escapeChangedText', id: todo.id }); + dispatch({ type: 'setChanged', id: todo.id }); + } + }} + autoFocus + onChange={e => + dispatch({ + type: 'changed', + id: todo.id, + text: e.target.value, + }) + } + onBlur={() => dispatch({ type: 'setChanged', id: todo.id })} + /> +
+ )} + + {!todo.changed && ( + + )} +
+ ))} +
+ ); +}; diff --git a/src/index.tsx b/src/index.tsx index a9689cb38..0310f3210 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 './Storage/storageFiles'; 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..72904c4f1 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,3 +1,7 @@ +* { + box-sizing: border-box; +} + iframe { display: none; } diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 000000000..03abaacf5 --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,6 @@ +export type Todo = { + id: number; + title: string; + completed: boolean; + changed: boolean; +}; From c47ade5613fddecae3c9056592cf0eb905ea904c Mon Sep 17 00:00:00 2001 From: Dmytro Loboda Date: Wed, 25 Sep 2024 03:53:30 +0800 Subject: [PATCH 2/5] TodoApp v.2 --- src/App.tsx | 23 +++++----- src/components/Footer.tsx | 7 ++- src/components/Header.tsx | 16 +++---- src/components/Todo.tsx | 87 ++++++++++++++++++++++++++++++++++++ src/components/Todos.tsx | 94 ++++++++++----------------------------- 5 files changed, 137 insertions(+), 90 deletions(-) create mode 100644 src/components/Todo.tsx diff --git a/src/App.tsx b/src/App.tsx index 9de3c734e..69ce14bd6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,28 +6,29 @@ import { Footer } from './components/Footer'; import { DispatchContext, StateContext } from './Storage/storageFiles'; import { Todo } from './types/Todo'; -const getTodosFromLocalStorage: Todo[] = JSON.parse( - localStorage.getItem('todos') || '[]', -); - export const App: React.FC = () => { const { todos } = useContext(StateContext); const dispatch = useContext(DispatchContext); - - useEffect(() => { - if (todos.length === 0) { - dispatch({ type: 'setTodos', tod: getTodosFromLocalStorage }); - } - }, []); const temper = useRef(true); useEffect(() => { if (temper.current) { temper.current = false; - return; + try { + const storedTodos: Todo[] = JSON.parse( + localStorage.getItem('todos') || '[]', + ); + + dispatch({ type: 'setTodos', tod: storedTodos }); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to load todos from localStorage:', error); + } } + }, [dispatch]); + useEffect(() => { localStorage.setItem('todos', JSON.stringify(todos)); }, [todos]); diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 2f297bc4c..fa3d607cf 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -12,7 +12,7 @@ export const Footer = () => { {todos.length !== 0 && (