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 ( +
+ + {alertTitle} + + + {alertMessage} + + + + + + + +
+ ); +} 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 ( + + Add Task + + { + setSummary(e.target.value); + if(e.target.value.length < 10){ + setSummaryError(true) + }else{ + setSummaryError(false) + } + }} + /> + { + setDescription(e.target.value); + if(e.target.value.length < 10){ + setDescriptionError(true); + }else{ + setDescriptionError(false); + } + }} + /> +

+
+ + + + setDueDate(e.target.value)} + /> + + { + selectedTodoItem.createdOn && isEditMode === false ? ( + + ): null + } + { + selectedTodoItem.status && isEditMode === false ? ( +
+
+ Status: {selectedTodoItem.status} +
+ ): null + } + +
+
+ + + + +
+ ) +}; +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 ( + + ); + } + + 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') }) + ] +};