From 4603b13fd88543d954f2e1a9ac5be15b74cf21e6 Mon Sep 17 00:00:00 2001 From: Yuliia shevchuk Date: Sat, 2 Nov 2024 12:54:16 +0100 Subject: [PATCH 1/3] solution --- src/App.tsx | 154 ++---------------------------------- src/components/Content.tsx | 45 +++++++++++ src/components/Footer.tsx | 112 ++++++++++++++++++++++++++ src/components/Header.tsx | 107 +++++++++++++++++++++++++ src/components/TodoItem.tsx | 138 ++++++++++++++++++++++++++++++++ src/components/TodoList.tsx | 19 +++++ src/context/TodosContex.tsx | 32 ++++++++ src/index.tsx | 7 +- src/types/Filter.ts | 5 ++ src/types/FuncType.ts | 4 + src/types/Todo.ts | 5 ++ 11 files changed, 477 insertions(+), 151 deletions(-) create mode 100644 src/components/Content.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/TodoItem.tsx create mode 100644 src/components/TodoList.tsx create mode 100644 src/context/TodosContex.tsx create mode 100644 src/types/Filter.ts create mode 100644 src/types/FuncType.ts create mode 100644 src/types/Todo.ts diff --git a/src/App.tsx b/src/App.tsx index a399287bd..e95b7b7ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,157 +1,15 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ import React from 'react'; +import { Content } from './components/Content'; +import { TodosProvider } from './context/TodosContex'; export const App: React.FC = () => { return (
-

todos

+ +

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/Content.tsx b/src/components/Content.tsx new file mode 100644 index 000000000..8992dc09b --- /dev/null +++ b/src/components/Content.tsx @@ -0,0 +1,45 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Header } from './Header'; +import { TodoList } from './TodoList'; +import { Footer } from './Footer'; +import { TodosContext } from '../context/TodosContex'; + +export const Content: React.FC = () => { + const { todos, setTodos } = useContext(TodosContext); + + const [focused, setFocused] = useState(true); + const [visibleTodos, setVisibleTodos] = useState(todos); + + useEffect(() => { + localStorage.setItem('todos', JSON.stringify(todos)); + }, [todos]); + + const savedTodos = localStorage.getItem('todos'); + + useEffect(() => { + if (savedTodos) { + setTodos(JSON.parse(savedTodos)); + } + }, []); + + useEffect(() => { + setVisibleTodos(todos); + }, [todos]); + + return ( +
+
+ + + + {/* Hide the footer if there are no todos */} + {todos.length > 0 && ( +
+ )} +
+ ); +}; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 000000000..84d3fa976 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,112 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { Filter } from '../types/Filter'; +import classNames from 'classnames'; +import { TodosContext } from '../context/TodosContex'; +import { Todo } from '../types/Todo'; + +type Props = { + visibleTodos: Todo[]; + setVisibleTodos: (vilteredTodos: Todo[]) => void; + setFocused: (value: boolean) => void; +}; + +export const Footer: React.FC = ({ + visibleTodos, + setVisibleTodos, + setFocused, +}) => { + const { todos, setTodos } = useContext(TodosContext); + + const [selectedFilter, setSelectedFilter] = useState(Filter.All); + + const findFilterKey = (value: string): string | undefined => { + return Object.keys(Filter).find( + key => Filter[key as keyof typeof Filter] === value, + ); + }; + + const todosCount = useCallback( + (type: Filter.Active | Filter.Completed) => { + const value = type === Filter.Active ? false : true; + + return todos.filter(todo => todo.completed === value).length; + }, + [todos], + ); + + const filterFunction = (filter: Filter) => { + switch (filter) { + case Filter.All: + setVisibleTodos(todos); + break; + + case Filter.Active: + setVisibleTodos(todos.filter(todo => !todo.completed)); + break; + + case Filter.Completed: + setVisibleTodos(todos.filter(todo => todo.completed)); + break; + + default: + setTodos(todos); + } + }; + + const clearFunction = () => { + const completedTodos = todos + .filter(todo => todo.completed) + .map(todo => todo.id); + + setTodos(todos.filter(todo => !completedTodos.includes(todo.id))); + setFocused(true); + }; + + const setFilter = (filter: Filter) => { + setSelectedFilter(filter); + + filterFunction(filter); + }; + + useEffect(() => { + if (selectedFilter) { + filterFunction(selectedFilter); + } + }, [visibleTodos]); + + return ( + + ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 000000000..0656fa7c0 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,107 @@ +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { TodosContext } from '../context/TodosContex'; +import classNames from 'classnames'; +import { Todo } from '../types/Todo'; + +type Props = { + focused: boolean; + setFocused: (value: boolean) => void; +}; + +export const Header: React.FC = ({ focused, setFocused }) => { + const { todos, setTodos } = useContext(TodosContext); + + const [title, setTitle] = useState(''); + + const inputRef = useRef(null); + + useEffect(() => { + if (inputRef.current && focused) { + inputRef.current.focus(); + } + }, [focused]); + + const checkActiveTodos = useCallback((): boolean => { + if (todos.length > 0) { + return todos.every(todo => todo.completed); + } + + return false; + }, [todos]); + + const onInputChange = (event: React.ChangeEvent) => { + event.preventDefault(); + setTitle(event.target.value); + }; + + const addTodo = (todoTitle: string) => { + const newTodo: Todo = { + id: +new Date(), + title: todoTitle, + completed: false, + }; + + setTodos([...todos, newTodo]); + }; + + const onSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + const normalizedTitle = title.trim(); + + if (normalizedTitle.length > 0) { + addTodo(normalizedTitle); + setTitle(''); + } else { + return; + } + }; + + const toggleAll = () => { + const allCompleted = checkActiveTodos(); + + const toggledTodos = todos.map(todo => ({ + ...todo, + completed: allCompleted ? false : true, + })); + + setTodos(toggledTodos); + setFocused(true); + }; + + return ( +
+ {todos.length > 0 && ( +
+ ); +}; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 000000000..32bfb0587 --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,138 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { Todo } from '../types/Todo'; +import classNames from 'classnames'; +import { TodosContext } from '../context/TodosContex'; + +type Props = { + todo: Todo; + setFocused: (value: boolean) => void; +}; + +export const TodoItem: React.FC = ({ todo, setFocused }) => { + const { todos, setTodos } = useContext(TodosContext); + + const [onEdit, setOnEdit] = useState(false); + const [title, setTitle] = useState(todo.title); + + const todoInputRef = useRef(null); + const normalizedTitle = title.trim(); + + useEffect(() => { + if (onEdit && todoInputRef.current) { + todoInputRef.current.focus(); + } + }, [onEdit]); + + const handleKeyUp = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setOnEdit(false); + setTitle(todo.title); + } + }; + + const onInputChange = (event: React.ChangeEvent) => { + event.preventDefault(); + + setTitle(event.target.value); + }; + + const updateTodo = (updatedTodo: Todo) => { + const newTodos = [...todos]; + + const index = todos.findIndex( + currentTodo => currentTodo.id === updatedTodo.id, + ); + + newTodos.splice(index, 1, updatedTodo); + + setFocused(true); + setTodos(newTodos); + }; + + const deleteTodo = () => { + setTodos(todos.filter(currentTodo => currentTodo.id !== todo.id)); + }; + + const onDelete = () => { + deleteTodo(); + setFocused(true); + }; + + const statusChange = () => { + updateTodo({ ...todo, completed: !todo.completed }); + }; + + const onSubmit = (event?: React.FormEvent) => { + event?.preventDefault(); + + if (normalizedTitle.length === 0) { + deleteTodo(); + + return; + } + + if (normalizedTitle === todo.title) { + setTitle(normalizedTitle); + setOnEdit(false); + + return; + } + + updateTodo({ ...todo, title: title }); + setOnEdit(false); + }; + + return ( + <> +
setOnEdit(true)} + className={classNames('todo', { completed: todo.completed })} + > + + + {!onEdit ? ( + <> + + {todo.title} + + + + + ) : ( +
onSubmit(event)}> + { + onInputChange(event); + }} + onBlur={() => onSubmit()} + onKeyUp={event => handleKeyUp(event)} + type="text" + className="todo__title-field" + placeholder="Empty todo will be deleted" + /> +
+ )} +
+ + ); +}; diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx new file mode 100644 index 000000000..6a553c9d3 --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,19 @@ +import { TodoItem } from './TodoItem'; +import { Todo } from '../types/Todo'; + +type Props = { + visibleTodos: Todo[]; + setFocused: (value: boolean) => void; +}; + +export const TodoList: React.FC = ({ visibleTodos, setFocused }) => { + // const { todos } = useContext(TodosContext); + + return ( +
+ {visibleTodos.map(todo => ( + + ))} +
+ ); +}; diff --git a/src/context/TodosContex.tsx b/src/context/TodosContex.tsx new file mode 100644 index 000000000..71047fdd4 --- /dev/null +++ b/src/context/TodosContex.tsx @@ -0,0 +1,32 @@ +import React, { createContext, useMemo, useState } from 'react'; +import { Todo } from '../types/Todo'; + +type ContextProps = { + todos: Todo[]; + setTodos: (todos: Todo[]) => void; +}; + +export const TodosContext = createContext({ + todos: [], + setTodos: () => {}, +}); + +type Props = { + children: React.ReactNode; +}; + +export const TodosProvider: React.FC = ({ children }) => { + const [todos, setTodos] = useState([]); + + const value = useMemo( + () => ({ + todos, + setTodos, + }), + [todos], + ); + + return ( + {children} + ); +}; diff --git a/src/index.tsx b/src/index.tsx index a9689cb38..8e38aa180 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,9 @@ import { createRoot } from 'react-dom/client'; -import './styles/index.css'; -import './styles/todo-list.css'; -import './styles/filters.css'; +import './styles/index.scss'; +import './styles/todoapp.scss'; +import './styles/filter.scss'; +import './styles/todo.scss'; import { App } from './App'; diff --git a/src/types/Filter.ts b/src/types/Filter.ts new file mode 100644 index 000000000..174408fd6 --- /dev/null +++ b/src/types/Filter.ts @@ -0,0 +1,5 @@ +export enum Filter { + All = 'all', + Active = 'active', + Completed = 'completed', +} diff --git a/src/types/FuncType.ts b/src/types/FuncType.ts new file mode 100644 index 000000000..74f3a0522 --- /dev/null +++ b/src/types/FuncType.ts @@ -0,0 +1,4 @@ +export enum FuncType { + update = 'update', + delete = 'delete', +} 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 c1b90361074d7f57775710f19ed0974e69c76ef4 Mon Sep 17 00:00:00 2001 From: Yuliia shevchuk Date: Sat, 2 Nov 2024 12:56:38 +0100 Subject: [PATCH 2/3] delete comments --- src/components/Content.tsx | 1 - src/components/Footer.tsx | 2 -- src/components/TodoList.tsx | 2 -- 3 files changed, 5 deletions(-) diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 8992dc09b..e3b42455d 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -32,7 +32,6 @@ export const Content: React.FC = () => { - {/* Hide the footer if there are no todos */} {todos.length > 0 && (