From bec42d03292c295b22da4bd73306f2476606a3a2 Mon Sep 17 00:00:00 2001 From: Alexandre Luz Date: Wed, 11 Dec 2024 11:13:42 +0100 Subject: [PATCH 1/6] feat: order todos by most recent --- src/components/TodoList.test.tsx | 19 +++++++++++++++++++ src/components/TodoList.tsx | 8 +++++--- 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/components/TodoList.test.tsx diff --git a/src/components/TodoList.test.tsx b/src/components/TodoList.test.tsx new file mode 100644 index 0000000..7a9f9b2 --- /dev/null +++ b/src/components/TodoList.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import {render, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import {TodoList} from './TodoList'; +import {Todo} from '../types'; + +test('populates TodoList and assert they are listed by most recent', async () => { + const user = userEvent.setup(); + + const todos: Todo[] = [ + { id: '1', text: 'First todo', done: false, createdTimestamp: 1000 }, + { id: '2', text: 'Second todo', done: false, createdTimestamp: 2000 } + ]; + const {getByText} = render(); + const items = screen.getAllByRole('listitem'); + + expect(items[0]).toHaveTextContent("Second todo"); + expect(items[1]).toHaveTextContent("First todo"); +}); diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx index 4ed9568..b02faef 100644 --- a/src/components/TodoList.tsx +++ b/src/components/TodoList.tsx @@ -11,9 +11,11 @@ export interface TodoListProps { const _TodoList: React.FC = ({todos, className}) => { return (
    - {todos.map((todo, index) => ( - - ))} + {todos + .sort((t1, t2) => t2.createdTimestamp - t1.createdTimestamp) + .map((todo, index) => ( + + ))}
); }; From db766f2acf64642675818a0542d369e46d54f513 Mon Sep 17 00:00:00 2001 From: Alexandre Luz Date: Wed, 11 Dec 2024 13:26:57 +0100 Subject: [PATCH 2/6] feat: make it possible to check todos and persist values --- src/components/TodoItem.tsx | 38 ++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 5694ebf..7bc93c0 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import styled from 'styled-components'; import {Todo} from '../types'; @@ -10,17 +10,41 @@ const TodoCheckbox = styled.input` margin-right: 8px; `; +const updateTodo = async (todo: Todo) => { + const response = await fetch(`http://localhost:3001/todos/${todo.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(todo), + }); + if (!response.ok) { + window.alert(`Unexpected error ${response.status}: ${response.statusText}`); + return todo; + } +}; + export interface TodoItemProps { todo: Todo; className?: string; } -const _TodoItem: React.FC = ({todo, className}) => ( -
  • - - {todo.text} -
  • -); +const _TodoItem: React.FC = ({todo, className}) => { + const [checked, setChecked] = useState(todo.done); + + const handleChange = (event: React.ChangeEvent) => { + todo.done = event.target.checked.valueOf(); + setChecked(todo.done); + updateTodo(todo); + }; + + return ( +
  • + + {todo.text} +
  • + ); +}; export const TodoItem = styled(_TodoItem)` display: flex; From 35e4a8ff96f673eca8b5bc95761d9443981fc2ca Mon Sep 17 00:00:00 2001 From: Alexandre Luz Date: Wed, 11 Dec 2024 14:23:25 +0100 Subject: [PATCH 3/6] feat: update done counter in TodoStatusBar when checking todos --- src/App.tsx | 30 ++++++++++++++++++++++++++---- src/components/TodoItem.tsx | 19 ++++--------------- src/components/TodoList.test.tsx | 5 ++++- src/components/TodoList.tsx | 7 ++++--- src/components/TodoStatusBar.tsx | 9 +++++++-- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f7fe958..6487328 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import {OnSubmit, TodoInput} from './components/TodoInput'; import {TodoList} from './components/TodoList'; import {Todo} from './types'; import {TodoStatusBar} from './components/TodoStatusBar'; +import {OnTodoChange} from './components/TodoItem'; export const AppContainer = styled.div` display: flex; @@ -22,13 +23,16 @@ export interface AppState { export const App: React.FC = () => { const [todos, setTodos] = React.useState([]); + const updateTotalDone = () => todos.filter(todo => todo.done).length; + const [totalDone, setTotalDone] = React.useState(updateTotalDone); React.useEffect(() => { (async () => { const response = await fetch('http://localhost:3001/todos'); setTodos(await response.json()); + setTotalDone(updateTotalDone); })(); - }, []); + }, [updateTotalDone]); const createTodo: OnSubmit = async text => { const newTodo = { @@ -54,15 +58,33 @@ export const App: React.FC = () => { return ''; }; + const updateTodo: OnTodoChange = async (todo: Todo) => { + const response = await fetch(`http://localhost:3001/todos/${todo.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(todo), + }); + if (!response.ok) { + window.alert( + `Unexpected error ${response.status}: ${response.statusText}` + ); + return todo; + } + setTotalDone(updateTotalDone); + return todo; + }; + return ( - + - + - + ); diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 7bc93c0..1f993eb 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -10,32 +10,21 @@ const TodoCheckbox = styled.input` margin-right: 8px; `; -const updateTodo = async (todo: Todo) => { - const response = await fetch(`http://localhost:3001/todos/${todo.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(todo), - }); - if (!response.ok) { - window.alert(`Unexpected error ${response.status}: ${response.statusText}`); - return todo; - } -}; +export type OnTodoChange = (todo: Todo) => Promise; export interface TodoItemProps { todo: Todo; + todoChange: OnTodoChange; className?: string; } -const _TodoItem: React.FC = ({todo, className}) => { +const _TodoItem: React.FC = ({todo, className, todoChange}) => { const [checked, setChecked] = useState(todo.done); const handleChange = (event: React.ChangeEvent) => { todo.done = event.target.checked.valueOf(); setChecked(todo.done); - updateTodo(todo); + todoChange(todo); }; return ( diff --git a/src/components/TodoList.test.tsx b/src/components/TodoList.test.tsx index 7a9f9b2..64d88f3 100644 --- a/src/components/TodoList.test.tsx +++ b/src/components/TodoList.test.tsx @@ -11,7 +11,10 @@ test('populates TodoList and assert they are listed by most recent', async () => { id: '1', text: 'First todo', done: false, createdTimestamp: 1000 }, { id: '2', text: 'Second todo', done: false, createdTimestamp: 2000 } ]; - const {getByText} = render(); + + const onChange = jest.fn(async () => todos[0]); + + const {getByText} = render(); const items = screen.getAllByRole('listitem'); expect(items[0]).toHaveTextContent("Second todo"); diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx index b02faef..6f4672f 100644 --- a/src/components/TodoList.tsx +++ b/src/components/TodoList.tsx @@ -1,20 +1,21 @@ import React from 'react'; import styled from 'styled-components'; import {Todo} from '../types'; -import {TodoItem} from './TodoItem'; +import {TodoItem, OnTodoChange} from './TodoItem'; export interface TodoListProps { todos: Array; className?: string; + todoChange: OnTodoChange; } -const _TodoList: React.FC = ({todos, className}) => { +const _TodoList: React.FC = ({todos, className, todoChange}) => { return (
      {todos .sort((t1, t2) => t2.createdTimestamp - t1.createdTimestamp) .map((todo, index) => ( - + ))}
    ); diff --git a/src/components/TodoStatusBar.tsx b/src/components/TodoStatusBar.tsx index 2f36f7f..7de3b04 100644 --- a/src/components/TodoStatusBar.tsx +++ b/src/components/TodoStatusBar.tsx @@ -9,13 +9,18 @@ const InfoBar = styled.div` export interface TodoStatusBarProps { className?: string; total: number; + totalDone: number; } -const _TodoStatusBar: React.FC = ({className, total}) => ( +const _TodoStatusBar: React.FC = ({ + className, + total, + totalDone, +}) => (
    Total: {total} - Done: 0 + Done: {totalDone}
    ); From 19ecb75b25361169705a02d273d59f37b5330152 Mon Sep 17 00:00:00 2001 From: Alexandre Luz Date: Wed, 11 Dec 2024 15:42:59 +0100 Subject: [PATCH 4/6] feat: show message when all todos are done --- src/App.tsx | 5 +++++ src/components/TodoStatusBar.tsx | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 6487328..176864d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -72,6 +72,11 @@ export const App: React.FC = () => { ); return todo; } + if (todo.done && totalDone + 1 === todos.length) { + window.alert( + `"Congratulations, you're all set! You've done everything on your list."` + ); + } setTotalDone(updateTotalDone); return todo; }; diff --git a/src/components/TodoStatusBar.tsx b/src/components/TodoStatusBar.tsx index 7de3b04..2a51d97 100644 --- a/src/components/TodoStatusBar.tsx +++ b/src/components/TodoStatusBar.tsx @@ -6,22 +6,34 @@ const InfoBar = styled.div` justify-content: space-between; `; +const CongratulationsBanner = styled.div<{isCongratsBannerVisible: string}>` + display: ${props => props.isCongratsBannerVisible || 'none'}; + justify-content: center; +`; + export interface TodoStatusBarProps { className?: string; total: number; totalDone: number; + congratsBannerVisibility: string; } const _TodoStatusBar: React.FC = ({ className, total, totalDone, + congratsBannerVisibility, }) => (
    Total: {total} Done: {totalDone} + + + Congratulations, you're all set! You've done everything on your list. + +
    ); From 7bd8c19ae9ca9604b96e319f65223876b525ac17 Mon Sep 17 00:00:00 2001 From: Alexandre Luz Date: Thu, 12 Dec 2024 11:10:38 +0100 Subject: [PATCH 5/6] fix: todo counter not properly being updated and 'checked' attribute not being added --- src/App.tsx | 48 ++++++++++++++++++++++++-------- src/components/TodoItem.tsx | 11 ++++---- src/components/TodoList.test.tsx | 2 +- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 176864d..b6d4d9f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useReducer} from 'react'; import styled from 'styled-components'; import {TodosFooter} from './components/TodosFooter'; import {TodosHeader} from './components/TodosHeader'; @@ -23,16 +23,32 @@ export interface AppState { export const App: React.FC = () => { const [todos, setTodos] = React.useState([]); - const updateTotalDone = () => todos.filter(todo => todo.done).length; - const [totalDone, setTotalDone] = React.useState(updateTotalDone); + const [congratsVisibility, setCongratsVisibility] = React.useState('none'); + type CounterAction = {type: 'ADD'} | {type: 'REMOVE'} | {type: 'INITIAL'}; + const [totalDone, dispatchDoneCounter] = useReducer( + (state: number, action: CounterAction): number => { + switch (action.type) { + case 'ADD': + return state + 1; + case 'REMOVE': + return state - 1; + case 'INITIAL': + return todos.filter(todo => todo.done).length; + default: + return 0; + } + }, + 0 + ); React.useEffect(() => { (async () => { const response = await fetch('http://localhost:3001/todos'); setTodos(await response.json()); - setTotalDone(updateTotalDone); + // setTotalDone(todos.filter(todo => todo.done).length); + dispatchDoneCounter({type: 'INITIAL'}); })(); - }, [updateTotalDone]); + }, []); const createTodo: OnSubmit = async text => { const newTodo = { @@ -72,24 +88,32 @@ export const App: React.FC = () => { ); return todo; } - if (todo.done && totalDone + 1 === todos.length) { - window.alert( - `"Congratulations, you're all set! You've done everything on your list."` - ); + let totalDoneCount = todos.filter(todo => todo.done).length; + if (todo.done && totalDoneCount === todos.length) { + setCongratsVisibility('flex'); } - setTotalDone(updateTotalDone); + dispatchDoneCounter(todo.done ? {type: 'ADD'} : {type: 'REMOVE'}); return todo; }; return ( - + - + ); diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 1f993eb..0d6ab59 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React from 'react'; import styled from 'styled-components'; import {Todo} from '../types'; @@ -19,17 +19,18 @@ export interface TodoItemProps { } const _TodoItem: React.FC = ({todo, className, todoChange}) => { - const [checked, setChecked] = useState(todo.done); - const handleChange = (event: React.ChangeEvent) => { todo.done = event.target.checked.valueOf(); - setChecked(todo.done); todoChange(todo); }; return (
  • - + {todo.text}
  • ); diff --git a/src/components/TodoList.test.tsx b/src/components/TodoList.test.tsx index 64d88f3..26e3e81 100644 --- a/src/components/TodoList.test.tsx +++ b/src/components/TodoList.test.tsx @@ -14,7 +14,7 @@ test('populates TodoList and assert they are listed by most recent', async () => const onChange = jest.fn(async () => todos[0]); - const {getByText} = render(); + render(); const items = screen.getAllByRole('listitem'); expect(items[0]).toHaveTextContent("Second todo"); From ba6a79aa51e28fb7ba3dd738b8e6e70f3f77b71f Mon Sep 17 00:00:00 2001 From: Alexandre Luz Date: Thu, 12 Dec 2024 20:43:21 +0100 Subject: [PATCH 6/6] fix: remove commented code --- src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index b6d4d9f..fc8dd6b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,7 +45,6 @@ export const App: React.FC = () => { (async () => { const response = await fetch('http://localhost:3001/todos'); setTodos(await response.json()); - // setTotalDone(todos.filter(todo => todo.done).length); dispatchDoneCounter({type: 'INITIAL'}); })(); }, []);