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;