diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000..14a6811b
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": [ "@babel/env", "@babel/react" ],
+ "plugins": [ "@babel/plugin-proposal-class-properties" ]
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..2e2d9d30
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+package-lock.json
+/dist
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..3019b715
--- /dev/null
+++ b/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "system-test",
+ "version": "1.0.0",
+ "description": "Sureify - Front end system test",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "babel-node ./node_modules/webpack-dev-server/bin/webpack-dev-server --open"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/jay2452/system-test.git"
+ },
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/jay2452/system-test/issues"
+ },
+ "homepage": "https://github.com/jay2452/system-test#readme",
+ "devDependencies": {
+ "@babel/core": "7.6.4",
+ "@babel/node": "7.6.3",
+ "@babel/plugin-proposal-class-properties": "^7.5.5",
+ "@babel/preset-env": "7.6.3",
+ "@babel/preset-react": "7.6.3",
+ "babel-loader": "8.0.6",
+ "css-loader": "3.2.0",
+ "file-loader": "^4.2.0",
+ "html-webpack-plugin": "3.2.0",
+ "node-sass": "^4.14.1",
+ "path": "0.12.7",
+ "sass-loader": "8.0.0",
+ "style-loader": "1.0.0",
+ "webpack": "4.41.2",
+ "webpack-cli": "3.3.9",
+ "webpack-dev-server": "^3.11.0"
+ },
+ "dependencies": {
+ "@material-ui/core": "^4.9.3",
+ "@material-ui/icons": "^4.9.1",
+ "material-table": "^1.64.0",
+ "react": "16.11.0",
+ "react-dom": "16.11.0",
+ "react-redux": "7.1.1",
+ "redux": "4.0.4",
+ "redux-thunk": "2.3.0"
+ }
+}
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 00000000..ef2b3cec
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,70 @@
+import React, { useReducer } from'react';
+import AddTaskButton from './Components/AddTaskButton';
+import TodoForm from './Components/TodoForm';
+import TodoTabs from './Containers/TodoTabs';
+import Alert from './Components/Alert';
+import { useSelector, useDispatch } from 'react-redux';
+import InputSearchBox from './Components/InputSearchBox';
+
+import * as todoActionCreators from './store/actionCreators/todoActions';
+import CustomBackdrop from './Components/Backdrop';
+import { Typography } from '@material-ui/core';
+
+const App = props => {
+ // const [isFormOpen, setIsFormOpen] = React.useState(false);
+ const {isFormOpen, data, isDataLoaded} = useSelector(state => state.todos);
+ const dispatch = useDispatch();
+
+ // const [isEditMode, setIsEditMode] = React.useState(true);
+ const toggleOpenForm = () => dispatch(todoActionCreators.openTodoForm(null, true));
+
+ const handleSearch = value => dispatch(todoActionCreators.searchTodoItems(value));
+
+ React.useEffect(() => {
+ if(isDataLoaded === false){
+ dispatch(todoActionCreators.getAllTodos())
+ }
+ }, []);
+
+ if(isDataLoaded === false) return ;
+ return (
+
+ {/* render tabs */}
+
+
+
+
+ My Tasks
+
+
+
+ Better to do, than to say
+
+
+
+
+
+
+
+
+
+ )
+};
+
+export default App;
\ No newline at end of file
diff --git a/src/Components/AddTaskButton/index.js b/src/Components/AddTaskButton/index.js
new file mode 100644
index 00000000..176aea8b
--- /dev/null
+++ b/src/Components/AddTaskButton/index.js
@@ -0,0 +1,28 @@
+import React from "react";
+
+import { makeStyles } from '@material-ui/core/styles';
+import Fab from '@material-ui/core/Fab';
+import AddIcon from '@material-ui/icons/Add';
+
+const useStyles = makeStyles(theme => ({
+ addtaskButton: {
+ position: 'absolute',
+ bottom: 10,
+ right: 10
+ }
+}));
+
+const AddTaskButton = props => {
+
+ const classes = useStyles();
+
+ return (
+
+ );
+};
+
+export default AddTaskButton;
\ No newline at end of file
diff --git a/src/Components/Alert/index.js b/src/Components/Alert/index.js
new file mode 100644
index 00000000..4def4a0f
--- /dev/null
+++ b/src/Components/Alert/index.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import Button from '@material-ui/core/Button';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogContentText from '@material-ui/core/DialogContentText';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import { useSelector } from 'react-redux';
+
+export default function Alert(props) {
+
+ const { isAlertOpen, yesAction, noAction, alertTitle, alertMessage } = useSelector(state => state.ui);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/Components/Backdrop/index.js b/src/Components/Backdrop/index.js
new file mode 100644
index 00000000..f7ef4203
--- /dev/null
+++ b/src/Components/Backdrop/index.js
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import Backdrop from '@material-ui/core/Backdrop';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import { makeStyles } from '@material-ui/core/styles';
+import { useDispatch, useSelector } from 'react-redux';
+import { toggleBackdrop } from '../../store/actionCreators/todoActions';
+
+const useStyles = makeStyles((theme) => ({
+ backdrop: {
+ zIndex: theme.zIndex.drawer + 1,
+ color: '#fff',
+ },
+}));
+
+const CustomBackdrop = props => {
+ const classes = useStyles();
+ // const dispatch = useDispatch();
+ const { isBackdropOpen } = useSelector(state => state.ui);
+ return (
+
+
+
+ )
+};
+
+export default CustomBackdrop;
\ No newline at end of file
diff --git a/src/Components/Dropdown/index.js b/src/Components/Dropdown/index.js
new file mode 100644
index 00000000..459ccceb
--- /dev/null
+++ b/src/Components/Dropdown/index.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import FormControl from '@material-ui/core/FormControl';
+import Select from '@material-ui/core/Select';
+import MenuItem from '@material-ui/core/MenuItem';
+import InputLabel from '@material-ui/core/InputLabel';
+
+const Dropdown = props => {
+
+ const { data, valueColumn, displayColumn, handleChange, isDisabled, selectedValue, displayLabel } = props;
+
+ return (
+
+ {displayLabel}
+
+
+ )
+};
+
+export default Dropdown;
\ No newline at end of file
diff --git a/src/Components/InputSearchBox/index.js b/src/Components/InputSearchBox/index.js
new file mode 100644
index 00000000..d0e79033
--- /dev/null
+++ b/src/Components/InputSearchBox/index.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import clsx from 'clsx';
+import TextField from '@material-ui/core/TextField';
+import InputAdornment from '@material-ui/core/InputAdornment';
+import { makeStyles } from '@material-ui/core/styles';
+import SearchIcon from '@material-ui/icons/Search';
+import debounce from '../../utils/debounce';
+
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ display: 'flex',
+ flexWrap: 'wrap',
+ },
+ margin: {
+ margin: theme.spacing(1),
+ },
+ withoutLabel: {
+ marginTop: theme.spacing(3),
+ },
+ textField: {
+ width: '25ch',
+ },
+}));
+
+(function () {
+ const keysPressed = {};
+ document.addEventListener('keydown', (event) => {
+ keysPressed[event.key] = true;
+ if (keysPressed['Control'] && keysPressed['Shift'] && event.key == 'F') {
+ const node = document.getElementById('global-searchbox');
+ if (node) {
+ node.focus();
+ }
+ }
+ });
+
+ document.addEventListener('keyup', (event) => {
+ delete keysPressed[event.key];
+ });
+})();
+
+const InputSearchBox = props => {
+ const classes = useStyles();
+ const debouncedSearch = debounce(value => props.handleSearch(value), 200)
+ return (
+ ,
+ }}
+ onChange={({ target: { value } }) => debouncedSearch(value)}
+ />
+ )
+};
+export default InputSearchBox;
\ No newline at end of file
diff --git a/src/Components/TodoForm/index.js b/src/Components/TodoForm/index.js
new file mode 100644
index 00000000..cecf8089
--- /dev/null
+++ b/src/Components/TodoForm/index.js
@@ -0,0 +1,208 @@
+import React from "react";
+import { connect, useSelector } from 'react-redux';
+import { makeStyles } from '@material-ui/core/styles';
+import Button from '@material-ui/core/Button';
+import TextField from '@material-ui/core/TextField';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import Dropdown from "../Dropdown";
+import * as todoActionCreators from "../../store/actionCreators/todoActions";
+import { priorities } from '../../config';
+
+
+const useStyles = makeStyles(theme => ({
+ root: {
+
+ },
+ formControl: {
+
+ },
+ dueDate: {
+ marginLeft: theme.spacing(1),
+ marginRight: theme.spacing(1),
+ width: 200,
+ },
+}));
+
+
+const TodoForm = props => {
+
+ const classes = useStyles();
+ const {isFormOpen, isEditMode} = props;
+
+ const selectedTodoItem = props.selectedTodoItem || {};
+
+ const [priority, setPriority] = React.useState(priorities[0].value);
+ const [summary, setSummary] = React.useState('');
+ const [summaryError, setSummaryError ] = React.useState(false);
+ const [description, setDescription] = React.useState('');
+ const [descriptionError, setDescriptionError] = React.useState(false);
+ const [dueDate, setDueDate] = React.useState(new Date().toISOString().substr(0, 19));
+
+ React.useEffect(() => {
+ setPriority(selectedTodoItem.priority || 'low');
+ setSummary(selectedTodoItem.summary || '');
+ setDescription(selectedTodoItem.description || '');
+ if(selectedTodoItem.dueDate && selectedTodoItem.dueDate.toISOString){
+ setDueDate(selectedTodoItem.dueDate.toISOString().substr(0, 19));
+ }else{
+ setDueDate(selectedTodoItem.dueDate || '');
+ }
+
+ // setIsEditMode(false);
+ }, [selectedTodoItem.priority, selectedTodoItem.summary, selectedTodoItem.description, selectedTodoItem.dueDate])
+
+ const handlePriority = (e) => {
+ setPriority(e.target.value);
+ }
+
+ const handleSaveTodo = () => {
+ const values = {
+ summary,
+ description,
+ dueDate,
+ priority,
+ createdOn: new Date()
+ };
+ if(confirm("Are you sure ?")){
+ props.saveTodoItem(values);
+ props.handleClose();
+ }
+ }
+
+ return (
+
+ )
+};
+const mapStateToProps = state => {
+ return {
+ selectedTodoItem: state.todos.selectedTodoItem,
+ isEditMode: state.todos.isEditMode
+ }
+}
+const mapDispatchToProps = dispatch => {
+ return {
+ saveTodoItem: values => dispatch(todoActionCreators.saveTodoItem(values))
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(TodoForm);
\ No newline at end of file
diff --git a/src/Containers/TodoList/Table.js b/src/Containers/TodoList/Table.js
new file mode 100644
index 00000000..bf6b81c5
--- /dev/null
+++ b/src/Containers/TodoList/Table.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import MaterialTable, { MTableBodyRow } from "material-table";
+import EditOutlinedIcon from '@material-ui/icons/EditOutlined';
+
+const Table = props => {
+ const { columns, data, handleDeleteTodo, handleRowClick,
+ handleEditTodo,
+ toggleCompleteTask,
+ onSelectionChange
+ } = props;
+ return (
+
+ {
+ if(row.status === "completed"){
+ return {
+ "textDecoration" : "line-through"
+ }
+ }else{
+ return {
+ "textDecoration" : "none"
+ }
+ }
+ }
+ }}
+ onRowClick={handleRowClick}
+ components={{
+ Groupbar: props => {
+ return null
+ },
+ Row: props => {
+ if(props.data.status === "pending"){
+ props.actions[0].tooltip = "Mark as complete";
+ props.actions[0].icon = "check_circle_outline";
+ }else {
+ props.actions[0].tooltip = "Re Open";
+ props.actions[0].icon = "check_circle";
+ }
+ return
+ }
+ }}
+ actions={[
+ {
+ icon: 'assignment_turned_in',
+ tooltip: 'Mark as complete',
+ onClick: toggleCompleteTask,
+ position: "row"
+ }, {
+ icon: 'edit_outline',
+ tooltip: 'Edit',
+ onClick: handleEditTodo,
+ position: "row"
+ },
+ {
+ icon: 'delete_outline',
+ tooltip: 'Delete',
+ onClick: handleDeleteTodo,
+ position: "row"
+ }
+ ]}
+ onSelectionChange={onSelectionChange}
+ />
+
+ )
+};
+
+export default Table;
\ No newline at end of file
diff --git a/src/Containers/TodoList/index.js b/src/Containers/TodoList/index.js
new file mode 100644
index 00000000..c17c4b85
--- /dev/null
+++ b/src/Containers/TodoList/index.js
@@ -0,0 +1,178 @@
+import React from 'react';
+import Dropdown from '../../Components/Dropdown';
+import { connect } from 'react-redux';
+import Table from './Table';
+import * as todoActionreators from '../../store/actionCreators/todoActions';
+import { groupByFields, gridColumns } from '../../config';
+import { Button, Tooltip } from '@material-ui/core';
+
+const TodoList = props => {
+ const [columns, setColumns] = React.useState(gridColumns);
+ const [currentGrouping, setCurrentGrouping] = React.useState('none');
+ const tabView = props.tabView;
+ const [tableComp, setTableComp] = React.useState(null);
+ const [selectedRows, setSelectedRows] = React.useState([]);
+
+ const handleDeleteTodo = (event, rowData) => {
+ props.toggleAlertBox({
+ title: rowData.summary,
+ alertMessage: "Do you want to delete this task?",
+ yesAction: () => {
+ props.deleteTodo(rowData.id);
+ props.toggleAlertBox();
+ },
+ noAction: () => {
+ props.toggleAlertBox();
+ }
+ });
+ }
+
+ const handleRowClick = (e, rowData) => {
+ props.openTodoForm(rowData.id, false);
+ };
+
+ const handleEditTodo = (e, rowData) => {
+ props.openTodoForm(rowData.id, true);
+ }
+
+ const toggleCompleteTask = (e, rowData) => {
+ props.toggleTaskStatus(rowData.id);
+ }
+
+ const onSelectionChange = (rows) => {
+ setSelectedRows(rows.map(row => row.id));
+ }
+
+ React.useEffect(() => {
+ let viewData = null;
+ if(tabView === "all"){
+ viewData = props.todoData;
+
+ }else if(tabView === "completed"){
+ viewData = props.todoData.filter(record => record.status === "completed");
+
+ }else if(tabView === "pending"){
+ viewData = props.todoData.filter(record => record.status === "pending");
+ }
+ // console.log("data ::", viewData);
+ setTableComp(
+
+ );
+ }, [props.todoData.length, props.lastEditTimestamp, columns])
+
+ const handleGrouping = (e) => {
+ const value = e.target.value;
+ setCurrentGrouping(value);
+ const updatedColumns = columns.map(col => {
+ if (col.field === value) {
+ col.tableData.groupOrder = 0;
+ } else {
+ delete col.tableData.groupOrder;
+ }
+ return col;
+ });
+ setColumns(updatedColumns);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {tableComp}
+
+ );
+};
+
+
+const mapStatetoProps = state => {
+ return {
+ todoData: state.todos.filteredData,
+ lastEditTimestamp: state.todos.lastEditTimestamp,
+ isAlertBoxOpen: state.ui.isAlertOpen
+ }
+}
+const mapDispatchToProps = dispatch => {
+ return {
+ deleteTodo: (todoId) => dispatch(todoActionreators.deleteTodo(todoId)),
+ toggleAlertBox: (info) => dispatch(todoActionreators.toggleAlertBox(info)),
+ openTodoForm: (id, isEditable) => dispatch(todoActionreators.openTodoForm(id, isEditable)),
+ toggleTaskStatus: (id) => dispatch(todoActionreators.toggleTaskStatus(id)),
+ markListAsDone: (ids) => dispatch(todoActionreators.markListAsDone(ids)),
+ markListAsPending: ids => dispatch(todoActionreators.markListAsPending(ids))
+ }
+}
+const options = {
+ areOwnPropsEqual: () => false
+}
+
+export default connect(mapStatetoProps, mapDispatchToProps, undefined, options)(TodoList);
\ No newline at end of file
diff --git a/src/Containers/TodoTabs/TabPanel.js b/src/Containers/TodoTabs/TabPanel.js
new file mode 100644
index 00000000..e0f314dd
--- /dev/null
+++ b/src/Containers/TodoTabs/TabPanel.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Typography from '@material-ui/core/Typography';
+import Box from '@material-ui/core/Box';
+
+function TabPanel(props) {
+ const { children, value, index, ...other } = props;
+
+ return (
+
+ {value === index && (
+
+ {children}
+
+ )}
+
+ );
+ }
+
+ TabPanel.propTypes = {
+ children: PropTypes.node,
+ index: PropTypes.any.isRequired,
+ value: PropTypes.any.isRequired,
+ };
+
+export default TabPanel;
\ No newline at end of file
diff --git a/src/Containers/TodoTabs/index.js b/src/Containers/TodoTabs/index.js
new file mode 100644
index 00000000..d2cf2adc
--- /dev/null
+++ b/src/Containers/TodoTabs/index.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import Paper from '@material-ui/core/Paper';
+import Tabs from '@material-ui/core/Tabs';
+import Tab from '@material-ui/core/Tab';
+
+import TabPanel from './TabPanel';
+import TodoList from '../TodoList';
+
+const useStyles = makeStyles({
+ root: {
+ flexGrow: 1,
+ },
+});
+
+const TodoTabs = (props) => {
+ const classes = useStyles();
+ const [value, setValue] = React.useState(0);
+
+ const handleChange = (event, newValue) => {
+ setValue(newValue);
+ };
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+
+export default (TodoTabs);
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 00000000..af5c834d
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,45 @@
+export const groupByFields = [{
+ value: 'none',
+ label: 'None'
+}, {
+ value: 'createdOn',
+ label: 'Created On'
+}, {
+ value: "dueDate",
+ label: 'Due Date'
+}, {
+ value: "priority",
+ label: 'Priority'
+}];
+
+export const priorities = [{
+ value: 'none',
+ label: 'None'
+},{
+ value: 'low',
+ label: "Low"
+}, {
+ value: "medium",
+ label: "Medium"
+}, {
+ value: "high",
+ label: "High"
+}];
+
+export const searchableFields = ["summary", "description"];
+export const sortableFields = ["summary", "priority", "dueDate", "createdOn"];
+export const gridColumns = [
+ { title: 'Id', field: 'id', hidden: true },
+ { title: 'Summary', field: 'summary', sorting: sortableFields.includes('summary') ? true : false },
+ { title: 'Priority', field: 'priority', sorting: sortableFields.includes('priority') ? true : false },
+ { title: 'Created On',
+ defaultSort: 'desc',
+ format: "%d%m%Y - %hh:%mm",
+ field: 'createdOn', type: 'datetime', sorting: sortableFields.includes('createdOn') ? true : false },
+ {
+ title: 'Due Date',
+ field: 'dueDate',
+ type: 'datetime',
+ sorting: sortableFields.includes('dueDate') ? true : false
+ }
+]
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 00000000..e69de29b
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 00000000..bfc9b657
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Todo Application
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 00000000..b9c9f46d
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import CssBaseline from '@material-ui/core/CssBaseline';
+import { ThemeProvider } from "@material-ui/core";
+
+import { Provider } from 'react-redux'
+import { createStore, applyMiddleware } from 'redux';
+import thunk from 'redux-thunk';
+import App from './App.jsx';
+import theme from './theme';
+import rootReducer from './store/reducers';
+
+const store = createStore(rootReducer, applyMiddleware(thunk));
+
+ReactDOM.render(
+
+
+
+
+
+
+ , document.getElementById('root')
+);
\ No newline at end of file
diff --git a/src/store/actionCreators/todoActions.js b/src/store/actionCreators/todoActions.js
new file mode 100644
index 00000000..cdfdaa29
--- /dev/null
+++ b/src/store/actionCreators/todoActions.js
@@ -0,0 +1,108 @@
+import todoListData from '../staticData/todoListData';
+
+export const getAllTodos = () => {
+ return dispatch => {
+ dispatch(toggleBackdrop());
+ setTimeout(() => {
+ dispatch({
+ type: "GET_TODO_LIST",
+ values: {
+ data: todoListData
+ }
+ });
+ dispatch(toggleBackdrop());
+ }, 1000)
+ }
+}
+
+export const searchTodoItems = value => {
+ return {
+ type: "SEARCH_TODO_ITEMS",
+ values: {
+ searchText: value
+ }
+ }
+}
+
+export const deleteTodo = id => {
+ return dispatch => {
+ dispatch(toggleBackdrop());
+ setTimeout(() => {
+ dispatch({
+ type: "DELETE_TODO",
+ values: {
+ id
+ }
+ });
+ dispatch(toggleBackdrop());
+ }, 500)
+
+ }
+}
+
+export const toggleAlertBox = info => {
+ return {
+ type: "TOGGLE_ALERT_BOX",
+ values: {
+ ...info
+ }
+ }
+}
+
+export const openTodoForm = (id, isEditable) => {
+ return {
+ type: "TOGGLE_TODO_FORM",
+ values: {
+ id,
+ isEditable
+ }
+ }
+}
+
+export const toggleTaskStatus = id => {
+ return {
+ type: "TOGGLE_COMPLETE",
+ values: {
+ id
+ }
+ }
+}
+
+export const saveTodoItem = values => {
+ return dispatch => {
+ dispatch(toggleBackdrop());
+ setTimeout(() => {
+ dispatch({
+ type: "ADD_TODO_LIST",
+ values : {
+ ...values
+ }
+ })
+ dispatch(toggleBackdrop());
+ }, 500)
+ }
+}
+
+export const toggleBackdrop = () => {
+ return {
+ type: "TOGGLE_BACKDROP"
+ }
+}
+
+export const markListAsDone = (ids) => {
+ return {
+ type: "MARK_LIST_DONE",
+ values: {
+ ids
+ }
+ }
+}
+
+export const markListAsPending = (ids) => {
+ return {
+ type: "MARK_LIST_PENDING",
+ values: {
+ ids
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js
new file mode 100644
index 00000000..20cc9223
--- /dev/null
+++ b/src/store/reducers/index.js
@@ -0,0 +1,8 @@
+import { combineReducers } from 'redux';
+import todoReducer from "./todoReducer";
+import uiReducer from './uiReducer';
+
+export default combineReducers({
+ todos: todoReducer,
+ ui: uiReducer
+})
\ No newline at end of file
diff --git a/src/store/reducers/todoReducer.js b/src/store/reducers/todoReducer.js
new file mode 100644
index 00000000..37648222
--- /dev/null
+++ b/src/store/reducers/todoReducer.js
@@ -0,0 +1,126 @@
+import { searchableFields } from '../../config';
+
+
+const initialState = {
+ isDataLoaded: false,
+ data: [],
+ isFormOpen: false,
+ isEditMode: true,
+ selectedTodoId: null,
+ selectedTodoItem: {},
+ lastEditTimestamp: null,
+ filteredData: []
+};
+
+
+export default function (state = initialState, action) {
+ const updatedState = { ...state };
+ switch (action.type) {
+ case "GET_TODO_LIST":
+ updatedState.data = action.values.data;
+ updatedState.filteredData = action.values.data;
+ updatedState.isDataLoaded = true;
+ break;
+
+ case "TOGGLE_COMPLETE":
+ const id = action.values.id;
+ const item = updatedState.data.find(item => item.id === id);
+ if (item.status === "pending") item.status = "completed";
+ else if (item.status === "completed") item.status = "pending";
+
+ updatedState.lastEditTimestamp = new Date().getTime();
+ break;
+
+ case "ADD_TODO_LIST":
+ const fieldValues = action.values;
+ if (updatedState.selectedTodoId > 0 || fieldValues.id > 0) {
+ const id = updatedState.selectedTodoId || fieldValues.id;
+ const record = updatedState.data.find(item => item.id === id);
+ record.summary = fieldValues.summary;
+ record.description = fieldValues.description;
+ record.priority = fieldValues.priority;
+ record.dueDate = fieldValues.dueDate;
+ } else {
+ fieldValues.id = updatedState.data.length + 1;
+ fieldValues.status = "pending";
+ updatedState.data.push(fieldValues);
+ }
+ updatedState.lastEditTimestamp = new Date().getTime();
+ break;
+ case "DELETE_TODO":
+ const todoList = updatedState.data.filter((item => item.id !== action.values.id));
+ const filteredTodoList = updatedState.filteredData.filter((item => item.id !== action.values.id))
+ updatedState.data = todoList;
+ updatedState.filteredData = filteredTodoList;
+ break;
+
+ case "TOGGLE_TODO_FORM":
+ updatedState.isFormOpen = !updatedState.isFormOpen;
+ const values = action.values;
+
+ if (updatedState.isFormOpen && action.values && Object.keys(action.values).length > 0) {
+ updatedState.selectedTodoId = action.values.id;
+ if (values.isEditable) {
+ updatedState.isEditMode = true;
+ } else {
+ updatedState.isEditMode = false;
+ }
+ updatedState.selectedTodoItem = updatedState.data.find(item => item.id === action.values.id);
+ }
+ break;
+ case "SEARCH_TODO_ITEMS":
+ const searchText = action.values.searchText;
+ if (searchText === "") {
+ updatedState.filteredData = updatedState.data;
+ } else {
+ updatedState.filteredData = updatedState.data.filter(item => {
+ for (let field of searchableFields) {
+ if (item[field] && item[field].toLowerCase().includes(searchText.toLowerCase())) {
+ return item;
+ }
+ }
+ });
+ }
+
+ break;
+ case "MARK_LIST_DONE":
+ const ids = action.values.ids;
+ updatedState.data.forEach(data => {
+ ids.forEach(id => {
+ if (data.id === id) {
+ data.status = "completed";
+ }
+ })
+ });
+ updatedState.filteredData.forEach(data => {
+ ids.forEach(id => {
+ if (data.id === id) {
+ data.status = "completed";
+ }
+ })
+ });
+ updatedState.lastEditTimestamp = new Date().getTime();
+ break;
+ case "MARK_LIST_PENDING":
+ {
+ const ids = action.values.ids;
+ updatedState.data.forEach(data => {
+ ids.forEach(id => {
+ if (data.id === id) {
+ data.status = "pending";
+ }
+ })
+ })
+ updatedState.filteredData.forEach(data => {
+ ids.forEach(id => {
+ if (data.id === id) {
+ data.status = "pending";
+ }
+ })
+ })
+ updatedState.lastEditTimestamp = new Date().getTime();
+ }
+ break;
+ }
+ return updatedState;
+}
\ No newline at end of file
diff --git a/src/store/reducers/uiReducer.js b/src/store/reducers/uiReducer.js
new file mode 100644
index 00000000..185160f8
--- /dev/null
+++ b/src/store/reducers/uiReducer.js
@@ -0,0 +1,30 @@
+const initialState = {
+ isAlertOpen: false,
+ alertTitle: "Alert",
+ yesAction: undefined,
+ noAction: undefined,
+ alertMessage: "Message !!",
+ isBackdropOpen: false
+}
+
+
+export default function (state = initialState, action) {
+ const updatedState = {...state};
+ switch (action.type) {
+ case "TOGGLE_ALERT_BOX":
+ updatedState.isAlertOpen = !updatedState.isAlertOpen;
+ if(action.values){
+ updatedState.alertTitle = action.values.title;
+ updatedState.yesAction = action.values.yesAction;
+ updatedState.noAction = action.values.noAction;
+ updatedState.alertMessage = action.values.alertMessage;
+ }
+ break;
+ case "TOGGLE_BACKDROP":
+ updatedState.isBackdropOpen = !updatedState.isBackdropOpen;
+ break;
+ default:
+ break;
+ }
+ return updatedState;
+}
\ No newline at end of file
diff --git a/src/store/staticData/todoListData.js b/src/store/staticData/todoListData.js
new file mode 100644
index 00000000..e9db4eb6
--- /dev/null
+++ b/src/store/staticData/todoListData.js
@@ -0,0 +1,33 @@
+const todoListData = [{
+ id: 1, summary: "Start Sureify Project", description: "description for todo 1", status: "pending",
+ priority: "high",
+ createdOn: new Date(2020, 4, 2),
+ dueDate: new Date(2020, 4, 6)
+},{
+ id: 2, summary: "Use React, Redux, Webpack", description: "description for todo 2", status: "pending",
+ priority: "medium",
+ createdOn: new Date(2020, 4, 3),
+ dueDate: new Date(2020, 4, 7)
+},{
+ id: 3, summary: "Include Material UI", description: "description for todo 3", status: "pending",
+ priority: "high",
+ createdOn: new Date(2020, 5, 1),
+ dueDate: new Date(2020, 1, 6)
+},{
+ id: 4, summary: "Develop some components", description: "description for todo 4", status: "completed",
+ priority: "low",
+ createdOn: new Date(2019, 4, 2),
+ dueDate: new Date(2019, 5, 6)
+},{
+ id: 5, summary: "Test all the features", description: "description for todo 5", status: "pending",
+ priority: "high",
+ createdOn: new Date(2020, 4, 2),
+ dueDate: new Date(2020, 4, 6)
+},{
+ id: 6, summary: "Take bouns points", description: "description for todo 6", status: "completed",
+ priority: "medium",
+ createdOn: new Date(2020, 4, 2),
+ dueDate: new Date(2020, 4, 6)
+}];
+
+export default todoListData;
\ No newline at end of file
diff --git a/src/theme.js b/src/theme.js
new file mode 100644
index 00000000..b92b1e57
--- /dev/null
+++ b/src/theme.js
@@ -0,0 +1,31 @@
+import { createMuiTheme } from "@material-ui/core";
+
+const theme = createMuiTheme({
+ palette:{
+ primary: {
+ main: '#03a9f4'
+ },
+ secondary:{
+ main: '#03a9f4'
+ }
+ },
+ overrides: {
+ MuiFormControl: {
+ root:{
+ minWidth: '120px'
+ }
+ },
+ MuiIconButton: {
+ root: {
+ color: '#03a9f4'
+ }
+ },
+ MuiDialogTitle: {
+ root:{
+ padding: "16px 24px 0 24px"
+ }
+ }
+ }
+});
+
+export default theme;
\ No newline at end of file
diff --git a/src/utils/debounce.js b/src/utils/debounce.js
new file mode 100644
index 00000000..41a72076
--- /dev/null
+++ b/src/utils/debounce.js
@@ -0,0 +1,11 @@
+export default function debounce(fn, delay){
+ let timer;
+ return function(){
+ const args = arguments;
+ const context = this;
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ fn.apply(context, args);
+ }, delay);
+ }
+};
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 00000000..5de38d23
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,41 @@
+// old// const path = require('path');// const HtmlWebpackPlugin = require('html-webpack-plugin');
+// new
+import path from 'path';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+module.exports = {
+ entry: path.join(__dirname,'src','index.js'),
+ output: {
+ path: path.join(__dirname,'build'),
+ filename: 'index.bundle.js'
+ },
+ mode: process.env.NODE_ENV || 'development',
+ resolve: {
+ modules: [path.resolve(__dirname, 'src'), 'node_modules']
+ },
+ devServer: {
+ contentBase: path.join(__dirname,'src')
+ },
+ module: {
+ rules: [{
+ // this is so that we can compile any React,
+ // ES6 and above into normal ES5 syntax
+ test: /\.(js|jsx)$/,
+ // we do not want anything from node_modules to be compiled
+ exclude: /node_modules/,
+ use: ['babel-loader']
+ },{
+ test: /\.(css|scss)$/,
+ use: [
+ "style-loader", // creates style nodes from JS strings
+ "css-loader", // translates CSS into CommonJS
+ "sass-loader" // compiles Sass to CSS, using Node Sass by default
+ ]
+ },{
+ test: /\.(jpg|jpeg|png|gif|mp3|svg)$/,
+ loaders: ['file-loader']
+ }]
+ },
+ plugins: [
+ new HtmlWebpackPlugin({template: path.join(__dirname,'src','index.html') })
+ ]
+};