├── .eslintcache
├── .github
└── FUNDING.yml
├── .gitignore
├── README.md
├── package.json
├── public
└── index.html
└── src
├── App.js
├── assets
└── money.png
├── components
├── Details
│ ├── Details.jsx
│ └── styles.js
├── InfoCard.jsx
├── Main
│ ├── Form
│ │ ├── Form.jsx
│ │ └── styles.js
│ ├── List
│ │ ├── List.jsx
│ │ └── styles.js
│ ├── Main.jsx
│ └── styles.js
├── Snackbar
│ ├── Snackbar.jsx
│ └── styles.js
└── index.js
├── constants
└── categories.js
├── context
├── context.js
└── contextReducer.js
├── index.css
├── index.js
├── styles.js
├── useTransactions.js
└── utils
└── formatDate.js
/.eslintcache:
--------------------------------------------------------------------------------
1 | [{"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\index.js":"1","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\App.js":"2","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\context\\context.js":"3","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\styles.js":"4","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\context\\contextReducer.js":"5","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\index.js":"6","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\InfoCard.jsx":"7","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\Main.jsx":"8","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Details\\Details.jsx":"9","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Snackbar\\Snackbar.jsx":"10","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Snackbar\\styles.js":"11","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\useTransactions.js":"12","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\constants\\categories.js":"13","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Details\\styles.js":"14","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\styles.js":"15","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\List\\List.jsx":"16","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\Form\\Form.jsx":"17","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\List\\styles.js":"18","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\Form\\styles.js":"19","C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\utils\\formatDate.js":"20"},{"size":431,"mtime":1607902054215,"results":"21","hashOfConfig":"22"},{"size":1440,"mtime":1607902054208,"results":"23","hashOfConfig":"22"},{"size":2032,"mtime":1607902054214,"results":"24","hashOfConfig":"22"},{"size":582,"mtime":1607902054215,"results":"25","hashOfConfig":"22"},{"size":583,"mtime":1608125576892,"results":"26","hashOfConfig":"22"},{"size":216,"mtime":1607902054213,"results":"27","hashOfConfig":"22"},{"size":460,"mtime":1607902054210,"results":"28","hashOfConfig":"22"},{"size":1271,"mtime":1607902054212,"results":"29","hashOfConfig":"22"},{"size":725,"mtime":1607902054210,"results":"30","hashOfConfig":"22"},{"size":781,"mtime":1607902054213,"results":"31","hashOfConfig":"22"},{"size":206,"mtime":1607902054213,"results":"32","hashOfConfig":"22"},{"size":1124,"mtime":1607902054215,"results":"33","hashOfConfig":"22"},{"size":1693,"mtime":1607902054214,"results":"34","hashOfConfig":"22"},{"size":247,"mtime":1607902054210,"results":"35","hashOfConfig":"22"},{"size":514,"mtime":1607902054212,"results":"36","hashOfConfig":"22"},{"size":1403,"mtime":1607902054212,"results":"37","hashOfConfig":"22"},{"size":5289,"mtime":1607949008975,"results":"38","hashOfConfig":"22"},{"size":416,"mtime":1607902054212,"results":"39","hashOfConfig":"22"},{"size":247,"mtime":1607902054211,"results":"40","hashOfConfig":"22"},{"size":301,"mtime":1607902054216,"results":"41","hashOfConfig":"22"},{"filePath":"42","messages":"43","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"bi7swa",{"filePath":"44","messages":"45","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"50","messages":"51","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"52","messages":"53","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"54","messages":"55","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"56","messages":"57","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"58","messages":"59","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"60","messages":"61","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"62","messages":"63","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"64","messages":"65","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"66","messages":"67","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"68","messages":"69","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"70","messages":"71","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"72","messages":"73","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"74","messages":"75","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"76","messages":"77","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"78","messages":"79","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"80","messages":"81","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\index.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\App.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\context\\context.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\styles.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\context\\contextReducer.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\index.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\InfoCard.jsx",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\Main.jsx",["82","83","84"],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Details\\Details.jsx",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Snackbar\\Snackbar.jsx",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Snackbar\\styles.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\useTransactions.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\constants\\categories.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Details\\styles.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\styles.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\List\\List.jsx",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\Form\\Form.jsx",["85","86","87"],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\List\\styles.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\components\\Main\\Form\\styles.js",[],"C:\\Users\\Adrian\\Desktop\\Projects\\speechly_expense_tracker_project\\src\\utils\\formatDate.js",["88"],{"ruleId":"89","severity":1,"message":"90","line":1,"column":17,"nodeType":"91","messageId":"92","endLine":1,"endColumn":25},{"ruleId":"89","severity":1,"message":"93","line":1,"column":27,"nodeType":"91","messageId":"92","endLine":1,"endColumn":36},{"ruleId":"89","severity":1,"message":"94","line":3,"column":10,"nodeType":"91","messageId":"92","endLine":3,"endColumn":26},{"ruleId":"89","severity":1,"message":"95","line":5,"column":10,"nodeType":"91","messageId":"92","endLine":5,"endColumn":23},{"ruleId":"89","severity":1,"message":"96","line":27,"column":10,"nodeType":"91","messageId":"92","endLine":27,"endColumn":20},{"ruleId":"97","severity":1,"message":"98","line":90,"column":6,"nodeType":"99","endLine":90,"endColumn":15,"suggestions":"100"},{"ruleId":"101","severity":1,"message":"102","line":1,"column":1,"nodeType":"103","endLine":11,"endColumn":3},"no-unused-vars","'useState' is defined but never used.","Identifier","unusedVar","'useEffect' is defined but never used.","'useSpeechContext' is defined but never used.","'BigTranscript' is defined but never used.","'isSpeaking' is assigned a value but never used.","react-hooks/exhaustive-deps","React Hook useEffect has missing dependencies: 'createTransaction' and 'formData'. Either include them or remove the dependency array. You can also do a functional update 'setFormData(f => ...)' if you only need 'formData' in the 'setFormData' call.","ArrayExpression",["104"],"import/no-anonymous-default-export","Assign arrow function to a variable before exporting as module default","ExportDefaultDeclaration",{"desc":"105","fix":"106"},"Update the dependencies array to be: [createTransaction, formData, segment]",{"range":"107","text":"108"},[3349,3358],"[createTransaction, formData, segment]"]
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: adrianhajdin
2 |
--------------------------------------------------------------------------------
/.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 | # Speechly Expense Tracker
2 |
3 | 
4 |
5 | ## Introduction
6 | This is a code repository for the corresponding video tutorial - https://youtu.be/NnUFOWR_V4Y.
7 |
8 | In this video, you're going to build a Complex Expense Budget Tracker. While building it, you're going to learn many advanced React & JavaScript topics. Some of them are State Management in React, Context API, Local Storage, Material UI, and how to create a scalable React folder structure. But most importantly, you're going to learn how to add voice capabilities to your applications using Speechly.
9 |
10 | Setup:
11 | - run ```npm i && npm start```
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "speechly",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.2",
7 | "@material-ui/icons": "^4.11.2",
8 | "@material-ui/lab": "^4.0.0-alpha.57",
9 | "@speechly/react-client": "0.0.4",
10 | "@speechly/react-ui": "^1.0.1",
11 | "@testing-library/jest-dom": "^5.11.6",
12 | "@testing-library/react": "^11.2.2",
13 | "@testing-library/user-event": "^12.5.0",
14 | "chart.js": "^2.9.4",
15 | "react": "^17.0.1",
16 | "react-chartjs-2": "^2.11.1",
17 | "react-dom": "^17.0.1",
18 | "react-scripts": "4.0.1",
19 | "uuid": "^8.3.1",
20 | "web-vitals": "^0.2.4"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Expense Tracker
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { Grid } from '@material-ui/core';
3 |
4 | import { SpeechState, useSpeechContext } from "@speechly/react-client";
5 | import { PushToTalkButton, PushToTalkButtonContainer } from '@speechly/react-ui';
6 |
7 | import { Details, Main } from './components';
8 | import useStyles from './styles';
9 |
10 | const App = () => {
11 | const classes = useStyles();
12 | const { speechState } = useSpeechContext();
13 | const main = useRef(null)
14 |
15 | const executeScroll = () => main.current.scrollIntoView()
16 |
17 | useEffect(() => {
18 | if (speechState === SpeechState.Recording) {
19 | executeScroll();
20 | }
21 | }, [speechState]);
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default App;
47 |
--------------------------------------------------------------------------------
/src/assets/money.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/speechly_expense_tracker_project/132225454dcfedba080815dabbe7654aee533c73/src/assets/money.png
--------------------------------------------------------------------------------
/src/components/Details/Details.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardHeader, CardContent, Typography } from '@material-ui/core';
3 | import { Doughnut } from 'react-chartjs-2';
4 |
5 | import useStyles from './styles';
6 | import useTransactions from '../../useTransactions';
7 |
8 | const DetailsCard = ({ title, subheader }) => {
9 | const { total, chartData } = useTransactions(title);
10 | const classes = useStyles();
11 |
12 | return (
13 |
14 |
15 |
16 | ${total}
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default DetailsCard;
24 |
--------------------------------------------------------------------------------
/src/components/Details/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles(() => ({
4 | income: {
5 | borderBottom: '10px solid rgba(0, 255, 0, 0.5)',
6 | },
7 | expense: {
8 | borderBottom: '10px solid rgba(255, 0, 0, 0.5)',
9 | },
10 | }));
11 |
--------------------------------------------------------------------------------
/src/components/InfoCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const isIncome = Math.round(Math.random());
4 |
5 | const InfoCard = () => {
6 | return (
7 |
8 | Try saying:
9 | Add {isIncome ? 'Income ' : 'Expense '}
10 | for {isIncome ? '$100 ' : '$50 '}
11 | in Category {isIncome ? 'Salary ' : 'Travel '}
12 | for {isIncome ? 'Monday ' : 'Thursday '}
13 |
14 | );
15 | };
16 |
17 | export default InfoCard;
18 |
--------------------------------------------------------------------------------
/src/components/Main/Form/Form.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, useEffect } from 'react';
2 | import { TextField, Typography, Grid, Button, FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
3 | import { v4 as uuidv4 } from 'uuid';
4 |
5 | import { useSpeechContext } from '@speechly/react-client';
6 | import Snackbar from '../../Snackbar/Snackbar';
7 | import formatDate from '../../../utils/formatDate';
8 | import { ExpenseTrackerContext } from '../../../context/context';
9 | import { incomeCategories, expenseCategories } from '../../../constants/categories';
10 | import useStyles from './styles';
11 |
12 | const initialState = {
13 | amount: '',
14 | category: '',
15 | type: 'Income',
16 | date: formatDate(new Date()),
17 | };
18 |
19 | const NewTransactionForm = () => {
20 | const classes = useStyles();
21 | const { addTransaction } = useContext(ExpenseTrackerContext);
22 | const [formData, setFormData] = useState(initialState);
23 | const { segment } = useSpeechContext();
24 | const [open, setOpen] = React.useState(false);
25 |
26 | const createTransaction = () => {
27 | if (Number.isNaN(Number(formData.amount)) || !formData.date.includes('-')) return;
28 |
29 | if (incomeCategories.map((iC) => iC.type).includes(formData.category)) {
30 | setFormData({ ...formData, type: 'Income' });
31 | } else if (expenseCategories.map((iC) => iC.type).includes(formData.category)) {
32 | setFormData({ ...formData, type: 'Expense' });
33 | }
34 |
35 | setOpen(true);
36 | addTransaction({ ...formData, amount: Number(formData.amount), id: uuidv4() });
37 | setFormData(initialState);
38 | };
39 |
40 | useEffect(() => {
41 | if (segment) {
42 | if (segment.intent.intent === 'add_expense') {
43 | setFormData({ ...formData, type: 'Expense' });
44 | } else if (segment.intent.intent === 'add_income') {
45 | setFormData({ ...formData, type: 'Income' });
46 | } else if (segment.isFinal && segment.intent.intent === 'create_transaction') {
47 | return createTransaction();
48 | } else if (segment.isFinal && segment.intent.intent === 'cancel_transaction') {
49 | return setFormData(initialState);
50 | }
51 |
52 | segment.entities.forEach((s) => {
53 | const category = `${s.value.charAt(0)}${s.value.slice(1).toLowerCase()}`;
54 |
55 | switch (s.type) {
56 | case 'amount':
57 | setFormData({ ...formData, amount: s.value });
58 | break;
59 | case 'category':
60 | if (incomeCategories.map((iC) => iC.type).includes(category)) {
61 | setFormData({ ...formData, type: 'Income', category });
62 | } else if (expenseCategories.map((iC) => iC.type).includes(category)) {
63 | setFormData({ ...formData, type: 'Expense', category });
64 | }
65 | break;
66 | case 'date':
67 | setFormData({ ...formData, date: s.value });
68 | break;
69 | default:
70 | break;
71 | }
72 | });
73 |
74 | if (segment.isFinal && formData.amount && formData.category && formData.type && formData.date) {
75 | createTransaction();
76 | }
77 | }
78 | }, [segment]);
79 |
80 | const selectedCategories = formData.type === 'Income' ? incomeCategories : expenseCategories;
81 |
82 | return (
83 |
84 |
85 |
86 |
87 | {segment ? (
88 |
89 | {segment.words.map((w) => w.value).join(" ")}
90 |
91 | ) : null}
92 | {/* {isSpeaking ? : 'Start adding transactions'} */}
93 |
94 |
95 |
96 |
97 | Type
98 |
102 |
103 |
104 |
105 |
106 | Category
107 |
110 |
111 |
112 |
113 |
114 | setFormData({ ...formData, amount: e.target.value })} fullWidth />
115 |
116 |
117 | setFormData({ ...formData, date: formatDate(e.target.value) })} />
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default NewTransactionForm;
125 |
--------------------------------------------------------------------------------
/src/components/Main/Form/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles(() => ({
4 | radioGroup: {
5 | display: 'flex',
6 | justifyContent: 'center',
7 | marginBottom: '-10px',
8 | },
9 | button: {
10 | marginTop: '20px',
11 | },
12 | }));
13 |
--------------------------------------------------------------------------------
/src/components/Main/List/List.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { List as MUIList, ListItem, ListItemAvatar, Avatar, ListItemText, ListItemSecondaryAction, IconButton, Slide } from '@material-ui/core';
3 | import { Delete, MoneyOff } from '@material-ui/icons';
4 |
5 | import { ExpenseTrackerContext } from '../../../context/context';
6 | import useStyles from './styles';
7 |
8 | const List = () => {
9 | const classes = useStyles();
10 | const { transactions, deleteTransaction } = useContext(ExpenseTrackerContext);
11 |
12 | return (
13 |
14 | {transactions.map((transaction) => (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | deleteTransaction(transaction.id)}>
25 |
26 |
27 |
28 |
29 |
30 | ))}
31 |
32 | );
33 | };
34 |
35 | export default List;
36 |
--------------------------------------------------------------------------------
/src/components/Main/List/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 | import { red, green } from '@material-ui/core/colors';
3 |
4 | export default makeStyles((theme) => ({
5 | avatarIncome: {
6 | color: '#fff',
7 | backgroundColor: green[500],
8 | },
9 | avatarExpense: {
10 | color: theme.palette.getContrastText(red[500]),
11 | backgroundColor: red[500],
12 | },
13 | list: {
14 | maxHeight: '150px',
15 | overflow: 'auto',
16 | },
17 | }));
18 |
--------------------------------------------------------------------------------
/src/components/Main/Main.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from 'react';
2 | import { Card, CardHeader, CardContent, Typography, Grid, Divider } from '@material-ui/core';
3 | import { useSpeechContext } from '@speechly/react-client';
4 | import { ExpenseTrackerContext } from '../../context/context';
5 | import useStyles from './styles';
6 | import Form from './Form/Form';
7 | import List from './List/List';
8 | import InfoCard from '../InfoCard';
9 |
10 | const ExpenseTracker = () => {
11 | const classes = useStyles();
12 | const { balance } = useContext(ExpenseTrackerContext);
13 |
14 | return (
15 |
16 |
17 |
18 | Total Balance ${balance}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default ExpenseTracker;
37 |
--------------------------------------------------------------------------------
/src/components/Main/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | media: {
5 | height: 0,
6 | paddingTop: '56.25%', // 16:9
7 | },
8 | expand: {
9 | transform: 'rotate(0deg)',
10 | marginLeft: 'auto',
11 | transition: theme.transitions.create('transform', {
12 | duration: theme.transitions.duration.shortest,
13 | }),
14 | },
15 | expandOpen: {
16 | transform: 'rotate(180deg)',
17 | },
18 | cartContent: {
19 | paddingTop: 0,
20 | },
21 | divider: {
22 | margin: '20px 0',
23 | },
24 | }));
25 |
--------------------------------------------------------------------------------
/src/components/Snackbar/Snackbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Snackbar from '@material-ui/core/Snackbar';
3 | import MuiAlert from '@material-ui/lab/Alert';
4 |
5 | import useStyles from './styles';
6 |
7 | const CustomizedSnackbar = ({ open, setOpen }) => {
8 | const classes = useStyles();
9 |
10 | const handleClose = (event, reason) => {
11 | if (reason === 'clickaway') {
12 | return;
13 | }
14 |
15 | setOpen(false);
16 | };
17 |
18 | return (
19 |
20 |
23 | Transaction successfully created.
24 |
25 |
26 | );
27 | };
28 |
29 | export default CustomizedSnackbar;
30 |
--------------------------------------------------------------------------------
/src/components/Snackbar/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | root: {
5 | width: '100%',
6 | '& > * + *': {
7 | marginTop: theme.spacing(2),
8 | },
9 | },
10 | }));
11 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Details } from './Details/Details';
2 | export { default as Main } from './Main/Main';
3 | export { default as Snackbar } from './Snackbar/Snackbar';
4 | export { default as InfoCard } from './InfoCard';
5 |
--------------------------------------------------------------------------------
/src/constants/categories.js:
--------------------------------------------------------------------------------
1 | const incomeColors = ['#123123', '#154731', '#165f40', '#16784f', '#14915f', '#10ac6e', '#0bc77e', '#04e38d', '#00ff9d'];
2 | const expenseColors = ['#b50d12', '#bf2f1f', '#c9452c', '#d3583a', '#dc6a48', '#e57c58', '#ee8d68', '#f79d79', '#ffae8a', '#cc474b', '#f55b5f'];
3 |
4 | export const incomeCategories = [
5 | { type: 'Business', amount: 0, color: incomeColors[0] },
6 | { type: 'Investments', amount: 0, color: incomeColors[1] },
7 | { type: 'Extra income', amount: 0, color: incomeColors[2] },
8 | { type: 'Deposits', amount: 0, color: incomeColors[3] },
9 | { type: 'Lottery', amount: 0, color: incomeColors[4] },
10 | { type: 'Gifts', amount: 0, color: incomeColors[5] },
11 | { type: 'Salary', amount: 0, color: incomeColors[6] },
12 | { type: 'Savings', amount: 0, color: incomeColors[7] },
13 | { type: 'Rental income', amount: 0, color: incomeColors[8] },
14 | ];
15 |
16 | export const expenseCategories = [
17 | { type: 'Bills', amount: 0, color: expenseColors[0] },
18 | { type: 'Car', amount: 0, color: expenseColors[1] },
19 | { type: 'Clothes', amount: 0, color: expenseColors[2] },
20 | { type: 'Travel', amount: 0, color: expenseColors[3] },
21 | { type: 'Food', amount: 0, color: expenseColors[4] },
22 | { type: 'Shopping', amount: 0, color: expenseColors[5] },
23 | { type: 'House', amount: 0, color: expenseColors[6] },
24 | { type: 'Entertainment', amount: 0, color: expenseColors[7] },
25 | { type: 'Phone', amount: 0, color: expenseColors[8] },
26 | { type: 'Pets', amount: 0, color: expenseColors[9] },
27 | { type: 'Other', amount: 0, color: expenseColors[10] },
28 | ];
29 |
30 | export const resetCategories = () => {
31 | incomeCategories.forEach((c) => c.amount = 0);
32 | expenseCategories.forEach((c) => c.amount = 0);
33 | };
34 |
--------------------------------------------------------------------------------
/src/context/context.js:
--------------------------------------------------------------------------------
1 | import React, { useReducer, createContext } from 'react';
2 |
3 | import contextReducer from './contextReducer';
4 |
5 | const initialState = JSON.parse(localStorage.getItem('transactions')) || [{ amount: 500, category: 'Salary', type: 'Income', date: '2020-11-16', id: '44c68123-5b86-4cc8-b915-bb9e16cebe6a' }, { amount: 225, category: 'Investments', type: 'Income', date: '2020-11-16', id: '33b295b8-a8cb-49f0-8f0d-bb268686de1a' }, { amount: 50, category: 'Salary', type: 'Income', date: '2020-11-13', id: '270304a8-b11d-4e16-9341-33df641ede64' }, { amount: 123, category: 'Car', type: 'Expense', date: '2020-11-16', id: '0f72e66e-e144-4a72-bbc1-c3c92018635e' }, { amount: 50, category: 'Pets', type: 'Expense', date: '2020-11-13', id: 'c5647dde-d857-463d-8b4e-1c866cc5f83e' }, { amount: 500, category: 'Travel', type: 'Expense', date: '2020-11-13', id: '365a4ebd-9892-4471-ad55-36077e4121a9' }, { amount: 50, category: 'Investments', type: 'Income', date: '2020-11-23', id: '80cf7e33-fc3e-4f9f-a2aa-ecf140711460' }, { amount: 500, category: 'Savings', type: 'Income', date: '2020-11-23', id: 'ef090181-21d1-4568-85c4-5646232085b2' }, { amount: 5, category: 'Savings', type: 'Income', date: '2020-11-23', id: '037a35a3-40ec-4212-abe0-cc485a98aeee' }];
6 |
7 | export const ExpenseTrackerContext = createContext(initialState);
8 |
9 | export const Provider = ({ children }) => {
10 | const [transactions, dispatch] = useReducer(contextReducer, initialState);
11 |
12 | const deleteTransaction = (id) => {
13 | dispatch({ type: 'DELETE_TRANSACTION', payload: id });
14 | };
15 |
16 | const addTransaction = (transaction) => {
17 | dispatch({ type: 'ADD_TRANSACTION', payload: transaction });
18 | };
19 |
20 | const balance = transactions.reduce((acc, currVal) => (currVal.type === 'Expense' ? acc - currVal.amount : acc + currVal.amount), 0);
21 |
22 | return (
23 |
30 | {children}
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/context/contextReducer.js:
--------------------------------------------------------------------------------
1 | const contextReducer = (state, action) => {
2 | let transactions;
3 |
4 | switch (action.type) {
5 | case 'DELETE_TRANSACTION':
6 | transactions = state.filter((transaction) => transaction.id !== action.payload);
7 |
8 | localStorage.setItem('transactions', JSON.stringify(transactions));
9 |
10 | return transactions;
11 | case 'ADD_TRANSACTION':
12 | transactions = [action.payload, ...state];
13 |
14 | localStorage.setItem('transactions', JSON.stringify(transactions));
15 |
16 | return transactions;
17 | default:
18 | return state;
19 | }
20 | };
21 |
22 | export default contextReducer;
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | #root, body, html {
2 | height: 100%;
3 | margin: 0;
4 | }
5 |
6 | body {
7 | background: linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(./assets/money.png);
8 | background-size: cover;
9 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { SpeechProvider } from '@speechly/react-client';
4 |
5 | import { Provider } from './context/context';
6 | import App from './App';
7 | import './index.css';
8 |
9 | ReactDOM.render(
10 |
11 |
12 |
13 |
14 | ,
15 | document.getElementById('root'),
16 | );
17 |
--------------------------------------------------------------------------------
/src/styles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | desktop: {
5 | [theme.breakpoints.up('sm')]: {
6 | display: 'none',
7 | },
8 | },
9 | mobile: {
10 | [theme.breakpoints.down('sm')]: {
11 | display: 'none',
12 | },
13 | },
14 | main: {
15 | [theme.breakpoints.up('sm')]: {
16 | paddingBottom: '5%',
17 | },
18 | },
19 | last: {
20 | [theme.breakpoints.down('sm')]: {
21 | marginBottom: theme.spacing(3),
22 | paddingBottom: '200px',
23 | },
24 | },
25 | grid: {
26 | '& > *': {
27 | margin: theme.spacing(2),
28 | },
29 | },
30 | }));
31 |
--------------------------------------------------------------------------------
/src/useTransactions.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { ExpenseTrackerContext } from './context/context';
3 |
4 | import { incomeCategories, expenseCategories, resetCategories } from './constants/categories';
5 |
6 | const useTransactions = (title) => {
7 | resetCategories();
8 | const { transactions } = useContext(ExpenseTrackerContext);
9 | const rightTransactions = transactions.filter((t) => t.type === title);
10 | const total = rightTransactions.reduce((acc, currVal) => acc += currVal.amount, 0);
11 | const categories = title === 'Income' ? incomeCategories : expenseCategories;
12 |
13 | rightTransactions.forEach((t) => {
14 | const category = categories.find((c) => c.type === t.category);
15 |
16 | if (category) category.amount += t.amount;
17 | });
18 |
19 | const filteredCategories = categories.filter((sc) => sc.amount > 0);
20 |
21 | const chartData = {
22 | datasets: [{
23 | data: filteredCategories.map((c) => c.amount),
24 | backgroundColor: filteredCategories.map((c) => c.color),
25 | }],
26 | labels: filteredCategories.map((c) => c.type),
27 | };
28 |
29 | return { filteredCategories, total, chartData };
30 | };
31 |
32 | export default useTransactions;
33 |
--------------------------------------------------------------------------------
/src/utils/formatDate.js:
--------------------------------------------------------------------------------
1 | export default (date) => {
2 | const d = new Date(date);
3 | let month = `${d.getMonth() + 1}`;
4 | let day = `${d.getDate()}`;
5 | const year = d.getFullYear();
6 |
7 | if (month.length < 2) { month = `0${month}`; }
8 | if (day.length < 2) { day = `0${day}`; }
9 |
10 | return [year, month, day].join('-');
11 | };
12 |
--------------------------------------------------------------------------------