├── .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 |
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 |
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 | Description |
126 | Value ($) |
127 | Status |
128 | Date |
129 |
130 |
131 |
132 | {rows}
133 |
134 |
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 |
8 |
--------------------------------------------------------------------------------