├── .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 |
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 |
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 |
13 | {transactions.map(transaction => ())}
14 |
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 |
--------------------------------------------------------------------------------