From 7a546627e308bd9da893a89a5ec6fcd774a0919c Mon Sep 17 00:00:00 2001 From: Szymon Pachucki Date: Wed, 20 Sep 2023 13:22:33 +0200 Subject: [PATCH 1/5] Everything works outside of escape. Would love some advice on that. --- src/App.tsx | 113 +++++++++++++++++++++++++----- src/components/footer.tsx | 55 +++++++++++++++ src/components/inCaseOfError.tsx | 29 ++++++++ src/components/inputOfTodos.tsx | 77 +++++++++++++++++++++ src/components/task.tsx | 114 +++++++++++++++++++++++++++++++ src/components/todoCount.tsx | 20 ++++++ src/components/todoList.tsx | 36 ++++++++++ src/types/Errors.ts | 5 ++ src/types/Todo.ts | 7 ++ src/types/filterOption.ts | 1 + 10 files changed, 439 insertions(+), 18 deletions(-) create mode 100644 src/components/footer.tsx create mode 100644 src/components/inCaseOfError.tsx create mode 100644 src/components/inputOfTodos.tsx create mode 100644 src/components/task.tsx create mode 100644 src/components/todoCount.tsx create mode 100644 src/components/todoList.tsx create mode 100644 src/types/Errors.ts create mode 100644 src/types/Todo.ts create mode 100644 src/types/filterOption.ts diff --git a/src/App.tsx b/src/App.tsx index 5749bdf78..36aafeb2d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,101 @@ -/* eslint-disable max-len */ -/* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; -import { UserWarning } from './UserWarning'; - -const USER_ID = 0; +import React, { useEffect, useState } from 'react'; +import { Todo } from './types/Todo'; +import { Errors } from './types/Errors'; +import { FilterOption } from './types/filterOption'; +import { TodoList } from './components/todoList'; +import { InCaseOfError } from './components/inCaseOfError'; +import { InputOfTodos } from './components/inputOfTodos'; +import { Footer } from './components/footer'; export const App: React.FC = () => { - if (!USER_ID) { - return ; - } + const [todos, setTodos] = useState([ + // Initialize with some example todos + + { + id: 1, + title: 'Example Todo 1', + completed: false, + removed: false, + editing: false, + }, + + { + id: 2, + title: 'Example Todo 2', + completed: true, + removed: false, + editing: false, + }, + ]); + const [error, setError] = useState(null); + const [filterTodos, setFilterTodos] = useState('All'); + const [newTodo, setNewTodo] = useState(''); // State for new todo input + + const handleSetFilter = (newFilter: FilterOption) => { + setFilterTodos(newFilter); + }; + + const closeError = () => { + setError(null); + }; + + const addTodo = () => { + if (newTodo.trim() !== '') { + const newTodoItem: Todo = { + id: todos.length + 1, + title: newTodo, + completed: false, + removed: false, + editing: false, + }; + + setTodos([...todos, newTodoItem]); + setNewTodo(''); // Clear the input field + } + }; + + useEffect(() => { + // You can perform any additional actions here when the todos state changes. + }, [todos]); return ( -
-

- Copy all you need from the prev task: -
- React Todo App - Add and Delete -

- -

Styles are already copied

-
+
+

todos

+ +
+ {/* Input for adding new todos */} + + + {/* Pass the filtered todos to TodoList */} + {todos && ( + + )} + + {/* Hide the footer if there are no todos */} + {todos.length > 0 && ( +
+ )} +
+ + {/* Notification is shown in case of any error */} + {/* Add the 'hidden' class to hide the message smoothly */} + {error !== null && ( + + )} +
); }; diff --git a/src/components/footer.tsx b/src/components/footer.tsx new file mode 100644 index 000000000..48a57ab2c --- /dev/null +++ b/src/components/footer.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { TodoCount } from './todoCount'; +import { Todo } from '../types/Todo'; +import { FilterOption } from '../types/filterOption'; + +interface Props { + handleSetFilter: (newFilter: FilterOption) => void; + todos: Todo[]; + setTodos: (todos: Todo[]) => void; +} + +// eslint-disable-next-line max-len +export const Footer: React.FC = ({ handleSetFilter, todos, setTodos }) => { + const handleRemoveComplited = () => { + setTodos(todos.filter((todo) => todo.completed !== true)); + }; + + return ( + + ); +}; diff --git a/src/components/inCaseOfError.tsx b/src/components/inCaseOfError.tsx new file mode 100644 index 000000000..725c4e339 --- /dev/null +++ b/src/components/inCaseOfError.tsx @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react'; +import { Errors } from '../types/Errors'; + +interface Props { + error: Errors; + closeError: () => void; +} + +export const InCaseOfError: React.FC = ({ error, closeError }) => { + useEffect(() => { + // Set a timeout to call closeError after 3 seconds + const timeoutId = setTimeout(() => { + closeError(); + }, 3000); + + // Clean up the timeout when the component unmounts + return () => { + clearTimeout(timeoutId); + }; + }, [closeError]); + + return ( +
+ +
+ ); +}; diff --git a/src/components/inputOfTodos.tsx b/src/components/inputOfTodos.tsx new file mode 100644 index 000000000..daf10aa70 --- /dev/null +++ b/src/components/inputOfTodos.tsx @@ -0,0 +1,77 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import React from 'react'; +import { Todo } from '../types/Todo'; + +interface Props { + setNewTodo: (newTodo: string) => void; + newTodo: string; + addTodo: (title: string) => void; // Updated the return type to void + todos: Todo[]; + setTodos: (todos: Todo[]) => void; +} + +// eslint-disable-next-line max-len +export const InputOfTodos: React.FC = ({ + setNewTodo, newTodo, addTodo, todos, setTodos, +}) => { + const handleAddNewTodo = () => { + if (newTodo) { // Removed the unnecessary check + addTodo(newTodo); + setNewTodo(''); + } + }; + + const handleInputChange = (event: React.ChangeEvent) => { + setNewTodo(event.target.value); + }; + + // eslint-disable-next-line max-len + const handleInputKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); // Prevent form submission on Enter key press + handleAddNewTodo(); + } + }; + + const handleTaggleAll = () => { + let updatedTodos = todos; + + if (todos.some((t) => !t.completed)) { + updatedTodos = todos.map((t) => ({ + ...t, + completed: true, + })); + } else { + updatedTodos = todos.map((t) => ({ + ...t, + completed: !t.completed, + })); + } + + setTodos(updatedTodos); + }; + + return ( +
+ {todos.length !== 0 && ( +
+ ); +}; diff --git a/src/components/task.tsx b/src/components/task.tsx new file mode 100644 index 000000000..449bcf284 --- /dev/null +++ b/src/components/task.tsx @@ -0,0 +1,114 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Todo } from '../types/Todo'; + +type Props = { + todo: Todo; + setTodos: React.Dispatch>; + todos: Todo[]; +}; + +export const Task: React.FC = ({ todo, setTodos, todos }) => { + const [inputValue, setInputValue] = useState(todo.title); + const inputRef = useRef(null); + + const handleToggleCompletion = () => { + const updatedTodo = { ...todo, completed: !todo.completed }; + + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + }; + + const handleRemove = () => { + const updatedTodo = { ...todo, removed: true }; + + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + }; + + const handleEditStart = () => { + const updatedTodo = { ...todo, editing: true }; + + setInputValue(todo.title); + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + }; + + const saveChanges = () => { + const updatedTodo = { ...todo, title: inputValue, editing: false }; + + if (updatedTodo.title === '') { + handleRemove(); + } else { + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + } + + setInputValue(''); + }; + + const handleInputChange = (event: React.ChangeEvent) => { + setInputValue(event.target.value); + }; + + const cancelEditing = () => { + const updatedTodo = { ...todo, editing: false }; + + setTodos(todos.map((t) => (t.id === updatedTodo.id ? updatedTodo : t))); + + setInputValue(''); + }; + + // eslint-disable-next-line max-len + const handleInputKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + cancelEditing(); + } + + if (event.key === 'Enter') { + event.preventDefault(); + saveChanges(); + } + }; + + const handleInputBlur = () => { + saveChanges(); + }; + + useEffect(() => { + if (todo.editing && inputRef.current) { + inputRef.current.focus(); + } + }, [todo.editing]); + + return ( +
+ + {!todo.editing ? ( + <> + {todo.title} + + + ) : ( + + )} +
+ ); +}; diff --git a/src/components/todoCount.tsx b/src/components/todoCount.tsx new file mode 100644 index 000000000..a801ff9d0 --- /dev/null +++ b/src/components/todoCount.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Todo } from '../types/Todo'; + +interface Props { + todos: Todo[]; +} + +export const TodoCount: React.FC = ({ todos }) => { + const todosLeftCount = todos.filter((todo) => !todo.completed).length; + + return ( + + {todosLeftCount} + {' '} + {todosLeftCount === 1 ? 'item' : 'items'} + {' '} + left + + ); +}; diff --git a/src/components/todoList.tsx b/src/components/todoList.tsx new file mode 100644 index 000000000..043d7ad14 --- /dev/null +++ b/src/components/todoList.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Todo } from '../types/Todo'; +import { FilterOption } from '../types/filterOption'; +import { Task } from './task'; + +interface Props { + todos: Todo[]; + filterTodos: FilterOption; + setTodos: React.Dispatch>; +} + +// eslint-disable-next-line max-len +export const TodoList: React.FC = ({ todos, filterTodos, setTodos }) => { + const visibleTodos = () => { + let filteredTodos = todos; + + if (filterTodos === 'Active') { + filteredTodos = filteredTodos.filter((todo) => !todo.completed); + } else if (filterTodos === 'Completed') { + filteredTodos = filteredTodos.filter((todo) => todo.completed); + } + + // Filter out todos with removed set to true + filteredTodos = filteredTodos.filter((todo) => !todo.removed); + + return filteredTodos; + }; + + return ( +
+ {visibleTodos().map((todo) => ( + + ))} +
+ ); +}; diff --git a/src/types/Errors.ts b/src/types/Errors.ts new file mode 100644 index 000000000..626405351 --- /dev/null +++ b/src/types/Errors.ts @@ -0,0 +1,5 @@ +export enum Errors { + Add = 'Unable to add a todo', + Delete = 'Unable to delete a todo', + Update = 'Unable to update a todo', +} diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 000000000..0274a3bbf --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,7 @@ +export type Todo = { + id: number; + title: string; + completed: boolean; + removed: boolean; + editing: boolean; +}; diff --git a/src/types/filterOption.ts b/src/types/filterOption.ts new file mode 100644 index 000000000..2205557d2 --- /dev/null +++ b/src/types/filterOption.ts @@ -0,0 +1 @@ +export type FilterOption = 'All' | 'Active' | 'Completed'; From ae96632a45ea40dee823c413064e9ffc8f399aad Mon Sep 17 00:00:00 2001 From: Szymon Pachucki Date: Wed, 20 Sep 2023 13:27:28 +0200 Subject: [PATCH 2/5] fixed counter --- src/components/todoCount.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/todoCount.tsx b/src/components/todoCount.tsx index a801ff9d0..cbc071e19 100644 --- a/src/components/todoCount.tsx +++ b/src/components/todoCount.tsx @@ -6,7 +6,8 @@ interface Props { } export const TodoCount: React.FC = ({ todos }) => { - const todosLeftCount = todos.filter((todo) => !todo.completed).length; + const todosLeftCount = todos.filter((todo) => !todo.completed + && !todo.removed).length; return ( From 060a99fad1993a1a3ddcc7235a760ba6fa6aefe7 Mon Sep 17 00:00:00 2001 From: Szymon Pachucki Date: Wed, 27 Sep 2023 11:06:01 +0200 Subject: [PATCH 3/5] add task solution --- src/App.tsx | 37 +++++--------------------------- src/components/footer.tsx | 7 ++++-- src/components/inCaseOfError.tsx | 2 -- src/components/inputOfTodos.tsx | 14 ++++++------ src/components/task.tsx | 4 ++-- src/components/todoList.tsx | 8 +++---- 6 files changed, 22 insertions(+), 50 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 36aafeb2d..d145398ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { Todo } from './types/Todo'; import { Errors } from './types/Errors'; import { FilterOption } from './types/filterOption'; @@ -8,28 +8,10 @@ import { InputOfTodos } from './components/inputOfTodos'; import { Footer } from './components/footer'; export const App: React.FC = () => { - const [todos, setTodos] = useState([ - // Initialize with some example todos - - { - id: 1, - title: 'Example Todo 1', - completed: false, - removed: false, - editing: false, - }, - - { - id: 2, - title: 'Example Todo 2', - completed: true, - removed: false, - editing: false, - }, - ]); + const [todos, setTodos] = useState([]); const [error, setError] = useState(null); const [filterTodos, setFilterTodos] = useState('All'); - const [newTodo, setNewTodo] = useState(''); // State for new todo input + const [newTodo, setNewTodo] = useState(''); const handleSetFilter = (newFilter: FilterOption) => { setFilterTodos(newFilter); @@ -50,20 +32,15 @@ export const App: React.FC = () => { }; setTodos([...todos, newTodoItem]); - setNewTodo(''); // Clear the input field + setNewTodo(''); } }; - useEffect(() => { - // You can perform any additional actions here when the todos state changes. - }, [todos]); - return (

todos

- {/* Input for adding new todos */} { setTodos={setTodos} /> - {/* Pass the filtered todos to TodoList */} {todos && ( { /> )} - {/* Hide the footer if there are no todos */} {todos.length > 0 && (
{ )}
- {/* Notification is shown in case of any error */} - {/* Add the 'hidden' class to hide the message smoothly */} - {error !== null && ( + {error && ( )}
diff --git a/src/components/footer.tsx b/src/components/footer.tsx index 48a57ab2c..6697782f0 100644 --- a/src/components/footer.tsx +++ b/src/components/footer.tsx @@ -9,8 +9,11 @@ interface Props { setTodos: (todos: Todo[]) => void; } -// eslint-disable-next-line max-len -export const Footer: React.FC = ({ handleSetFilter, todos, setTodos }) => { +export const Footer: React.FC = ({ + handleSetFilter, + todos, + setTodos, +}) => { const handleRemoveComplited = () => { setTodos(todos.filter((todo) => todo.completed !== true)); }; diff --git a/src/components/inCaseOfError.tsx b/src/components/inCaseOfError.tsx index 725c4e339..3cddcb8db 100644 --- a/src/components/inCaseOfError.tsx +++ b/src/components/inCaseOfError.tsx @@ -8,12 +8,10 @@ interface Props { export const InCaseOfError: React.FC = ({ error, closeError }) => { useEffect(() => { - // Set a timeout to call closeError after 3 seconds const timeoutId = setTimeout(() => { closeError(); }, 3000); - // Clean up the timeout when the component unmounts return () => { clearTimeout(timeoutId); }; diff --git a/src/components/inputOfTodos.tsx b/src/components/inputOfTodos.tsx index daf10aa70..76b479585 100644 --- a/src/components/inputOfTodos.tsx +++ b/src/components/inputOfTodos.tsx @@ -5,17 +5,16 @@ import { Todo } from '../types/Todo'; interface Props { setNewTodo: (newTodo: string) => void; newTodo: string; - addTodo: (title: string) => void; // Updated the return type to void + addTodo: (title: string) => void; todos: Todo[]; setTodos: (todos: Todo[]) => void; } -// eslint-disable-next-line max-len export const InputOfTodos: React.FC = ({ setNewTodo, newTodo, addTodo, todos, setTodos, }) => { const handleAddNewTodo = () => { - if (newTodo) { // Removed the unnecessary check + if (newTodo) { addTodo(newTodo); setNewTodo(''); } @@ -25,10 +24,10 @@ export const InputOfTodos: React.FC = ({ setNewTodo(event.target.value); }; - // eslint-disable-next-line max-len - const handleInputKeyPress = (event: React.KeyboardEvent) => { + const handleInputKeyPress + = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { - event.preventDefault(); // Prevent form submission on Enter key press + event.preventDefault(); handleAddNewTodo(); } }; @@ -53,7 +52,8 @@ export const InputOfTodos: React.FC = ({ return (
- {todos.length !== 0 && ( + {/* i can not write it as {todos.lenght && as when its value is 0 it will display '0' on the screen */} + {todos.length > 0 && (