From 678eb9ccc83255f61691a2bbff945f1a0dd7cb86 Mon Sep 17 00:00:00 2001 From: Mariana Trinko Date: Sun, 12 Jan 2025 18:47:09 +0100 Subject: [PATCH 1/7] message --- src/App.tsx | 384 +++++++++++++++++++++------------- src/components/TodoItem.tsx | 29 +++ src/components/TodoList.tsx | 16 ++ src/hooks/useLocalStorage.tsx | 58 +++++ src/index.tsx | 7 +- src/store/TodoContext.tsx | 33 +++ src/types/Todo.ts | 5 + 7 files changed, 380 insertions(+), 152 deletions(-) create mode 100644 src/components/TodoItem.tsx create mode 100644 src/components/TodoList.tsx create mode 100644 src/hooks/useLocalStorage.tsx create mode 100644 src/store/TodoContext.tsx create mode 100644 src/types/Todo.ts diff --git a/src/App.tsx b/src/App.tsx index a399287bd..f05c82891 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,157 +1,239 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; - +/* eslint-disable object-curly-newline */ /* eslint-disable @typescript-eslint/quotes */ /* eslint-disable jsx-a11y/control-has-associated-label */ +import React, { ChangeEvent, FormEvent, useContext, useState } from 'react'; +import './styles/todoapp.scss'; +import { Todo } from './types/Todo'; +import { TodoContext } from './store/TodoContext'; +import { TodoList } from './components/TodoList'; export const App: React.FC = () => { + const { todos, setTodos } = useContext(TodoContext); + const [title, setTitle] = useState(''); + + function handleTitleChange(event: ChangeEvent) { + setTitle(event.target.value); + } + + function addTodo(newTodo: Todo) { + setTodos([...todos, newTodo]); + } + + function onFormSubmit(event: FormEvent) { + event.preventDefault(); + const newTodo: Todo = { id: +new Date(), title, completed: false }; + + addTodo(newTodo); + } + 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 - - - -
-
- - {/* 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 */} - -
-
+ {' '} +
+

todos {title} 

+
+ {' '} + {' '} +
{' '} +
+ +
+ {' '} + +    {' '} +
+ {' '}
); }; + +// /* eslint-disable jsx-a11y/control-has-associated-label */ +// import React from 'react'; + +// import './styles/todoapp.scss'; + +// 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 */} +//
+// + +// +// 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 +// + +// +//
+//
+ +// {/* 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 */} +// +//
+//
+//
+// ); +// }; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 000000000..3b867cc24 --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,29 @@ +/* eslint-disable prettier/prettier */ +import { Todo } from '../types/Todo'; + +/* eslint-disable import/extensions */ +type Props = { todo: Todo }; +export const TodoItem: React.FC = ({ todo }) => { + return ( +
  • + {' '} +
    + {' '} + useLocalStorageToggleCompleted(todo)} + /> + +
    + +
  • + ); +}; diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx new file mode 100644 index 000000000..5096fe36c --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,16 @@ +/* eslint-disable import/extensions */ +/* eslint-disable @typescript-eslint/quotes */ import React from 'react'; +import '../styles/todo-list.css'; + +import { TodoItem } from './TodoItem'; +import { Todo } from '../types/Todo'; +type Props = { todos: Todo[] }; +export const TodoList: React.FC = ({ todos }) => { + return ( +
      + {todos.map(todo => ( + + ))} +
    + ); +}; diff --git a/src/hooks/useLocalStorage.tsx b/src/hooks/useLocalStorage.tsx new file mode 100644 index 000000000..df2ebd9f0 --- /dev/null +++ b/src/hooks/useLocalStorage.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import { Todo } from '../types/Todo'; + +export function useLocalStorage( + key: string, + startTodo: Todo, +): [Todo, (t: Todo) => void] { + const [todo, setTodo] = useState(() => { + const data = localStorage.getItem(key); + + if (data === null) { + localStorage.setItem(key, JSON.stringify(startTodo)); + + return startTodo; + } + + try { + return JSON.parse(data); + } catch (e) { + localStorage.removeItem(key); + + return startTodo; + } + }); + + const save = (newTodo: Todo) => { + localStorage.setItem(key, JSON.stringify(newTodo)); + setTodo(newTodo); + }; + + return [todo, save]; +} + +export function useLocalStorageRemoveTodo(todo: Todo) { + const todos: Todo[] = JSON.parse(localStorage.getItem('todos') || ''); + const index = todos.findIndex(t => t.id === todo.id); + + todos.splice(index, 1); + const todosToStr = JSON.stringify(todos); + + localStorage.setItem('todos', todosToStr); +} + +export function useLocalStorageToggleCompleted(todo: Todo) { + const todos: Todo[] = JSON.parse(localStorage.getItem('todos') || ''); + const index = todos.findIndex(t => t.id === todo.id); + + todos[index].completed = !todos[index].completed; + const todosToStr = JSON.stringify(todos); + + localStorage.setItem('todos', todosToStr); +} +// export function useLocalStorage(key: string, startValue: T) {//   const [value, setValue] = useState(() => {//     const data = localStorage.getItem(key); +//     if (data === null) {//       return startValue;//     } +//     try {//       return JSON.parse(data);//     } catch (e) {//       localStorage.removeItem(key); +//       return startValue;//     }//   }); +//   const save = (newValue: T) => {//     localStorage.setItem(key, JSON.stringify(newValue));//     setValue(newValue);//   }; +//   return [value, save];// } diff --git a/src/index.tsx b/src/index.tsx index a9689cb38..17628ea8e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,7 +5,12 @@ import './styles/todo-list.css'; import './styles/filters.css'; import { App } from './App'; +import { TodoProvider } from './store/TodoContext'; const container = document.getElementById('root') as HTMLDivElement; -createRoot(container).render(); +createRoot(container).render( + + + , +); diff --git a/src/store/TodoContext.tsx b/src/store/TodoContext.tsx new file mode 100644 index 000000000..a7fb99112 --- /dev/null +++ b/src/store/TodoContext.tsx @@ -0,0 +1,33 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { useMemo, useState } from 'react'; +import { Todo } from '../types/Todo'; +import { useLocalStorage } from '../hooks/useLocalStorage'; + +type T = { + todos: Todo[]; + setTodos: (_todos: Todo[]) => void; +}; + +export const TodoContext = React.createContext({ + todos: [], + setTodos: (_todos: Todo[]) => {}, +}); + +type Props = { + children: React.ReactNode; +}; + +export const TodoProvider: React.FC = ({ children }) => { + const [todos, setTodos] = useState(useLocalStorage('todos', [])); + + const value = useMemo( + () => ({ + todos, + setTodos, + }), + [todos, setTodos], + ); + + return {children}; +}; diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 000000000..f9e06b381 --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,5 @@ +export interface Todo { + id: number; + title: string; + completed: boolean; +} From b599f0506ff1b1c656bf3e4cf1473d586318971c Mon Sep 17 00:00:00 2001 From: Mariana Trinko Date: Thu, 16 Jan 2025 09:34:29 +0100 Subject: [PATCH 2/7] message --- src/App.tsx | 238 ++----------------------------- src/components/Footer.tsx | 93 ++++++++++++ src/components/Header.tsx | 110 ++++++++++++++ src/components/TodoApp.scss | 122 ++++++++++++++++ src/components/TodoApp.tsx | 3 + src/components/TodoItem.tsx | 163 ++++++++++++++++++--- src/components/TodoList.tsx | 19 +-- src/index.tsx | 5 +- src/store/FilterdTodoContext.tsx | 50 +++++++ src/store/TodoContext.tsx | 8 +- src/types/Status.ts | 5 + src/utils/completedTodos.ts | 5 + 12 files changed, 560 insertions(+), 261 deletions(-) create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/TodoApp.scss create mode 100644 src/components/TodoApp.tsx create mode 100644 src/store/FilterdTodoContext.tsx create mode 100644 src/types/Status.ts create mode 100644 src/utils/completedTodos.ts diff --git a/src/App.tsx b/src/App.tsx index f05c82891..793cc7e9a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,239 +1,19 @@ /* eslint-disable object-curly-newline */ /* eslint-disable @typescript-eslint/quotes */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React, { ChangeEvent, FormEvent, useContext, useState } from 'react'; +import React, { useContext } from 'react'; import './styles/todoapp.scss'; -import { Todo } from './types/Todo'; -import { TodoContext } from './store/TodoContext'; import { TodoList } from './components/TodoList'; +import { Header } from './components/Header'; +import { Footer } from './components/Footer'; +import { FilteredTodoContext } from './store/FilterdTodoContext'; +import { TodoContext } from './store/TodoContext'; export const App: React.FC = () => { - const { todos, setTodos } = useContext(TodoContext); - const [title, setTitle] = useState(''); - - function handleTitleChange(event: ChangeEvent) { - setTitle(event.target.value); - } - - function addTodo(newTodo: Todo) { - setTodos([...todos, newTodo]); - } - - function onFormSubmit(event: FormEvent) { - event.preventDefault(); - const newTodo: Todo = { id: +new Date(), title, completed: false }; - - addTodo(newTodo); - } + const { todos } = useContext(TodoContext); + const { filteredTodos } = useContext(FilteredTodoContext); return (
    - {' '} -
    -

    todos {title} 

    -
    - {' '} - {' '} -
    {' '} -
    - -
    - {' '} - -    {' '} -
    - {' '} +
    {todos.length && } + {todos.length &&
    }
    ); }; - -// /* eslint-disable jsx-a11y/control-has-associated-label */ -// import React from 'react'; - -// import './styles/todoapp.scss'; - -// 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 */} -//
    -// - -// -// 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 -// - -// -//
    -//
    - -// {/* 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 */} -// -//
    -//
    -//
    -// ); -// }; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 000000000..0bc33a047 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,93 @@ +import { useContext } from 'react'; +import { TodoContext } from '../store/TodoContext'; +import './TodoApp.scss'; +import classNames from 'classnames'; +import { Status } from '../types/Status'; +import { FilteredTodoContext } from '../store/FilterdTodoContext'; + +export const Footer = () => { + const { todos, setTodos } = useContext(TodoContext); + const { status, setStatus, filteredTodos, setfilteredTodos } = + useContext(FilteredTodoContext); + + const notCompletedTodos = todos.filter(t => t.completed === false); + const completedTodos = todos.filter(t => t.completed === true); + + const handleChooseAll = () => { + setfilteredTodos(todos); + setStatus(Status.all); + }; + + const handleChooseActive = () => { + setfilteredTodos(todos.filter(t => t.completed === false)); + setStatus(Status.active); + }; + + const handleChooseCompleted = () => { + setfilteredTodos(todos.filter(t => t.completed === true)); + setStatus(Status.completed); + }; + + function handleClearCompleted() { + const tempTodos = todos.filter(todo => todo.completed === false); + + setTodos(tempTodos); + setfilteredTodos(tempTodos); + } + + return ( +
    + + {notCompletedTodos.length} items left + + + {/* Active link should have the 'selected' class */} + + + {/* this button should be disabled if there are no completed todos */} + +
    + ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 000000000..eb002df4c --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,110 @@ +/* eslint-disable no-param-reassign */ +import { + ChangeEvent, + FormEvent, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import './TodoApp.scss'; + +import { TodoContext } from '../store/TodoContext'; +import { Todo } from '../types/Todo'; +import { FilteredTodoContext } from '../store/FilterdTodoContext'; + +export const Header = () => { + const { todos, setTodos } = useContext(TodoContext); + const [title, setTitle] = useState(''); + const [isToggle, setIsToggle] = useState(false); + const { setfilteredTodos } = useContext(FilteredTodoContext); + const titleField = useRef(null); + + useEffect(() => { + titleField.current?.focus(); + }, [todos]); + + const completedTodos = + todos.filter(t => t.completed === true).length === todos.length + ? true + : false; + + function handleTitleChange(event: ChangeEvent) { + setTitle(event.target.value); + } + + function onFormSubmit(event: FormEvent) { + event.preventDefault(); + if (!title.trim()) { + setTitle(''); + + return; + } + + const newTodo: Todo = { + id: +new Date(), + title: title.trim(), + completed: false, + }; + + setTodos([...todos, newTodo]); + setfilteredTodos([...todos, newTodo]); + + setTitle(''); + } + + function handleToggleAll() { + if (!isToggle && completedTodos) { + todos.forEach(t => { + t.completed = true; + }); + setIsToggle(true); + } + + if (isToggle && completedTodos) { + todos.forEach(t => { + t.completed = false; + }); + setIsToggle(false); + } + + if (!completedTodos) { + todos.forEach(t => { + t.completed = true; + }); + setIsToggle(true); + } + + setfilteredTodos(todos); + + setTodos([...todos]); + } + + return ( +
    + {/* this button should have `active` class only if all todos are completed */} + {todos.length && ( +
    + ); +}; diff --git a/src/components/TodoApp.scss b/src/components/TodoApp.scss new file mode 100644 index 000000000..f55c9b3e8 --- /dev/null +++ b/src/components/TodoApp.scss @@ -0,0 +1,122 @@ +.todo { + position: relative; + + display: grid; + grid-template-columns: 45px 1fr; + justify-items: stretch; + + font-size: 24px; + line-height: 1.4em; + border-bottom: 1px solid #ededed; + + &:last-child { + border-bottom: 0; + } + + &__status-label { + cursor: pointer; + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; + } + + &.completed &__status-label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); + } + + &__status { + opacity: 0; + } + + &__title { + padding: 12px 15px; + + word-break: break-all; + transition: color 0.4s; + } + + &.completed &__title { + color: #d9d9d9; + text-decoration: line-through; + } + + &__remove { + position: absolute; + right: 12px; + top: 0; + bottom: 0; + + font-size: 120%; + line-height: 1; + font-family: inherit; + font-weight: inherit; + color: #cc9a9a; + + float: right; + border: 0; + background: none; + cursor: pointer; + + transform: translateY(-2px); + opacity: 0; + transition: color 0.2s ease-out; + + &:hover { + color: #af5b5e; + } + } + + &:hover &__remove { + opacity: 1; + } + + &__title-field { + width: 100%; + padding: 11px 14px; + + font-size: inherit; + line-height: inherit; + font-family: inherit; + font-weight: inherit; + color: inherit; + + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + + &::placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; + } + } + + .overlay { + position: absolute; + inset: 0; + + opacity: 0.5; + } +} + +.filter { + display: flex; + + &__link { + margin: 3px; + padding: 3px 7px; + + color: inherit; + text-decoration: none; + + border: 1px solid transparent; + border-radius: 3px; + + &:hover { + border-color: rgba(175, 47, 47, 0.1); + } + + &.selected { + border-color: rgba(175, 47, 47, 0.2); + } + } +} diff --git a/src/components/TodoApp.tsx b/src/components/TodoApp.tsx new file mode 100644 index 000000000..93b03cb5e --- /dev/null +++ b/src/components/TodoApp.tsx @@ -0,0 +1,3 @@ +export const TodoApp = () => { + return
    ; +}; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 3b867cc24..15494cf83 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -1,29 +1,152 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable prettier/prettier */ +import { ChangeEvent, useContext, useState } from 'react'; import { Todo } from '../types/Todo'; +import { TodoContext } from '../store/TodoContext'; +import './TodoApp.scss'; +import classNames from 'classnames'; +import { FilteredTodoContext } from '../store/FilterdTodoContext'; +import { Status } from '../types/Status'; /* eslint-disable import/extensions */ type Props = { todo: Todo }; + +function filterTodos(isCompleted) { + switch (isCompleted) { + case isCompleted: + return Status.completed; + case !isCompleted: + return Status.active; + default: + return Status.all; + } +} + export const TodoItem: React.FC = ({ todo }) => { + const { todos, setTodos } = useContext(TodoContext); + const { filteredTodos, setfilteredTodos, status, setStatus } = + useContext(FilteredTodoContext); + const index = todos.findIndex(t => t.id === todo.id); + const [isEdit, setIsEdit] = useState(false); + const [tempTitle, setTempTitle] = useState(todo.title); + + const handleTitleChange = (event: ChangeEvent) => { + setTempTitle(event.target.value); + }; + + function handleToggleCompleted() { + const isCompleted = !todos[index].completed; + + todos[index].completed = isCompleted; + + setTodos(todos); + setfilteredTodos([...todos].filter(t => t.completed === !isCompleted)); + } + + function handleDoubleClick() { + setIsEdit(true); + } + + function handleRemoveTodo() { + todos.splice(index, 1); + setTodos([...todos]); + setfilteredTodos(todos); + } + + function onEditFormSubmit() { + // event.preventDefault(); + if (!tempTitle.trim()) { + handleRemoveTodo(); + } + + if (tempTitle.trim()) { + todos[index].title = tempTitle.trim(); + } + + setTodos([...todos]); + } + + function handleOnBlur() { + // event.preventDefault(); + if (!tempTitle.trim()) { + handleRemoveTodo(); + } + + if (tempTitle.trim()) { + todos[index].title = tempTitle.trim(); + } + + setTodos([...todos]); + setIsEdit(false); + } + + function handleCancelClick(event: React.KeyboardEvent) { + if (event.key === 'Escape') { + setIsEdit(false); + } + } + return ( -
  • - {' '} -
    - {' '} - useLocalStorageToggleCompleted(todo)} - /> - -
    - -
  • + <> + {!isEdit ? ( +
    + + + + {todo.title} + + + {/* Remove button appears only on hover */} + +
    + ) : ( +
    + + + {/* This form is shown instead of the title and remove button */} +
    + +
    +
    + )} + ); }; diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx index 5096fe36c..a860cb615 100644 --- a/src/components/TodoList.tsx +++ b/src/components/TodoList.tsx @@ -1,16 +1,19 @@ /* eslint-disable import/extensions */ -/* eslint-disable @typescript-eslint/quotes */ import React from 'react'; -import '../styles/todo-list.css'; +/* eslint-disable @typescript-eslint/quotes */ +import { useContext } from 'react'; +import './TodoApp.scss'; import { TodoItem } from './TodoItem'; -import { Todo } from '../types/Todo'; -type Props = { todos: Todo[] }; -export const TodoList: React.FC = ({ todos }) => { +import { FilteredTodoContext } from '../store/FilterdTodoContext'; +// type Props = { todos: Todo[] }; +export const TodoList = () => { + const { filteredTodos } = useContext(FilteredTodoContext); + return ( -
      - {todos.map(todo => ( +
      + {filteredTodos.map(todo => ( ))} -
    + ); }; diff --git a/src/index.tsx b/src/index.tsx index 17628ea8e..83baa14b0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,11 +6,14 @@ import './styles/filters.css'; import { App } from './App'; import { TodoProvider } from './store/TodoContext'; +import { FilteredTodoProvider } from './store/FilterdTodoContext'; const container = document.getElementById('root') as HTMLDivElement; createRoot(container).render( - + + + , ); diff --git a/src/store/FilterdTodoContext.tsx b/src/store/FilterdTodoContext.tsx new file mode 100644 index 000000000..7a2c2dbb5 --- /dev/null +++ b/src/store/FilterdTodoContext.tsx @@ -0,0 +1,50 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { useContext, useMemo, useState } from 'react'; +import { Todo } from '../types/Todo'; +import { TodoContext } from './TodoContext'; +import { Status } from '../types/Status'; + +type T = { + status: Status; + setStatus: (status: Status) => void; + todos: Todo[]; + filteredTodos: Todo[]; + setfilteredTodos: (todos: Todo[]) => void; +}; + +export const FilteredTodoContext = React.createContext({ + status: Status.all, + setStatus: (_status: Status) => {}, + todos: [], + filteredTodos: [], + setfilteredTodos: (_todos: Todo[]) => {}, +}); + +type Props = { + children: React.ReactNode; +}; + +export const FilteredTodoProvider: React.FC = ({ children }) => { + const { todos } = useContext(TodoContext); + + const [filteredTodos, setfilteredTodos] = useState(todos); + const [status, setStatus] = useState(Status.all); + + const value = useMemo( + () => ({ + status, + setStatus, + todos, + filteredTodos, + setfilteredTodos, + }), + [filteredTodos, setfilteredTodos, todos, status, setStatus], + ); + + return ( + + {children} + + ); +}; diff --git a/src/store/TodoContext.tsx b/src/store/TodoContext.tsx index a7fb99112..6e48a5fde 100644 --- a/src/store/TodoContext.tsx +++ b/src/store/TodoContext.tsx @@ -1,17 +1,19 @@ /* eslint-disable prettier/prettier */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Todo } from '../types/Todo'; import { useLocalStorage } from '../hooks/useLocalStorage'; type T = { todos: Todo[]; - setTodos: (_todos: Todo[]) => void; + setTodos: (todos: Todo[]) => void; + // addTodo: (todo: Todo) => void; }; export const TodoContext = React.createContext({ todos: [], setTodos: (_todos: Todo[]) => {}, + // addTodo: (_todo: Todo) => {}, }); type Props = { @@ -19,7 +21,7 @@ type Props = { }; export const TodoProvider: React.FC = ({ children }) => { - const [todos, setTodos] = useState(useLocalStorage('todos', [])); + const [todos, setTodos] = useLocalStorage('todos', []); const value = useMemo( () => ({ diff --git a/src/types/Status.ts b/src/types/Status.ts new file mode 100644 index 000000000..1e0bd526c --- /dev/null +++ b/src/types/Status.ts @@ -0,0 +1,5 @@ +export enum Status { + all = 'All', + active = 'Active', + completed = 'Completed', +} diff --git a/src/utils/completedTodos.ts b/src/utils/completedTodos.ts new file mode 100644 index 000000000..b71b738f0 --- /dev/null +++ b/src/utils/completedTodos.ts @@ -0,0 +1,5 @@ +import { Todo } from '../types/Todo'; + +export const completedTodos = (todos: Todo[]) => todos.filter(t => t.completed); + +export const activeTodos = (todos: Todo[]) => todos.filter(t => !t.completed); From c903c80cff67c1fe0fa3c470c4aa411b32a86381 Mon Sep 17 00:00:00 2001 From: Mariana Trinko Date: Thu, 16 Jan 2025 14:43:14 +0100 Subject: [PATCH 3/7] message --- src/App.tsx | 4 +-- src/components/Footer.tsx | 6 ++--- src/components/TodoItem.tsx | 43 +++++++++++++++++++------------- src/components/TodoList.tsx | 2 ++ src/store/FilterdTodoContext.tsx | 9 ++++--- src/store/TodoContext.tsx | 5 ++-- 6 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 793cc7e9a..39f829976 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,11 +4,11 @@ import './styles/todoapp.scss'; import { TodoList } from './components/TodoList'; import { Header } from './components/Header'; import { Footer } from './components/Footer'; -import { FilteredTodoContext } from './store/FilterdTodoContext'; +// import { FilteredTodoContext } from './store/FilterdTodoContext'; import { TodoContext } from './store/TodoContext'; export const App: React.FC = () => { const { todos } = useContext(TodoContext); - const { filteredTodos } = useContext(FilteredTodoContext); + // const { filteredTodos } = useContext(FilteredTodoContext); return (
    diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 0bc33a047..e41efdcfa 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -7,7 +7,7 @@ import { FilteredTodoContext } from '../store/FilterdTodoContext'; export const Footer = () => { const { todos, setTodos } = useContext(TodoContext); - const { status, setStatus, filteredTodos, setfilteredTodos } = + const { status, setStatus, setfilteredTodos } = useContext(FilteredTodoContext); const notCompletedTodos = todos.filter(t => t.completed === false); @@ -19,12 +19,12 @@ export const Footer = () => { }; const handleChooseActive = () => { - setfilteredTodos(todos.filter(t => t.completed === false)); + setfilteredTodos([...todos].filter(t => t.completed === false)); setStatus(Status.active); }; const handleChooseCompleted = () => { - setfilteredTodos(todos.filter(t => t.completed === true)); + setfilteredTodos([...todos].filter(t => t.completed === true)); setStatus(Status.completed); }; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 15494cf83..77f8ddeb4 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -6,26 +6,24 @@ import { TodoContext } from '../store/TodoContext'; import './TodoApp.scss'; import classNames from 'classnames'; import { FilteredTodoContext } from '../store/FilterdTodoContext'; -import { Status } from '../types/Status'; /* eslint-disable import/extensions */ type Props = { todo: Todo }; -function filterTodos(isCompleted) { - switch (isCompleted) { - case isCompleted: - return Status.completed; - case !isCompleted: - return Status.active; - default: - return Status.all; - } -} +// function filterTodos(isCompleted) { +// switch (isCompleted) { +// case isCompleted: +// return Status.completed; +// case !isCompleted: +// return Status.active; +// default: +// return Status.all; +// } +// } export const TodoItem: React.FC = ({ todo }) => { const { todos, setTodos } = useContext(TodoContext); - const { filteredTodos, setfilteredTodos, status, setStatus } = - useContext(FilteredTodoContext); + const { filteredTodos, setfilteredTodos } = useContext(FilteredTodoContext); const index = todos.findIndex(t => t.id === todo.id); const [isEdit, setIsEdit] = useState(false); const [tempTitle, setTempTitle] = useState(todo.title); @@ -35,14 +33,25 @@ export const TodoItem: React.FC = ({ todo }) => { }; function handleToggleCompleted() { - const isCompleted = !todos[index].completed; + setTodos((prev: Todo[]) => { + const newTodos = [...prev]; - todos[index].completed = isCompleted; + newTodos[index].completed = !prev[index].completed; - setTodos(todos); - setfilteredTodos([...todos].filter(t => t.completed === !isCompleted)); + return newTodos; + }); } + // function handleToggleCompleted() { + // todos[index].completed = !todo.completed; + + // const newTodos = [...todos]; + + // newTodos[index].completed = !todos[index].completed; + + // setTodos([...newTodos]); + // } + function handleDoubleClick() { setIsEdit(true); } diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx index a860cb615..e2189272d 100644 --- a/src/components/TodoList.tsx +++ b/src/components/TodoList.tsx @@ -5,9 +5,11 @@ import './TodoApp.scss'; import { TodoItem } from './TodoItem'; import { FilteredTodoContext } from '../store/FilterdTodoContext'; +// import { TodoContext } from '../store/TodoContext'; // type Props = { todos: Todo[] }; export const TodoList = () => { const { filteredTodos } = useContext(FilteredTodoContext); + // const { todos, setTodos } = useContext(TodoContext); return (
    diff --git a/src/store/FilterdTodoContext.tsx b/src/store/FilterdTodoContext.tsx index 7a2c2dbb5..ac54663a8 100644 --- a/src/store/FilterdTodoContext.tsx +++ b/src/store/FilterdTodoContext.tsx @@ -9,6 +9,7 @@ type T = { status: Status; setStatus: (status: Status) => void; todos: Todo[]; + setTodos: (todos: Todo[]) => void; filteredTodos: Todo[]; setfilteredTodos: (todos: Todo[]) => void; }; @@ -17,6 +18,7 @@ export const FilteredTodoContext = React.createContext({ status: Status.all, setStatus: (_status: Status) => {}, todos: [], + setTodos: (_todos: Todo[]) => {}, filteredTodos: [], setfilteredTodos: (_todos: Todo[]) => {}, }); @@ -26,9 +28,9 @@ type Props = { }; export const FilteredTodoProvider: React.FC = ({ children }) => { - const { todos } = useContext(TodoContext); + const { todos, setTodos } = useContext(TodoContext); - const [filteredTodos, setfilteredTodos] = useState(todos); + const [filteredTodos, setfilteredTodos] = useState([...todos]); const [status, setStatus] = useState(Status.all); const value = useMemo( @@ -36,10 +38,11 @@ export const FilteredTodoProvider: React.FC = ({ children }) => { status, setStatus, todos, + setTodos, filteredTodos, setfilteredTodos, }), - [filteredTodos, setfilteredTodos, todos, status, setStatus], + [filteredTodos, setfilteredTodos, todos, status, setStatus, setTodos], ); return ( diff --git a/src/store/TodoContext.tsx b/src/store/TodoContext.tsx index 6e48a5fde..7f30f4e29 100644 --- a/src/store/TodoContext.tsx +++ b/src/store/TodoContext.tsx @@ -6,14 +6,13 @@ import { useLocalStorage } from '../hooks/useLocalStorage'; type T = { todos: Todo[]; - setTodos: (todos: Todo[]) => void; - // addTodo: (todo: Todo) => void; + // setTodos: (_todos: Todo[]) => void | []; + setTodos: (_todos: Todo[]) => void; }; export const TodoContext = React.createContext({ todos: [], setTodos: (_todos: Todo[]) => {}, - // addTodo: (_todo: Todo) => {}, }); type Props = { From 1ee7010e1a9a4576a834cd9f3976586cdcf77d51 Mon Sep 17 00:00:00 2001 From: Mariana Trinko Date: Fri, 17 Jan 2025 19:28:39 +0100 Subject: [PATCH 4/7] solution --- src/App.tsx | 18 ++--------- src/components/Footer.tsx | 9 +----- src/components/Header.tsx | 29 +++++------------ src/components/TodoApp.tsx | 15 ++++++++- src/components/TodoItem.tsx | 34 ++------------------ src/components/TodoList.tsx | 9 +++--- src/hooks/useLocalStorage.tsx | 27 ---------------- src/index.tsx | 5 +-- src/store/FilterdTodoContext.tsx | 53 -------------------------------- src/store/TodoContext.tsx | 30 ++++++++++++++++-- 10 files changed, 61 insertions(+), 168 deletions(-) delete mode 100644 src/store/FilterdTodoContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 39f829976..45ffffd56 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,7 @@ /* eslint-disable object-curly-newline */ /* eslint-disable @typescript-eslint/quotes */ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React, { useContext } from 'react'; +import { TodoApp } from './components/TodoApp'; import './styles/todoapp.scss'; -import { TodoList } from './components/TodoList'; -import { Header } from './components/Header'; -import { Footer } from './components/Footer'; -// import { FilteredTodoContext } from './store/FilterdTodoContext'; -import { TodoContext } from './store/TodoContext'; -export const App: React.FC = () => { - const { todos } = useContext(TodoContext); - // const { filteredTodos } = useContext(FilteredTodoContext); - return ( -
    -
    {todos.length && } - {todos.length &&
    } -
    - ); +export const App: React.FC = () => { + return ; }; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index e41efdcfa..4335a0825 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -3,28 +3,22 @@ import { TodoContext } from '../store/TodoContext'; import './TodoApp.scss'; import classNames from 'classnames'; import { Status } from '../types/Status'; -import { FilteredTodoContext } from '../store/FilterdTodoContext'; export const Footer = () => { - const { todos, setTodos } = useContext(TodoContext); - const { status, setStatus, setfilteredTodos } = - useContext(FilteredTodoContext); + const { todos, setTodos, status, setStatus } = useContext(TodoContext); const notCompletedTodos = todos.filter(t => t.completed === false); const completedTodos = todos.filter(t => t.completed === true); const handleChooseAll = () => { - setfilteredTodos(todos); setStatus(Status.all); }; const handleChooseActive = () => { - setfilteredTodos([...todos].filter(t => t.completed === false)); setStatus(Status.active); }; const handleChooseCompleted = () => { - setfilteredTodos([...todos].filter(t => t.completed === true)); setStatus(Status.completed); }; @@ -32,7 +26,6 @@ export const Footer = () => { const tempTodos = todos.filter(todo => todo.completed === false); setTodos(tempTodos); - setfilteredTodos(tempTodos); } return ( diff --git a/src/components/Header.tsx b/src/components/Header.tsx index eb002df4c..a64a2558c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -11,20 +11,19 @@ import './TodoApp.scss'; import { TodoContext } from '../store/TodoContext'; import { Todo } from '../types/Todo'; -import { FilteredTodoContext } from '../store/FilterdTodoContext'; +import classNames from 'classnames'; export const Header = () => { const { todos, setTodos } = useContext(TodoContext); const [title, setTitle] = useState(''); - const [isToggle, setIsToggle] = useState(false); - const { setfilteredTodos } = useContext(FilteredTodoContext); + const titleField = useRef(null); useEffect(() => { titleField.current?.focus(); }, [todos]); - const completedTodos = + const isAllTodosCompleted = todos.filter(t => t.completed === true).length === todos.length ? true : false; @@ -48,35 +47,21 @@ export const Header = () => { }; setTodos([...todos, newTodo]); - setfilteredTodos([...todos, newTodo]); setTitle(''); } function handleToggleAll() { - if (!isToggle && completedTodos) { - todos.forEach(t => { - t.completed = true; - }); - setIsToggle(true); - } - - if (isToggle && completedTodos) { + if (isAllTodosCompleted) { todos.forEach(t => { t.completed = false; }); - setIsToggle(false); - } - - if (!completedTodos) { + } else { todos.forEach(t => { t.completed = true; }); - setIsToggle(true); } - setfilteredTodos(todos); - setTodos([...todos]); } @@ -85,8 +70,10 @@ export const Header = () => { {/* this button should have `active` class only if all todos are completed */} {todos.length && (