diff --git a/src/App.tsx b/src/App.tsx index f7fe958..fc8dd6b 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'; @@ -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,11 +23,29 @@ export interface AppState { export const App: React.FC = () => { const [todos, setTodos] = React.useState([]); + 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()); + dispatchDoneCounter({type: 'INITIAL'}); })(); }, []); @@ -54,15 +73,46 @@ 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; + } + let totalDoneCount = todos.filter(todo => todo.done).length; + if (todo.done && totalDoneCount === todos.length) { + setCongratsVisibility('flex'); + } + dispatchDoneCounter(todo.done ? {type: 'ADD'} : {type: 'REMOVE'}); + return todo; + }; + return ( - + - + - + ); diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 5694ebf..0d6ab59 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -10,17 +10,31 @@ const TodoCheckbox = styled.input` margin-right: 8px; `; +export type OnTodoChange = (todo: Todo) => Promise; + export interface TodoItemProps { todo: Todo; + todoChange: OnTodoChange; className?: string; } -const _TodoItem: React.FC = ({todo, className}) => ( -
  • - - {todo.text} -
  • -); +const _TodoItem: React.FC = ({todo, className, todoChange}) => { + const handleChange = (event: React.ChangeEvent) => { + todo.done = event.target.checked.valueOf(); + todoChange(todo); + }; + + return ( +
  • + + {todo.text} +
  • + ); +}; export const TodoItem = styled(_TodoItem)` display: flex; diff --git a/src/components/TodoList.test.tsx b/src/components/TodoList.test.tsx new file mode 100644 index 0000000..26e3e81 --- /dev/null +++ b/src/components/TodoList.test.tsx @@ -0,0 +1,22 @@ +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 onChange = jest.fn(async () => todos[0]); + + 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..6f4672f 100644 --- a/src/components/TodoList.tsx +++ b/src/components/TodoList.tsx @@ -1,19 +1,22 @@ 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.map((todo, index) => ( - - ))} + {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..2a51d97 100644 --- a/src/components/TodoStatusBar.tsx +++ b/src/components/TodoStatusBar.tsx @@ -6,17 +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}) => ( +const _TodoStatusBar: React.FC = ({ + className, + total, + totalDone, + congratsBannerVisibility, +}) => (
    Total: {total} - Done: 0 + Done: {totalDone} + + + Congratulations, you're all set! You've done everything on your list. + +
    );