├── .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 | ![Expense Tracker](https://i.ibb.co/VJjj3Kp/Screenshot-2020-12-18-205600.png) 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 | --------------------------------------------------------------------------------