diff --git a/.gitignore b/.gitignore
index 6704566..6049740 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,3 +102,9 @@ dist
# TernJS port file
.tern-port
+
+.vscode
+
+.eslintrc.json
+
+package-lock.json
\ No newline at end of file
diff --git a/README.md b/README.md
index 10d0299..5a9a93a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
# todo-react
-A simple todo app written in React
+A simple todo app written in React with Redux
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9f1ecc8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "todo",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@material-ui/core": "^4.9.1",
+ "@material-ui/icons": "^4.9.1",
+ "@testing-library/jest-dom": "^4.2.4",
+ "@testing-library/react": "^9.4.0",
+ "@testing-library/user-event": "^7.2.1",
+ "react": "^16.12.0",
+ "react-dom": "^16.12.0",
+ "react-redux": "^7.1.3",
+ "react-scripts": "3.4.0",
+ "redux": "^4.0.5"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": "react-app"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-plugin-react": "^7.18.0"
+ }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..199b90c
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Todo App
+
+
+
+
+
+
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000..080d6c7
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/src/actions/actions.js b/src/actions/actions.js
new file mode 100644
index 0000000..1ee3943
--- /dev/null
+++ b/src/actions/actions.js
@@ -0,0 +1,29 @@
+const addTodo = content => {
+ return {
+ type: "ADD_TODO",
+ content: content
+ }
+}
+
+const removeTodo = id => {
+ return {
+ type: "REMOVE_TODO",
+ id: id
+ }
+}
+
+const mark_checked = id => {
+ return {
+ type: "MARK_CHECKED",
+ id: id
+ }
+}
+
+const searchTodo = keyword => {
+ return {
+ type: "SEARCH",
+ keyword
+ }
+}
+
+export {addTodo, mark_checked, removeTodo, searchTodo}
\ No newline at end of file
diff --git a/src/components/Add.js b/src/components/Add.js
new file mode 100644
index 0000000..0031969
--- /dev/null
+++ b/src/components/Add.js
@@ -0,0 +1,69 @@
+import React, { useState } from "react";
+import { connect } from "react-redux";
+import { addTodo } from "../actions/actions";
+import { Grid } from "@material-ui/core";
+import InputAdornment from "@material-ui/core/InputAdornment";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import PropTypes from "prop-types";
+import PlusIcon from "@material-ui/icons/AddRounded";
+
+function Add(props) {
+ const [value, setValue] = useState("");
+
+ function onKeyDown(e) {
+ if (e.keyCode === 13 && !/^\s*$/.test(e.target.value)) {
+ props.addTodo(e.target.value);
+ setValue("");
+ }
+ }
+
+ function onClick() {
+ if (!/^\s*$/.test(value)) {
+ props.addTodo(value);
+ setValue("");
+ }
+ }
+
+ return (
+
+
+ onKeyDown(e)}
+ onChange={e => {
+ setValue(e.target.value);
+ }}
+ value={value}
+ autoFocus={true}
+ InputProps={{
+ startAdornment: (
+
+
+
+ )
+ }}
+ />
+
+
+
+
+
+ );
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ addTodo: id => dispatch(addTodo(id))
+ };
+};
+
+Add.propTypes = {
+ addTodo: PropTypes.func
+};
+
+export default connect(null, mapDispatchToProps)(Add);
diff --git a/src/components/App.js b/src/components/App.js
new file mode 100644
index 0000000..6d4c257
--- /dev/null
+++ b/src/components/App.js
@@ -0,0 +1,58 @@
+import React from "react";
+import Grid from "@material-ui/core/Grid";
+import Typography from "@material-ui/core/Typography";
+import Add from "./Add";
+import Items from "./Items/items";
+import Search from "./Search";
+import Counter from "./Counter";
+import PropTypes from "prop-types";
+class App extends React.Component {
+ render() {
+ return (
+ <>
+
+
+ Todo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ }
+}
+
+App.propTypes = {
+ title: PropTypes.string
+};
+
+export default App;
\ No newline at end of file
diff --git a/src/components/Counter.js b/src/components/Counter.js
new file mode 100644
index 0000000..c42ebd8
--- /dev/null
+++ b/src/components/Counter.js
@@ -0,0 +1,93 @@
+import React from "react";
+import { connect } from "react-redux";
+import PropTypes from "prop-types";
+import Grid from "@material-ui/core/Grid";
+import Typography from "@material-ui/core/Typography";
+function Counter(props) {
+ const { totalCount, doneCount, notDoneCount } = props;
+ return (
+ <>
+
+
+
+
+ {totalCount > 9 ? totalCount : "0" + totalCount}
+
+
+
+
+ Todo{totalCount > 1 || totalCount === 0 ? "s" : ""}
+
+
+
+
+
+
+ {doneCount > 9 ? doneCount : "0" + doneCount}
+
+
+
+ Done
+
+
+
+
+
+ {notDoneCount > 9 ? notDoneCount : "0" + notDoneCount}
+
+
+
+ Not Done
+
+
+
+ >
+ );
+}
+
+const mapStateToProps = state => {
+ let totalCount = state.items ? state.items.length : 0,
+ doneCount = 0,
+ notDoneCount = 0;
+ if (state.items)
+ state.items.forEach(item => {
+ item.checked === true ? doneCount++ : notDoneCount++;
+ });
+ return { totalCount, doneCount, notDoneCount };
+};
+
+Counter.propTypes = {
+ totalCount: PropTypes.number,
+ doneCount: PropTypes.number,
+ notDoneCount: PropTypes.number
+};
+
+export default connect(mapStateToProps, null)(Counter);
\ No newline at end of file
diff --git a/src/components/Items/item/item.js b/src/components/Items/item/item.js
new file mode 100644
index 0000000..cb17bc6
--- /dev/null
+++ b/src/components/Items/item/item.js
@@ -0,0 +1,53 @@
+import React from "react";
+import Checkbox from "@material-ui/core/Checkbox";
+import Grid from "@material-ui/core/Grid";
+import Paper from "@material-ui/core/Paper";
+import IconButton from "@material-ui/core/IconButton";
+import DeleteIcon from "@material-ui/icons/Delete";
+import Typography from "@material-ui/core/Typography";
+import PropTypes from "prop-types";
+
+class Item extends React.Component {
+ render() {
+ return (
+
+
+
+
+ this.props.check(this.props.id)}
+ />
+
+
+
+ {this.props.content}
+
+
+
+ this.props.onDel(this.props.id)}
+ >
+
+
+
+
+
+
+ );
+ }
+}
+
+Item.propTypes = {
+ checked: PropTypes.bool,
+ id: PropTypes.string,
+ content: PropTypes.string,
+ check: PropTypes.func,
+ onDel: PropTypes.func
+};
+
+export default Item;
\ No newline at end of file
diff --git a/src/components/Items/items.js b/src/components/Items/items.js
new file mode 100644
index 0000000..32abb22
--- /dev/null
+++ b/src/components/Items/items.js
@@ -0,0 +1,43 @@
+import React from "react";
+import Item from "./item/item";
+import { connect } from "react-redux";
+import { mark_checked, removeTodo } from "../../actions/actions";
+
+function Items(props) {
+ const unchecked_items = [];
+ const checked_items = [];
+ const { items = [], filtered_items = [], onDel, check} = props;
+ const itemsToRender = filtered_items.length ? filtered_items : items;
+
+ const itemToPush = (item) => {
+ const {id} = item;
+ return ;
+ }
+
+ itemsToRender.forEach(item => {
+ if (item.checked)
+ checked_items.push(
+ itemToPush(item)
+ );
+ else
+ unchecked_items.push(
+ itemToPush(item)
+ );
+ });
+ return unchecked_items.concat(checked_items);
+}
+
+const mapStateToProps = state => {
+ return {
+ ...state
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ check: id => dispatch(mark_checked(id)),
+ onDel: id => dispatch(removeTodo(id))
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(Items);
\ No newline at end of file
diff --git a/src/components/Search.js b/src/components/Search.js
new file mode 100644
index 0000000..1581a92
--- /dev/null
+++ b/src/components/Search.js
@@ -0,0 +1,39 @@
+import React from "react";
+import { connect } from "react-redux";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+import SearchIcon from '@material-ui/icons/Search';
+import { searchTodo } from "../actions/actions";
+import PropTypes from "prop-types";
+
+function Search(props) {
+ return (
+ {
+ props.searchTodo(e.target.value);
+ }}
+ InputProps={{
+ startAdornment: (
+
+
+
+ )
+ }}
+ />
+ );
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ searchTodo: keyword => dispatch(searchTodo(keyword))
+ };
+};
+
+Search.propTypes = {
+ searchTodo: PropTypes.func
+};
+
+export default connect(null, mapDispatchToProps)(Search);
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..54429da
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,31 @@
+body {
+ background-color: white;
+ font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen,
+ Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ margin: 0 !important;
+}
+
+#root {
+ display: flex;
+ height: 100vh;
+}
+
+.title {
+ text-shadow: 2px 2px 3px rgba(110, 110, 110, 0.637);
+ text-align: center;
+ text-transform: uppercase;
+ color: rgb(116, 116, 116);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu,
+ Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+}
+
+#addBtn {
+ background-color: rgb(0, 110, 228);
+ color: white;
+ padding: auto 10px;
+}
+
+.checked {
+ color: rgb(173, 173, 173);
+ text-decoration: line-through;
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..2831c16
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,15 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+import rootReducer from './reducers/todos';
+import App from './components/App';
+import "./index.css";
+
+const store = createStore(rootReducer);
+
+ReactDOM.render(
+
+
+
+ , document.getElementById("root"));
\ No newline at end of file
diff --git a/src/reducers/todos.js b/src/reducers/todos.js
new file mode 100644
index 0000000..5349261
--- /dev/null
+++ b/src/reducers/todos.js
@@ -0,0 +1,55 @@
+const todos = (state = {}, action) => {
+ switch (action.type) {
+ case "ADD_TODO": {
+ const { items = [] } = state;
+ return {
+ ...state,
+ items: [
+ ...items,
+ {
+ id: Date.now(),
+ content: action.content,
+ checked: false
+ }
+ ]
+ };
+ }
+
+ case "REMOVE_TODO":
+ return {
+ ...state,
+ items: state.items.filter(item => {
+ return item.id !== action.id;
+ })
+ };
+
+ case "MARK_CHECKED":
+ return {
+ ...state,
+ items: state.items.map(item => {
+ if (item.id === action.id) item.checked = !item.checked;
+ return item;
+ })
+ };
+
+ case "SEARCH": {
+ // const {filtered_items = []} = state;
+ const { items = [] } = state;
+
+ if (!(action.keyword === "")) {
+ return {
+ ...state,
+ filtered_items: items.filter(item => {
+ const keyword = new RegExp(`${action.keyword}`, "i");
+ return keyword.test(item.content);
+ })
+ };
+ } else return { ...state, filtered_items: [] };
+ }
+
+ default:
+ return state;
+ }
+};
+
+export default todos;