├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── components │ ├── AddTransaction.js │ ├── Balance.js │ ├── Header.js │ ├── IncomeExpenses.js │ ├── Transaction.js │ └── TransactionList.js ├── context │ ├── AppReducer.js │ └── GlobalState.js └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expense Tracker (React) 2 | 3 | This is a React version of the [vanilla JS Expense Tracker](https://github.com/bradtraversy/vanillawebprojects/tree/master/expense-tracker). It uses functional components with hooks and the context API 4 | 5 | ## Usage 6 | ``` 7 | npm install 8 | 9 | # Run on http://localhost:3000 10 | npm start 11 | 12 | # Build for prod 13 | npm run build 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expense-tracker-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-scripts": "3.4.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/expense-tracker-react/5e5ad9ad6f0929f80e1c9f6667b08870f87e7743/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/expense-tracker-react/5e5ad9ad6f0929f80e1c9f6667b08870f87e7743/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/expense-tracker-react/5e5ad9ad6f0929f80e1c9f6667b08870f87e7743/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Lato&display=swap'); 2 | 3 | :root { 4 | --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | background-color: #f7f7f7; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | min-height: 100vh; 18 | margin: 0; 19 | font-family: 'Lato', sans-serif; 20 | } 21 | 22 | .container { 23 | margin: 30px auto; 24 | width: 350px; 25 | } 26 | 27 | h1 { 28 | letter-spacing: 1px; 29 | margin: 0; 30 | } 31 | 32 | h3 { 33 | border-bottom: 1px solid #bbb; 34 | padding-bottom: 10px; 35 | margin: 40px 0 10px; 36 | } 37 | 38 | h4 { 39 | margin: 0; 40 | text-transform: uppercase; 41 | } 42 | 43 | .inc-exp-container { 44 | background-color: #fff; 45 | box-shadow: var(--box-shadow); 46 | padding: 20px; 47 | display: flex; 48 | justify-content: space-between; 49 | margin: 20px 0; 50 | } 51 | 52 | .inc-exp-container > div { 53 | flex: 1; 54 | text-align: center; 55 | } 56 | 57 | .inc-exp-container > div:first-of-type { 58 | border-right: 1px solid #dedede; 59 | } 60 | 61 | .money { 62 | font-size: 20px; 63 | letter-spacing: 1px; 64 | margin: 5px 0; 65 | } 66 | 67 | .money.plus { 68 | color: #2ecc71; 69 | } 70 | 71 | .money.minus { 72 | color: #c0392b; 73 | } 74 | 75 | label { 76 | display: inline-block; 77 | margin: 10px 0; 78 | } 79 | 80 | input[type='text'], 81 | input[type='number'] { 82 | border: 1px solid #dedede; 83 | border-radius: 2px; 84 | display: block; 85 | font-size: 16px; 86 | padding: 10px; 87 | width: 100%; 88 | } 89 | 90 | .btn { 91 | cursor: pointer; 92 | background-color: #9c88ff; 93 | box-shadow: var(--box-shadow); 94 | color: #fff; 95 | border: 0; 96 | display: block; 97 | font-size: 16px; 98 | margin: 10px 0 30px; 99 | padding: 10px; 100 | width: 100%; 101 | } 102 | 103 | .btn:focus, 104 | .delete-btn:focus { 105 | outline: 0; 106 | } 107 | 108 | .list { 109 | list-style-type: none; 110 | padding: 0; 111 | margin-bottom: 40px; 112 | } 113 | 114 | .list li { 115 | background-color: #fff; 116 | box-shadow: var(--box-shadow); 117 | color: #333; 118 | display: flex; 119 | justify-content: space-between; 120 | position: relative; 121 | padding: 10px; 122 | margin: 10px 0; 123 | } 124 | 125 | .list li.plus { 126 | border-right: 5px solid #2ecc71; 127 | } 128 | 129 | .list li.minus { 130 | border-right: 5px solid #c0392b; 131 | } 132 | 133 | .delete-btn { 134 | cursor: pointer; 135 | background-color: #e74c3c; 136 | border: 0; 137 | color: #fff; 138 | font-size: 20px; 139 | line-height: 20px; 140 | padding: 2px 5px; 141 | position: absolute; 142 | top: 50%; 143 | left: 0; 144 | transform: translate(-100%, -50%); 145 | opacity: 0; 146 | transition: opacity 0.3s ease; 147 | } 148 | 149 | .list li:hover .delete-btn { 150 | opacity: 1; 151 | } 152 | 153 | @media (max-width: 320px) { 154 | .container { 155 | width: 300px; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header } from './components/Header'; 3 | import { Balance } from './components/Balance'; 4 | import { IncomeExpenses } from './components/IncomeExpenses'; 5 | import { TransactionList } from './components/TransactionList'; 6 | import { AddTransaction } from './components/AddTransaction'; 7 | 8 | import { GlobalProvider } from './context/GlobalState'; 9 | 10 | import './App.css'; 11 | 12 | function App() { 13 | return ( 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 |
22 | 23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/components/AddTransaction.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useContext} from 'react' 2 | import { GlobalContext } from '../context/GlobalState'; 3 | 4 | export const AddTransaction = () => { 5 | const [text, setText] = useState(''); 6 | const [amount, setAmount] = useState(0); 7 | 8 | const { addTransaction } = useContext(GlobalContext); 9 | 10 | const onSubmit = e => { 11 | e.preventDefault(); 12 | 13 | const newTransaction = { 14 | id: Math.floor(Math.random() * 100000000), 15 | text, 16 | amount: +amount 17 | } 18 | 19 | addTransaction(newTransaction); 20 | } 21 | 22 | return ( 23 | <> 24 |

Add new transaction

25 |
26 |
27 | 28 | setText(e.target.value)} placeholder="Enter text..." /> 29 |
30 |
31 | 35 | setAmount(e.target.value)} placeholder="Enter amount..." /> 36 |
37 | 38 |
39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Balance.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { GlobalContext } from '../context/GlobalState'; 3 | 4 | //Money formatter function 5 | function moneyFormatter(num) { 6 | let p = num.toFixed(2).split('.'); 7 | return ( 8 | '$ ' + (p[0].split('')[0]=== '-' ? '-' : '') + 9 | p[0] 10 | .split('') 11 | .reverse() 12 | .reduce(function (acc, num, i, orig) { 13 | return num === '-' ? acc : num + (i && !(i % 3) ? ',' : '') + acc; 14 | }, '') + 15 | '.' + 16 | p[1] 17 | ); 18 | } 19 | 20 | export const Balance = () => { 21 | const { transactions } = useContext(GlobalContext); 22 | 23 | const amounts = transactions.map(transaction => transaction.amount); 24 | 25 | const total = amounts.reduce((acc, item) => (acc += item), 0); 26 | 27 | return ( 28 | <> 29 |

Your Balance

30 |

{moneyFormatter(total)}

31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Header = () => { 4 | return ( 5 |

6 | Expense Tracker 7 |

8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/components/IncomeExpenses.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { GlobalContext } from '../context/GlobalState'; 3 | 4 | //Money formatter function 5 | function moneyFormatter(num) { 6 | let p = num.toFixed(2).split('.'); 7 | return ( 8 | '$ ' + 9 | p[0] 10 | .split('') 11 | .reverse() 12 | .reduce(function (acc, num, i, orig) { 13 | return num === '-' ? acc : num + (i && !(i % 3) ? ',' : '') + acc; 14 | }, '') + 15 | '.' + 16 | p[1] 17 | ); 18 | } 19 | 20 | export const IncomeExpenses = () => { 21 | const { transactions } = useContext(GlobalContext); 22 | 23 | const amounts = transactions.map(transaction => transaction.amount); 24 | 25 | const income = amounts 26 | .filter(item => item > 0) 27 | .reduce((acc, item) => (acc += item), 0); 28 | 29 | const expense = ( 30 | amounts.filter(item => item < 0).reduce((acc, item) => (acc += item), 0) * 31 | -1 32 | ); 33 | 34 | return ( 35 |
36 |
37 |

Income

38 |

{moneyFormatter(income)}

39 |
40 |
41 |

Expense

42 |

{moneyFormatter(expense)}

43 |
44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Transaction.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import { GlobalContext } from '../context/GlobalState'; 3 | 4 | //Money formatter function 5 | function moneyFormatter(num) { 6 | let p = num.toFixed(2).split('.'); 7 | return ( 8 | '$ ' + 9 | p[0] 10 | .split('') 11 | .reverse() 12 | .reduce(function (acc, num, i, orig) { 13 | return num === '-' ? acc : num + (i && !(i % 3) ? ',' : '') + acc; 14 | }, '') + 15 | '.' + 16 | p[1] 17 | ); 18 | } 19 | 20 | export const Transaction = ({ transaction }) => { 21 | const { deleteTransaction } = useContext(GlobalContext); 22 | 23 | const sign = transaction.amount < 0 ? '-' : '+'; 24 | 25 | return ( 26 |
  • 27 | {transaction.text} {sign}{moneyFormatter(transaction.amount)} 28 |
  • 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/TransactionList.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Transaction } from './Transaction'; 3 | 4 | import { GlobalContext } from '../context/GlobalState'; 5 | 6 | export const TransactionList = () => { 7 | const { transactions } = useContext(GlobalContext); 8 | 9 | return ( 10 | <> 11 |

    History

    12 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/context/AppReducer.js: -------------------------------------------------------------------------------- 1 | export default (state, action) => { 2 | switch(action.type) { 3 | case 'DELETE_TRANSACTION': 4 | return { 5 | ...state, 6 | transactions: state.transactions.filter(transaction => transaction.id !== action.payload) 7 | } 8 | case 'ADD_TRANSACTION': 9 | return { 10 | ...state, 11 | transactions: [action.payload, ...state.transactions] 12 | } 13 | default: 14 | return state; 15 | } 16 | } -------------------------------------------------------------------------------- /src/context/GlobalState.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useReducer } from 'react'; 2 | import AppReducer from './AppReducer'; 3 | 4 | // Initial state 5 | const initialState = { 6 | transactions: [] 7 | } 8 | 9 | // Create context 10 | export const GlobalContext = createContext(initialState); 11 | 12 | // Provider component 13 | export const GlobalProvider = ({ children }) => { 14 | const [state, dispatch] = useReducer(AppReducer, initialState); 15 | 16 | // Actions 17 | function deleteTransaction(id) { 18 | dispatch({ 19 | type: 'DELETE_TRANSACTION', 20 | payload: id 21 | }); 22 | } 23 | 24 | function addTransaction(transaction) { 25 | dispatch({ 26 | type: 'ADD_TRANSACTION', 27 | payload: transaction 28 | }); 29 | } 30 | 31 | return ( 36 | {children} 37 | ); 38 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | --------------------------------------------------------------------------------