Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CodingChallenge.UI/TodoChallenge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
"version": "0.0.1",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.16",
"@mui/material": "^5.14.16",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"dayjs": "^1.11.10",
"node-sass": "^5.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-minimal-pie-chart": "^8.4.0",
"react-redux": "^7.2.4",
"react-scripts": "4.0.3",
"redux": "^4.1.0",
Expand Down
42 changes: 18 additions & 24 deletions CodingChallenge.UI/TodoChallenge/src/App.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
import React, {Component} from 'react';
import React from 'react';
import TodoList from "./components/todo/TodoList";
import TodoAdd from "./components/todo/TodoAdd";
import TodoDonutChart from './components/todo/TodoDonutChart';
import "./App.scss";
import './button.scss';

class App extends Component {
constructor(props) {
super(props);
import Container from '@mui/material/Container';
import Box from '@mui/material/Box';

this.state = {
newTodo: ''
};
}
const App = () => {

textInputChange = (e) => {
this.setState({...this.state, newTodo: e.target.value});
}

addNewTodo = () => {
console.warn('not implemented');
}

render() {
return (
<div className="App">
<input type="text" value={this.state.newTodo} onChange={this.textInputChange}></input>
<button className={"btn--default"} onClick={this.addNewTodo}>Add</button>
return (
<Container maxWidth="sm">
<Box className="App" sx={{ display: 'flex', flexDirection: 'column' }}>
<h1 style={{ margin: 'auto', textAlign: 'center' }}>
Todo App
</h1>
<TodoDonutChart />
<TodoAdd />
<TodoList />
</div>
)}
</Box>
</Container>
)
}

export default App;
export default App;
5 changes: 5 additions & 0 deletions CodingChallenge.UI/TodoChallenge/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
width: 400px;
}

input[type=date] {
height: 38px;
width: 400px;
}

input + .btn--default {
margin-left: 10px;
}
Expand Down
3 changes: 2 additions & 1 deletion CodingChallenge.UI/TodoChallenge/src/TodoModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export const TodoModel = {
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
isComplete: PropTypes.bool.isRequired,
type: PropTypes.oneOf(['MustDo', 'Optional'])
type: PropTypes.oneOf(['MustDo', 'Optional']),
dueDate: PropTypes.string.isRequired,
};

export const TodoListModel = {
Expand Down
38 changes: 19 additions & 19 deletions CodingChallenge.UI/TodoChallenge/src/TodoService.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
const KEY = "todo-data"
const svc = {
const TODOS_KEY = "todo-data"
const svc = {

getTodos: () => {
const data = localStorage.getItem(KEY);
const data = localStorage.getItem(TODOS_KEY);
if (!data) { return Promise.resolve([]); }
const parsed = JSON.parse(data);
return Promise.resolve(parsed);
},

updateTodo: async (todo) => {
const todos = (await svc.getTodos()).map((item) => (item.id === todo.id ? todo : item));
localStorage.setItem(KEY, JSON.stringify(todos));
localStorage.setItem(TODOS_KEY, JSON.stringify(todos));
return Promise.resolve(todo);
},

addTodo: async text => {
addTodo: async (text, dueDate) => {
const todos = (await svc.getTodos());
const nextId = Number(new Date());
const todo = { id: nextId, text, isComplete: false };
localStorage.setItem(KEY, JSON.stringify([...todos, todo]));
const todo = { id: nextId, text, isComplete: false, dueDate: dueDate };
localStorage.setItem(TODOS_KEY, JSON.stringify([...todos, todo]));
return Promise.resolve(todo);
},

initialize: () => {
if (!!localStorage.getItem(KEY)) return;
if (!!localStorage.getItem(TODOS_KEY)) return;
const firstId = Number(new Date());
let counter = 0;
const todos = [
{id: firstId, text: 'Run the todo app', isComplete: true},
{id: ++counter+firstId, text: 'Implement addNewTodo in App.js', isComplete: false},
{id: ++counter+firstId, text: 'Fix the bug where text changes to a todo item are lost on browser refresh', isComplete: false},
{id: ++counter+firstId, text: 'Match the provided design', isComplete: false},
{id: ++counter+firstId, text: 'Add a chart for complete and incomplete todo items', isComplete: false},
{id: ++counter+firstId, text: 'Replace the Edit/Save/Complete buttons with icons', isComplete: false},
{id: ++counter+firstId, text: 'Add a due date to each todo', isComplete: false},
{id: ++counter+firstId, text: 'Sort the todo list by due date', isComplete: false},
{id: ++counter+firstId, text: 'Make all of the tests pass', isComplete: false},
{id: ++counter+firstId, text: 'Refactor anything in this app to your liking', isComplete: false},
{ id: firstId, text: 'Run the todo app', isComplete: true, dueDate: "2023-11-15" },
{ id: ++counter + firstId, text: 'Implement addNewTodo in App.js', isComplete: false, dueDate: "2023-11-15" },
{ id: ++counter + firstId, text: 'Fix the bug where text changes to a todo item are lost on browser refresh', isComplete: false, dueDate: "2023-11-10" },
{ id: ++counter + firstId, text: 'Match the provided design', isComplete: false, dueDate: "2023-11-17" },
{ id: ++counter + firstId, text: 'Add a chart for complete and incomplete todo items', isComplete: false, dueDate: "2023-11-15" },
{ id: ++counter + firstId, text: 'Replace the Edit/Save/Complete buttons with icons', isComplete: false, dueDate: "2023-11-13" },
{ id: ++counter + firstId, text: 'Add a due date to each todo', isComplete: false, dueDate: "2023-11-15" },
{ id: ++counter + firstId, text: 'Sort the todo list by due date', isComplete: false, dueDate: "2023-11-15" },
{ id: ++counter + firstId, text: 'Make all of the tests pass', isComplete: false, dueDate: "2023-11-15" },
{ id: ++counter + firstId, text: 'Refactor anything in this app to your liking', isComplete: false, dueDate: "2023-11-15" },
];
localStorage.setItem(KEY, JSON.stringify(todos))
localStorage.setItem(TODOS_KEY, JSON.stringify(todos))
}
}

Expand Down
67 changes: 37 additions & 30 deletions CodingChallenge.UI/TodoChallenge/src/components/todo/Todo.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,65 @@
import React, {useState} from 'react';
import {TodoModel} from "../../TodoModel";
import React, { useState } from 'react';
import { TodoModel } from "../../TodoModel";
import PropTypes from "prop-types";
import './todo.scss';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
import ModeEditIcon from '@mui/icons-material/ModeEdit';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';

const Todo = (props) => {
const [editing, setStateEditing] = useState(false);
const [editingText, setStateEditText] = useState(props.todo.text);

const [isEditing, setIsEditing] = useState(false);
const [editingText, setEditingText] = useState(props.todo.text);

const toggleComplete = () => {
props.onCompleteChange({...props.todo, isComplete: !props.todo.isComplete});
props.onCompleteChange({ ...props.todo, isComplete: !props.todo.isComplete });
}

const toggleEditText = () => {
setStateEditing(!editing);
setIsEditing(!isEditing);
}

const saveText = () => {
if (!editing) return;
props.onTextChange(editingText, props.todo.id);
if (!isEditing) return;
props.onTextChange({ ...props.todo, text: editingText });
toggleEditText();
};

const onChangeEditText = (event) => {
setStateEditText(event.target.value);
}

const displayText = () => {
if (editing)
{
return <input onChange={onChangeEditText} value={editingText}></input>
}
else
{
return props.todo.text;
}
}
const getClassName = () => {
const {isComplete} = props.todo;
const { isComplete } = props.todo;
return `todo-item ${isComplete ? 'complete' : 'incomplete'}`;
}

return (
<div className={getClassName()}>
{displayText()}
<button onClick={toggleComplete} className={"btn--default btn--destructive"}>Toggle Complete</button>
{editing
? <button onClick={saveText} className={"btn--default btn--base"}>Save</button>
: <button onClick={toggleEditText} className={"btn--default btn--base"}>Edit</button>
}
{isEditing ?
<TodoEditMode setEditingText={setEditingText} editingText={editingText} saveText={saveText} toggleEditText={toggleEditText} />
: <TodoReadOnlyMode text={props.todo.text} toggleComplete={toggleComplete} toggleEditText={toggleEditText} />}
</div>
)
}

const TodoEditMode = ({ setEditingText, editingText, saveText, toggleEditText }) => {
return (
<>
<input type="text" onChange={(e) => setEditingText(e.target.value)} value={editingText}></input>
<IconButton onClick={saveText} className={"btn--default btn--base"}><CheckCircleOutlineIcon /></IconButton>
<IconButton onClick={toggleEditText} className={"btn--default btn--destructive"}><CancelOutlinedIcon /></IconButton>
</>
)
}

const TodoReadOnlyMode = ({ text, toggleComplete, toggleEditText }) => {
return (
<>
{text}
<IconButton onClick={toggleComplete} className={"btn--default btn--destructive"}><DeleteIcon /></IconButton>
<IconButton onClick={toggleEditText} className={"btn--default btn--base"}><ModeEditIcon /></IconButton>
</>
)
}

Todo.propTypes = {
todo: PropTypes.shape(TodoModel),
onTextChange: PropTypes.func,
Expand Down
27 changes: 27 additions & 0 deletions CodingChallenge.UI/TodoChallenge/src/components/todo/TodoAdd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useState } from 'react';
import { useDispatch } from "react-redux";
import { addTodo } from '../../todoActions';
import Box from '@mui/material/Box';

const TodoAdd = () => {
const dispatch = useDispatch();

const [newTodoText, setNewTodoText] = useState('');
const [newTodoDate, setNewTodoDate] = useState('');

return (
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', pb: 2 }}>
<Box sx={{ flexDirection: 'row', pb: 1 }}>
<input type="text" value={newTodoText} onChange={(e) => setNewTodoText(e.target.value)}></input>
<button className={"btn--default"} onClick={() => {
dispatch(addTodo(newTodoText, newTodoDate));
}}>
Add
</button>
</Box>
<input type="date" value={newTodoDate} onChange={(e) => setNewTodoDate(e.target.value)}></input>
</Box >
)
}

export default TodoAdd;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { PieChart } from 'react-minimal-pie-chart';
import { connect } from "react-redux";
import { TodoListModel } from "../../TodoModel";

const TodoDonutChart = (props) => {
const chartData = [
{ title: 'Done', value: props.todos.filter(todo => todo.isComplete).length, color: '#2769B6' },
{ title: 'Not Done', value: props.todos.filter(todo => !todo.isComplete).length, color: '#37D184' },
]

return (
<PieChart
style={{ height: '200px' }}
data={chartData}
label={({ dataEntry }) => dataEntry.title}
/>
);
}

TodoDonutChart.propTypes = TodoListModel;

const mapStateToProps = (state) => ({
todos: state.todos ?? []
});
const mapDispatchToProps = (dispatch) => ({});

export default connect(mapStateToProps, mapDispatchToProps)(TodoDonutChart);
32 changes: 20 additions & 12 deletions CodingChallenge.UI/TodoChallenge/src/components/todo/TodoList.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, {useState, useEffect} from 'react';
import React, { useState, useEffect } from 'react';
import Todo from "./Todo";
import {TodoListModel} from "../../TodoModel";
import {connect} from "react-redux";
import {completeTodo, getTodos, TODO_TEXT_CHANGE} from "../../todoActions";
import { TodoListModel } from "../../TodoModel";
import { connect } from "react-redux";
import { completeTodo, getTodos, updateTodoText } from "../../todoActions";
import dayjs from 'dayjs';

const TodoList = ({todos, getTodos, onTodoTextChange, onTodoCompleteChange}) => {
const TodoList = ({ todos, getTodos, onTodoTextChange, onTodoCompleteChange }) => {
const [filtered, setFiltered] = useState(true);

const filterByOnChange = () => {
Expand All @@ -16,20 +17,26 @@ const TodoList = ({todos, getTodos, onTodoTextChange, onTodoCompleteChange}) =>
}, [getTodos]);

const renderTodoList = (todos) => {
return todos.filter(filterTodos).map(mapTodoObjectToComponent);
return todos.filter(filterTodos).sort(sortTodos).map(mapTodoObjectToComponent);
}
const filterTodos = (todo) => filtered ? !todo.isComplete : true;
const mapTodoObjectToComponent = (todo, i) => (<Todo key={i}
todo={todo}
onTextChange={onTodoTextChange}
onCompleteChange={onTodoCompleteChange} />);
const sortTodos = (todoA, todoB) => dayjs(todoA.dueDate).diff(todoB.dueDate, 'd');
const mapTodoObjectToComponent = (todo, i) =>
(
<Todo
key={i}
todo={todo}
onTextChange={onTodoTextChange}
onCompleteChange={onTodoCompleteChange}
/>
);

return (
<div className="todo-list">
<h2>List of todos</h2>
<div>
<span>Filter by complete</span>
<input type="checkbox" defaultChecked={filtered} onChange={filterByOnChange} />
<input type="checkbox" defaultChecked={filtered} onChange={filterByOnChange} />
</div>
{renderTodoList(todos)}
</div>
Expand All @@ -41,8 +48,9 @@ TodoList.propTypes = TodoListModel;
const mapStateToProps = (state) => ({
todos: state.todos ?? []
});

const mapDispatchToProps = (dispatch) => ({
onTodoTextChange: (text, id) => dispatch({type: TODO_TEXT_CHANGE, text, id}),
onTodoTextChange: (todo) => dispatch(updateTodoText(todo)),
onTodoCompleteChange: (todo) => dispatch(completeTodo(todo)),
getTodos: () => dispatch(getTodos())
});
Expand Down
Loading