Hello Devs, recently I have learned ReactJS and I am creating apps to learn more. I've create an app "Expense Monitor" which can store expense and income entries in json file and show them as list. It's a beginners project and hope help it will others who are learning ReactJS. Full source code is available at following url.
https://bitbucket.org/deepaksinghkushwah/expense-monitor/src/master/
I have used following packages...
axios, bootstrap,concurrently, json-server, moment, react-bootstrap, react-icons, react-modal, react-moment, react-toastify
Lets start this with updateing App.js file....
App.js
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import "./App.css";
import "bootstrap/dist/css/bootstrap.min.css";
import EntryForm from "./components/EntryForm";
import { EntryProider } from "./context/entries/EntryContext";
import EntryList from "./components/EntryList";
import Modal from "react-modal";
import { useState } from "react";
function App() {
// set app element to root
Modal.setAppElement("#root");
// set state for open and close modal
const [modelIsOpen, setModelIsOpen] = useState(false);
// open modal function
const openModal = () => {
setModelIsOpen(true);
};
// close modal function
const closeModal = () => {
setModelIsOpen(false);
};
return (
<div className="App">
{/** Entry provider for entry context */}
<EntryProider>
{/** Modal config start */}
<Modal
isOpen={modelIsOpen}
onRequestClose={closeModal}
className="customModal mt-5 p-2"
>
<button
onClick={closeModal}
className="btn btn-sm btn-danger float-end"
>
close
</button>
<EntryForm />
</Modal>
<h1 className="mt-3 ms-3">
Expense Monitor
<span className="float-end me-3">
<button
type="button"
className="btn btn-primary btn-sm"
onClick={openModal}
>
Add Entry
</button>
</span>
</h1>
{/** Expense entries module */}
<EntryList />
{/** Toast container to show toast notifications */}
<ToastContainer />
</EntryProider>
</div>
);
}
export default App;
Now update the App.css
.App{
width: 800px;
margin: auto;
padding: auto;
}
.customModal{
top: 50%;
left: 50%;
margin: auto;
width: 400px;
height: 350px;
background-color: burlywood;
text-align: center;
}
Next create folder name "components" and create following files with code.
compomnents/EntryForm.jsx
import React, { useContext, useState } from 'react'
import { toast } from 'react-toastify'
import { addEntry, getEntries } from '../context/entries/EntryAction';
import EntryContext from '../context/entries/EntryContext';
function EntryForm() {
/** set states for form */
const [title, setTitle] = useState("");
const [amount, setAmount] = useState(0);
const [item_type, setItemType] = useState("income");
/** using entry context dispatch */
const { dispatch } = useContext(EntryContext);
/** handle form submit function */
const handleSubmit = async(e) => {
e.preventDefault();
dispatch({ type: 'SET_LOADING' });
if (title === "" || amount === "") {
toast.error("You must provide the title and amount");
return false;
}
await addEntry(title, amount, item_type);
toast.success("Entry added");
setTitle("");
setAmount("");
setItemType("income");
const allEntries = await getEntries();
dispatch({ type: 'GET_ENTRIES', payload: allEntries });
}
return (
<form onSubmit={handleSubmit}>
<table className="table table-bordered">
<tbody>
<tr>
<td><input type="text" className='form-control' id="title" name="title" value={title} placeholder="Title" onChange={(e) => setTitle(e.target.value)} /></td>
</tr>
<tr>
<td><input type="number" step=".1" min="0" className='form-control' id="amount" name="amount" value={amount} placeholder="Amount" onChange={(e) => setAmount(e.target.value)} /></td>
</tr>
<tr>
<td>
<select name="item_type" className='form-control' value={item_type} id="item_type" onChange={(e) => setItemType(e.target.value)}>
<option value="income">Income</option>
<option value="expense">Expense</option>
</select>
</td>
</tr>
</tbody>
</table>
<button className='btn btn-primary' type="submit">Send</button>
</form>
)
}
export default EntryForm
components/EntryList.jsx
import React, { useEffect } from 'react'
import { useContext } from 'react';
import { getEntries, removeEntry } from '../context/entries/EntryAction';
import EntryContext from "../context/entries/EntryContext";
import { FaTrash } from 'react-icons/fa';
import { toast } from 'react-toastify';
import moment from "moment";
function EntryList() {
/** use entry context to get fields */
const { entries, dispatch, totalIncome, totalExpense, loading } = useContext(EntryContext);
useEffect(() => {
dispatch({ type: 'SET_LOADING' });
const fetchEntries = async () => {
const r = await getEntries();
dispatch({ type: 'GET_ENTRIES', payload: r });
}
fetchEntries();
}, [dispatch]);
/** handle delete event */
const handleDelete = async (id) => {
if (window.confirm("Are you sure want to remove this entry?")) {
dispatch({ type: 'SET_LOADING' });
await removeEntry(id);
toast.success("Item deleted");
const r = await getEntries();
dispatch({ type: 'GET_ENTRIES', payload: r });
}
}
if (loading) {
return "Loading...";
}
return (
<>
{/** return entries if entries have rows */}
{entries && entries.length > 0 ? (
<table className='table table-hover table-small'>
<thead>
<tr>
<th>Item</th>
<th>Amount</th>
<th>Date</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{entries.map((item) => (
<tr key={item.id} className={item.item_type === 'expense' ? "table-danger" : "table-primary"} title={item.item_type}>
<td>
{item.title}
</td>
<td>
${item.amount}
</td>
<td>{moment(item.date).format("MMMM Do YYYY, h:mm:ss a")}</td>
<td>
<span className='float-end pe-3' onClick={() => handleDelete(item.id)}><FaTrash /></span>
</td>
</tr>
))}
</tbody>
<tfoot className='table-secondary'>
<tr>
<th>Total Income</th>
<th>${totalIncome}</th>
<th></th>
<th></th>
</tr>
<tr>
<th>Total Expense</th>
<th>${totalExpense}</th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
) : 'No entries found'}
</>
)
}
export default EntryList
Now we move to context. Create context folder in src folder and write these files...
context/entries/EntryAction.js
import axios from "axios"
import moment from "moment";
const http = axios.create({
baseURL: 'http://localhost:5000'
});
export const addEntry = async(title, amount, item_type) => {
const date = moment().format('LLLL');
const params = new URLSearchParams({title, amount, item_type, date });
const response = await http.post('/entries',params);
const data = await response.data;
return data;
}
export const getEntries = async() => {
const r = await http.get('/entries');
const data = await r.data;
return data;
}
export const removeEntry = async(id) => {
const response = await http.delete(`/entries/${id}`)
const data = await response.data;
return data;
}
EntryContext.js
import { createContext, useReducer } from "react";
import EntryReducter from "./EntryReducer";
const EntryContext = createContext();
export const EntryProider = ({children}) => {
const initalState = {
entries: [],
totalExpense: 0,
totalIncome: 0,
loading: true,
}
const [state, dispatch] = useReducer(EntryReducter, initalState);
return <EntryContext.Provider value={{
...state,
dispatch
}}>
{children}
</EntryContext.Provider>
}
export default EntryContext;
EntryReducer.js
const EntryReducter = (state, action) => {
let expense = 0;
let income = 0;
switch(action.type){
case 'GET_ENTRIES':
expense = setTotal('expense', action.payload);
income = setTotal('income', action.payload)
return {
entries: action.payload,
loading: false,
totalExpense: expense,
totalIncome: income
}
case 'SET_LOADING':
return {
loading: true
}
default:
return state;
}
}
function setTotal(type, entries){
let total = 0.00;
console.log(entries);
entries.map((item) => {
if(item.item_type === type){
total += parseFloat(item.amount);
}
})
return total;
}
export default EntryReducter;
Top comments (0)