How cool would that be if we could just talk about our expenses and our app would calculate and show graphical representation of all our income and expenses with history of our previous records as well.
To add cherry on top let's make our very own Expense Tracker using context API ,Speechly and local Storage.
Let's start with the setup first go to terminal of your VScode and write
npx create-react-app my-app
cd my-app
npm start
One more thing to add is our react version is 17 and we would need to degrade it because our speechly doesn't support the new version of react. so for downgrading write in your terminal.
npm i react@16.12.0 react-dom@16.12.0
Now, let's just install all the other packages that we would be needing for our build.
for design purpose
npm i @material-ui/core @material-ui/icons @material-ui/lab
for graphical representation and unique id
npm i chart.js react-chartjs-2 uuid
Now , for the special voice feature
npm i @speechly/react-client @speechly/react-ui
Let's start with setting up the background and basic structure of our app where we would have one component for expense and income and one as main components where we would be adding a form and voice feature to add our expense.
Make Details.js file and for structure have a look at the code below.
import React from 'react'
import {Card , CardHeader , CardContent , Typography } from '@material-ui/core';
// import { Doughnut } from 'react-chartjs-2';
import useStyles from './styles';
const Details = ({title}) => {
// We use useStyle hook for importing material-ui class
const classes =useStyles();
return (
// In th below statement for our income use income css class else expense
<Card className={title === 'Income' ? classes.income : classes.expense}>
<CardHeader title={title}/>
<CardContent>
<Typography varinat="h5"> $50</Typography>
{/* We will be adding data later */}
{/* <Doughnut data='DATA' /> */}
</CardContent>
</Card>
)
}
export default Details
Now Styles file
import { makeStyles } from '@material-ui/core/styles';
export default makeStyles(() => ({
income: {
borderBottom: '10px solid rgba(0, 255, 0, 0.5)',
},
expense: {
borderBottom: '10px solid rgba(255, 0, 0, 0.5)',
},
}));
import React from 'react'
import {Card , CardHeader , CardContent , Typography , Grid , Divider, FormControl, InputLabel, Select, MenuItem} from '@material-ui/core';
import useStyles from './styles'
import Form from './Form/Form';
const Main = () => {
// We use useStyle hook for importing material-ui class
const classes = useStyles();
return (
<Card className={classes.root}>
<CardHeader title ="Expense Tracker" subheader="Talk to Add your expenses"/>
<CardContent>
<Typography align="center" variant="h5"> Total Balance $100</Typography>
<Typography variant="subtitle1" style={{lineHeight: '1.5em', marginTop: '20px'}}>
</Typography>
<Divider/>
{/* Below is our Form component */}
<Form/>
</CardContent>
</Card>
)
}
export default Main
Now styles file for main component
import { makeStyles } from '@material-ui/core/styles';
export default makeStyles((theme) => ({
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
expand: {
transform: 'rotate(0deg)',
marginLeft: 'auto',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest,
}),
},
expandOpen: {
transform: 'rotate(180deg)',
},
cartContent: {
paddingTop: 0,
},
divider: {
margin: '20px 0',
},
}));
Now Form Component
import React from 'react'
import {TextField, Typography, Grid, Button, FormControl, InputLabel, Select, MenuItem} from '@material-ui/core';
import useStyles from './styles';
const Form = () => {
const classes = useStyles();
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography align='center' variant='subtitle1' gutterBottom>
...
</Typography>
</Grid>
{/* Another grid for Type */}
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>Type</InputLabel>
<Select>
<MenuItem value="Income">Income</MenuItem>
<MenuItem value="Expense">Expense</MenuItem>
</Select>
</FormControl>
</Grid>
{/* Another grid for conatiner */}
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel> Category</InputLabel>
<Select>
<MenuItem value="Income">I</MenuItem>
<MenuItem value="Expense">E</MenuItem>
</Select>
</FormControl>
</Grid>
{/*Amount */}
<Grid item xs={6}>
<TextField type="number" label ="Amount" fullWidth/>
</Grid>
{/* DATE */}
<Grid item xs={6}>
<TextField type="date" label ="Date" fullWidth/>
</Grid>
{/*BUTTON TO SUBMIT OUR FORM */}
<Button className={classes.button} variant="outlined" color="primary" fullWidth>
Create
</Button>
</Grid>
)
}
export default Form
Now style file for form
import { makeStyles } from '@material-ui/core/styles';
export default makeStyles(() => ({
radioGroup: {
display: 'flex',
justifyContent: 'center',
marginBottom: '-10px',
},
button: {
marginTop: '20px',
},
}));
Now App.js file where we will call and arrange our all components to be rendered on the website.
import React from 'react'
import {Grid} from '@material-ui/core';
import Details from './components/Details/Details';
import useStyles from './styles';
import Main from './components/Main/Main';
const App = () => {
const classes =useStyles();
return (
<div>
<Grid className= {classes.grid}
container spacing={0}
alignItems= "center"
justify="center"
style={{height: '100vh'}}>
<Grid item xs={12} sm={3}>
<Details title ="Income"/>
</Grid>
{/* Main component */}
<Grid item xs={12} sm={3}>
<Main/>
</Grid>
{/* Expense component */}
<Grid item xs={12} sm={3}>
<Details title="Expense"/>
</Grid>
</Grid>
</div>
)
}
export default App
Now styling file for App file
#root, body , html {
height: 100%;;
margin: 0;
}
body{
background: url(./Images/bg.jpg);
background-size: cover;
}
Now , Our structure is ready and after these lines of codes our app will look something like this.
Now, let's talk about one of the important things which we will be using for our app is Context. Here, you can relate context to
the reducer where we had a store and all the components used to take data from. It is similar to that but simpler and with less boilerplate code. Here, we have two main things to focus on right now is adding transactions and deleting one. Create a folder name context and in that files named context.js and contextReducer.js and have a look at the below code I have added explanation there.
context.js
import React, {useReducer , createContext} from 'react';
import contextReducer from './contextReducer';
const intitalState = [];
export const ExpenseTrackerContext = createContext(intitalState);
export const Provider = ({children}) => {
const [transactions, dispatch] = useReducer(contextReducer, intitalState);
// Action Creators
//which one we want to delete can be known by the id only
const deleteTransactions = (id) => {
dispatch({type: 'DELETE_TRANSACTION', payload: id});
};
// In here while creating we dont know the id so we need the whole transaction
// dispatching means changing the state of the transition
const addTransactions = (transaction) => {
dispatch({type: 'ADD_TRANSACTION', payload: transaction});
};
return (
// This line below means we are sending the delete and add transaction method to be used by the whole app
<ExpenseTrackerContext.Provider value={{
deleteTransactions,
addTransactions,
transactions,
}}>
{children}
</ExpenseTrackerContext.Provider>
);
};
contextReducer.js
//logic after doing that action like how changing old state to new
const contextReducer = (state, action) => {
let transactions;
switch(action.type){
case 'DELETE_TRANSACTION':
transactions = state.filter((t) => t.id !== action.payload);
return transactions;
case 'ADD_TRANSACTION':
transactions = [action.payload, ...state];
return transactions;
default:
return state;
}
}
export default contextReducer;
Now, calling it in our List.jsx
import React, {useContext} from 'react';
import { List as MUIList, ListItem, ListItemAvatar, Avatar, ListItemText, ListItemSecondaryAction, IconButton, Slide } from '@material-ui/core';
import { Delete, MoneyOff } from '@material-ui/icons';
import {ExpenseTrackerContext} from '../../../context/context';
import useStyles from './styles';
const List = () => {
const classes =useStyles();
const {transactions , deleteTransactions} = useContext(ExpenseTrackerContext);
return (
<MUIList dense={false} className={classes.list}>
{
transactions.map((transaction) => (
<Slide direction="down" in mountOnEnter unmountOnExit key={transaction.id}>
<ListItem>
<ListItemAvatar>
<Avatar className={transaction.type === 'Income' ?
classes.avatarIncome : classes.avatarExpense
}>
<MoneyOff/>
</Avatar>
</ListItemAvatar>
<ListItemText primary={transaction.category}
secondary ={`$${transaction.amount} - ${transaction.date}`}></ListItemText>
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete"
onClick={() => deleteTransactions(transaction.id)}>
<Delete/>
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Slide>
))
}
</MUIList>
)
}
export default List
Now we would also want to have a dynamic categories list for expenses and income and for that we would create constants and simply in there, we will be creating objects of type, amount, and color. Look at the picture below for a better understanding.
const incomeColors = ['#123123', '#154731', '#165f40', '#16784f', '#14915f', '#10ac6e', '#0bc77e', '#04e38d', '#00ff9d'];
const expenseColors = ['#b50d12', '#bf2f1f', '#c9452c', '#d3583a', '#dc6a48', '#e57c58', '#ee8d68', '#f79d79', '#ffae8a', '#cc474b', '#f55b5f'];
export const incomeCategories = [
{ type: 'Business', amount: 0, color: incomeColors[0] },
{ type: 'Investments', amount: 0, color: incomeColors[1] },
{ type: 'Extra income', amount: 0, color: incomeColors[2] },
{ type: 'Deposits', amount: 0, color: incomeColors[3] },
{ type: 'Lottery', amount: 0, color: incomeColors[4] },
{ type: 'Gifts', amount: 0, color: incomeColors[5] },
{ type: 'Salary', amount: 0, color: incomeColors[6] },
{ type: 'Savings', amount: 0, color: incomeColors[7] },
{ type: 'Rental income', amount: 0, color: incomeColors[8] },
];
export const expenseCategories = [
{ type: 'Bills', amount: 0, color: expenseColors[0] },
{ type: 'Car', amount: 0, color: expenseColors[1] },
{ type: 'Clothes', amount: 0, color: expenseColors[2] },
{ type: 'Travel', amount: 0, color: expenseColors[3] },
{ type: 'Food', amount: 0, color: expenseColors[4] },
{ type: 'Shopping', amount: 0, color: expenseColors[5] },
{ type: 'House', amount: 0, color: expenseColors[6] },
{ type: 'Entertainment', amount: 0, color: expenseColors[7] },
{ type: 'Phone', amount: 0, color: expenseColors[8] },
{ type: 'Pets', amount: 0, color: expenseColors[9] },
{ type: 'Other', amount: 0, color: expenseColors[10] },
];
// Reset function to reset all the categories if you want to clean them
export const resetCategories = () => {
incomeCategories.forEach((c) => c.amount = 0);
expenseCategories.forEach((c) => c.amount = 0);
};
Now, let's call everything in our Form.jsx
Form.jsx
import React, {useContext, useState} from 'react';
import {TextField, Typography, Grid, Button, FormControl, InputLabel, Select, MenuItem} from '@material-ui/core';
import useStyles from './styles';
import {ExpenseTrackerContext} from '../../../context/context';
import {v4 as uuidv4} from 'uuid';
import {incomeCategories , expenseCategories} from '../../../constants/categories';
import formatDate from '../../../Utils/formatDate';
const initialState = {
amount: '',
category: '',
type: 'Income',
date: formatDate(new Date()),
};
const Form = () => {
const classes = useStyles();
const {addTransactions} = useContext(ExpenseTrackerContext);
const [formData , setFormData] =useState(initialState);
const createTransaction = () => {
// For id we will be using uuid
const transaction = {...formData, amount: Number(formData.amount), id: uuidv4()}
addTransactions(transaction);
setFormData(initialState);
}
const selectedCategories = formData.type === 'Income' ? incomeCategories : expenseCategories;
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography align='center' variant='subtitle1' gutterBottom>
...
</Typography>
</Grid>
{/* Another grid for Type */}
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>Type</InputLabel>
<Select value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}>
<MenuItem value="Income">Income</MenuItem>
<MenuItem value="Expense">Expense</MenuItem>
</Select>
</FormControl>
</Grid>
{/* Another grid for conatiner */}
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>Category</InputLabel>
<Select value={formData.category}
onChange={ (e) => setFormData({...formData, category: e.target.value})}>
{
selectedCategories.map((c) =>
<MenuItem key ={c.type} value={c.type}>
{c.type}
</MenuItem>
)
}
</Select>
</FormControl>
</Grid>
{/*Amount */}
<Grid item xs={6}>
<TextField type="number" label ="Amount" fullWidth value={formData.amount}
onChange={(e) => {setFormData({...formData , amount: e.target.value})}}/>
</Grid>
{/* DATE */}
<Grid item xs={6}>
<TextField type="date" label ="Date" fullWidth value={formData.date}
onChange={(e)=> {setFormData({...formData, date: formatDate(e.target.value)})}}/>
</Grid>
{/*BUTTON TO SUBMIT OUR FORM */}
<Button className={classes.button} variant="outlined" color="primary" fullWidth
onClick={createTransaction}>
Create
</Button>
</Grid>
)
}
export default Form;
For the change in date Format have a look at the below code or you can also use moment library.
util.js
export default (date) => {
const d = new Date(date);
let month = `${d.getMonth() + 1}`;
let day = `${d.getDate()}`;
const year = d.getFullYear();
if (month.length < 2) { month = `0${month}`; }
if (day.length < 2) { day = `0${day}`; }
return [year, month, day].join('-');
};
Next part is to add charts the graphical representation of our income and expenses. For that we would be using custom hook(A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks).
UseTransaction.js
import { useContext } from 'react';
import { ExpenseTrackerContext } from './context/context';
import { incomeCategories, expenseCategories, resetCategories } from './constants/categories';
const useTransactions = (title) => {
resetCategories();
const { transactions } = useContext(ExpenseTrackerContext);
const rightTransactions = transactions.filter((t) => t.type === title);
const total = rightTransactions.reduce((acc, currVal) => acc += currVal.amount, 0);
const categories = title === 'Income' ? incomeCategories : expenseCategories;
rightTransactions.forEach((t) => {
const category = categories.find((c) => c.type === t.category);
if (category) category.amount += t.amount;
});
const filteredCategories = categories.filter((sc) => sc.amount > 0);
const chartData = {
datasets: [{
data: filteredCategories.map((c) => c.amount),
backgroundColor: filteredCategories.map((c) => c.color),
}],
labels: filteredCategories.map((c) => c.type),
};
return { filteredCategories, total, chartData };
};
export default useTransactions;
Let's simply call this hook in our details file and one more thing to notice we do need to downgrade out chart.js package fr doing that put these commands in the terminal
npm i chart.js@2.9.4 react-chartjs-2@2.11.1
Now , calling our hook in details.jsx
import React from 'react'
import {Card , CardHeader , CardContent , Typography } from '@material-ui/core';
import { Doughnut } from 'react-chartjs-2';
import useStyles from './styles';
import useTransactions from '../../useTransactions';
const Details = ({title, subheader}) => {
// We use useStyle hook for importing material-ui class
const {total, chartData} =useTransactions(title);
const classes =useStyles();
return (
// In th below statement for our income use income css class else expense
<Card className={title === 'Income' ? classes.income : classes.expense}>
<CardHeader title={title} subheader ={subheader}/>
<CardContent>
<Typography varinat="h5">${total}</Typography>
{/* We will be adding data later */}
{/* console.log(chartData) */}
<Doughnut data={chartData} />
</CardContent>
</Card>
);
}
export default Details;
Now, our App looks something like this
I have also added voice feature with local storage in it as well if you all want me to write an article on that part then please write in comments.
All the code is available here on github
And the app link
Github (leave a start ⭐)
Track_Your_Expenses
Thank You!!
Top comments (3)
Can you help me for my issue?
I want to begin from zero on the y-axis but after zero directly want to show dataset value on the y-axis as per I have attached image.
dev-to-uploads.s3.amazonaws.com/up...
Did you try this?
Yes. I tried beginAtZero but it automatically creating stepsize. It doesn't giving a desired result. I have attached image link of using beginAtZero.
dev-to-uploads.s3.amazonaws.com/up...
if you know how can i achieve desired result please let me know.