├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── App.css ├── App.js ├── App.test.js ├── ExpenseForm.js ├── ExpenseManager.js ├── ExpenseTable.js ├── index.css ├── index.js └── logo.svg /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "09:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4a6f616f Gracinha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Expense Manager 2 | A react app that manages expenses 3 | 4 | ## Live demo at 5 | #### [https://jgcmarins.github.io/react-expense-manager/](https://jgcmarins.github.io/react-expense-manager/) 6 | 7 | ## Usage 8 | #### Installation 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | #### Run 14 | ```bash 15 | npm start 16 | ``` 17 | 18 | ## LICENSE 19 | [MIT](https://github.com/jgcmarins/react-expense-manager/blob/master/LICENSE) 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expense-manager", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "bootstrap": "^5.0.0", 7 | "moment": "^2.18.1", 8 | "react": "^17.0.1", 9 | "react-bootstrap": "^1.0.0", 10 | "react-dom": "^17.0.1", 11 | "react-scripts": "^4.0.0" 12 | }, 13 | "devDependencies": { 14 | "gh-pages": "^3.0.0", 15 | "react-scripts": "4.0.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject", 22 | "predeploy": "npm run build", 23 | "deploy": "gh-pages -d build" 24 | }, 25 | "homepage": "https://jgcmarins.github.io/react-expense-manager" 26 | } 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgcmarins/react-expense-manager/76ba652a8268ae30c29157cdc7aeef75bc6f5a24/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React App 17 | 18 | 19 |
20 | 30 | 31 | 32 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ExpenseManager from './ExpenseManager'; 3 | 4 | class App extends Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/ExpenseForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormControl, FormGroup, ControlLabel, HelpBlock, Checkbox, Button } from 'react-bootstrap'; 3 | 4 | 5 | class ExpenseForm extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.handleDescriptionChange = this.handleDescriptionChange.bind(this); 10 | this.handleValueChange = this.handleValueChange.bind(this); 11 | this.handleStatusChange = this.handleStatusChange.bind(this); 12 | this.handleFormSubmit = this.handleFormSubmit.bind(this); 13 | } 14 | 15 | getValidationStateDescription() { 16 | if(this.props.description.length === 0) return null; 17 | else if(this.props.description.length < 3) return 'error'; 18 | else return 'success'; 19 | } 20 | 21 | getValidationStateValue() { 22 | if(this.props.value.length === 0) return null; 23 | else if(isNaN(this.props.value)) return 'error'; 24 | else return 'success'; 25 | } 26 | 27 | handleDescriptionChange(e) { 28 | e.preventDefault(); 29 | this.props.onDescription(e.target.value); 30 | } 31 | 32 | handleValueChange(e) { 33 | e.preventDefault(); 34 | this.props.onValue(e.target.value); 35 | } 36 | 37 | handleStatusChange(e) { 38 | this.props.onStatus(); 39 | } 40 | 41 | getValidation() { 42 | if(this.getValidationStateDescription() === 'error' || this.getValidationStateValue() === 'error') return false; 43 | else if(this.getValidationStateDescription() === null || this.getValidationStateValue() === null) return false; 44 | return true; 45 | } 46 | 47 | handleFormSubmit(e) { 48 | e.preventDefault(); 49 | if(this.getValidation()) this.props.onForm(); 50 | else { 51 | alert('Please, fill the fields correctly.'); 52 | } 53 | } 54 | 55 | render() { 56 | return ( 57 |
58 |

Expense Info

59 |
60 | 64 | Description 65 | 71 | 72 | Must have at least 3 characters 73 | 74 | 75 | 79 | Value ($) 80 | 86 | 87 | Must be a number 88 | 89 | 90 | 94 | Is paid? 95 | 96 | 97 | 102 | 103 |
104 | 105 |
106 | ); 107 | } 108 | } 109 | 110 | export default ExpenseForm; 111 | -------------------------------------------------------------------------------- /src/ExpenseManager.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Col } from 'react-bootstrap'; 3 | import ExpenseForm from './ExpenseForm'; 4 | import ExpenseTable from './ExpenseTable'; 5 | 6 | class ExpenseManager extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | id: '', 11 | description: '', 12 | value: '', 13 | status: false, 14 | date: '', 15 | expenses: [], 16 | index: -1, 17 | }; 18 | 19 | this.handleDescription = this.handleDescription.bind(this); 20 | this.handleValue = this.handleValue.bind(this); 21 | this.handleStatus = this.handleStatus.bind(this); 22 | this.handleForm = this.handleForm.bind(this); 23 | 24 | this.handleEdit = this.handleEdit.bind(this); 25 | } 26 | 27 | generateId() { 28 | return this.state.expenses.length + 1; 29 | } 30 | 31 | updateExpense() { 32 | this 33 | .state 34 | .expenses 35 | .splice( 36 | this.state.index, 37 | 1, 38 | { 39 | id: this.state.id, 40 | description: this.state.description, 41 | value: this.state.value, 42 | status: this.state.status, 43 | date: this.state.date, 44 | } 45 | ); 46 | this.setState({ 47 | index: -1, 48 | }); 49 | } 50 | 51 | newExpense() { 52 | this.state.expenses.push( 53 | { 54 | id: this.generateId(), 55 | description: this.state.description, 56 | value: this.state.value, 57 | status: this.state.status, 58 | date: Date.now(), 59 | } 60 | ); 61 | } 62 | 63 | handleDescription(description) { 64 | this.setState({ 65 | description: description, 66 | }); 67 | } 68 | 69 | handleValue(value) { 70 | this.setState({ 71 | value: value, 72 | }); 73 | } 74 | 75 | handleStatus() { 76 | this.setState({ 77 | status: !this.state.status, 78 | }); 79 | } 80 | 81 | handleForm() { 82 | if(this.state.index === -1) { 83 | this.newExpense(); 84 | } else { 85 | this.updateExpense(); 86 | } 87 | this.setState({ 88 | id: '', 89 | description: '', 90 | value: '', 91 | status: false, 92 | date: '', 93 | }); 94 | } 95 | 96 | handleEdit(id) { 97 | var index = this.state.expenses.findIndex((expense) => { return expense.id === id }); 98 | this.setState({ 99 | index: index, 100 | }); 101 | var expense = this.state.expenses[index]; 102 | this.setState({ 103 | id: expense.id, 104 | description: expense.description, 105 | value: expense.value, 106 | status: expense.status, 107 | date: expense.date, 108 | }); 109 | } 110 | 111 | render() { 112 | return ( 113 |
114 |
115 |
116 | 117 | 126 | 127 | 128 | 132 | 133 |
134 |
135 | 136 |
137 | Star 138 |
139 | 140 |
141 |
142 |
143 | ); 144 | } 145 | } 146 | 147 | export default ExpenseManager; 148 | -------------------------------------------------------------------------------- /src/ExpenseTable.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table, Label, Alert, Nav, NavItem } from 'react-bootstrap'; 3 | 4 | class ExpenseRow extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.handleEditClickRow = this.handleEditClickRow.bind(this); 9 | } 10 | 11 | handleEditClickRow(e) { 12 | e.preventDefault(); 13 | this.props.onEditClick(this.props.id); 14 | } 15 | 16 | render() { 17 | var status = this.props.expense.status ? : ; 18 | var moment = require('moment'); 19 | return ( 20 | 21 | {this.props.expense.description} 22 | {this.props.expense.value} 23 | {status} 24 | {moment(this.props.expense.date).format("DD/MM/YYYY HH:mm")} 25 | 26 | ); 27 | } 28 | } 29 | 30 | class ExpenseNav extends Component { 31 | constructor(props) { 32 | super(props); 33 | 34 | this.handleSelectTab = this.handleSelectTab.bind(this); 35 | } 36 | 37 | handleSelectTab(eventKey) { 38 | event.preventDefault(); 39 | this.props.onSelectTab(eventKey); 40 | } 41 | 42 | render() { 43 | return ( 44 | 49 | ); 50 | } 51 | } 52 | 53 | class ExpenseTable extends Component { 54 | constructor(props) { 55 | super(props); 56 | this.state = { 57 | tab: '1', 58 | status: '', 59 | }; 60 | 61 | this.handleEditClick = this.handleEditClick.bind(this); 62 | this.handleSelect = this.handleSelect.bind(this); 63 | } 64 | 65 | handleEditClick(id) { 66 | this.props.onEdit(id); 67 | } 68 | 69 | handleSelect(eventKey) { 70 | var status = ''; 71 | if(eventKey !== '1') { 72 | status = 'status'; 73 | } 74 | this.setState({ 75 | tab: eventKey, 76 | status: status, 77 | }); 78 | } 79 | 80 | prepareRows() { 81 | var expenses = this.props.expenses.slice(); 82 | if(this.state.tab === '1') { 83 | return expenses; 84 | } else { 85 | var rows = []; 86 | expenses.forEach((expense) => { 87 | if(this.state.tab === '2' & expense.status) { 88 | rows.push(expense); 89 | } else if(this.state.tab === '3' & !expense.status) { 90 | rows.push(expense); 91 | } 92 | }); 93 | return rows; 94 | } 95 | } 96 | 97 | render() { 98 | var expenses = this.prepareRows(); 99 | var rows = []; 100 | expenses.forEach((expense) => { 101 | rows.unshift( 102 | 109 | ); 110 | }); 111 | 112 | return ( 113 |
114 |

Expense List

115 | You can also edit an expense by clicking on it 116 | 117 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {rows} 133 | 134 |
DescriptionValue ($)StatusDate
135 |
136 | ); 137 | } 138 | } 139 | 140 | export default ExpenseTable; 141 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .status { 8 | display: none; 9 | } 10 | 11 | .github { 12 | text-align: center; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | import 'bootstrap/dist/css/bootstrap.css'; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------