Build an invoice management system using React & Firebase
David Asaolu
Posted on May 14, 2022
Hello there, welcome to this tutorial. In this article, you will learn how to use:
- Redux Toolkit
- Firebase
- React-router-dom v6 (latest version) and
- React-to-print library
by building an invoice management system that allows users to register their businesses, and craft printable invoices for their customers.
This is an excellent project to showcase to future employers, and there are quite a few things to learn, but never mind, it's going to be an engaging and educational read.
ANY PREREQUISITE? A basic understanding of React.js, including React Hooks, is required.
So grab a coffee, and let's go!
How to build a Quote sharing app using React.js, React-share and React-paginate
David Asaolu ・ Apr 6 '22
What is Firebase?
Firebase is a Backend-as-a-Service software (Baas) owned by Google that enables developers to build full-stack web applications in a few minutes. Services like Firebase make it very easy for front-end developers to build full-stack web applications with little or no backend programming skills.
Firebase provides various authentication methods, a NoSQL database, a real-time database, image storage, cloud functions, and hosting services. The NoSQL database is known as Firestore, and the image storage is known as Storage.
We will discuss how you can add Firebase authentication, its super-fast Firestore, and image storage to your web application.
How to add Firebase to Create-React-App
❇️ Visit the Firebase console and sign in with a Gmail account.
❇️ Create a Firebase project once you are signed in.
❇️ Create a Firebase app by clicking the </>
icon.
❇️ Provide the name of your app. You may choose to use Firebase hosting for your project.
❇️ Copy the config code and paste it somewhere for now. You'll be making use of it later.
Here is what the config code looks like:
// Import the functions you need from the SDKs you need
import { initializeApp } from 'firebase/app';
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: 'AIzaSyAnXkvMTXW9Mqq4wKgcq1IUDjd3mtemkmY',
authDomain: 'demo.firebaseapp.com',
projectId: 'demo',
storageBucket: 'demo.appspot.com',
messagingSenderId: '186441714475',
appId: '1:186441714475:web:1e29629ddd39101d83d36e',
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
Adding Firebase Email & Password Authentication
To make use of the Firebase Email and Password authentication.
❇️ Select Authentication on the sidebar of your screen.
❇️ Click the Get Started button and enable the Email & Password sign-in method.
Setting up Firestore
We will be adding Firestore, a super-fast data storage to our Firebase app.
❇️ Select Firestore Database from the sidebar menu.
❇️ Click the Get Started button and get started in test mode.
Next, let's set up Firebase Storage.
Setting up Firebase Storage for images
To set up Firebase Storage,
❇️ Select Storage from the sidebar menu.
❇️ Enable Firebase Storage by changing the rules from allow read, write: if false;
to allow read, write: if true
.
Congratulations! You've successfully set up the backend service needed for this project.
Project setup & Installations
Here, we will install all the necessary packages.
❇️ Install create-react-app, by running the code below.
npx create-react-app react-invoice
❇️ Cd into the react-invoice
directory and install Firebase:
npm i firebase
❇️ Connect the Firebase app created by creating a firebase.js
and copy the SDK config into the file.
//in firebase.js
import { initializeApp } from 'firebase/app';
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: 'AIzaSyAnXkvMTXW9Mqq4wKgcq1IUDjd3mtemkmY',
authDomain: 'demo.firebaseapp.com',
projectId: 'demo',
storageBucket: 'demo.appspot.com',
messagingSenderId: '186441714475',
appId: '1:186441714475:web:1e29629ddd39101d83d36e',
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
❇️ Import the necessary functions in the firebase.js
file
//in firebase.js
import { initializeApp } from 'firebase/app';
// -------> New imports <-----
import { getFirestore } from 'firebase/firestore'; //for access to Firestore
import { EmailAuthProvider } from 'firebase/auth'; //for email and password authentication
import { getAuth } from 'firebase/auth'; // for access to authentication
import { getStorage } from 'firebase/storage'; //for access to Firebase storage
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: 'AIzaSyAnXkvMTXW9Mqq4wKgcq1IUDjd3mtemkmY',
authDomain: 'demo.firebaseapp.com',
projectId: 'demo',
storageBucket: 'demo.appspot.com',
messagingSenderId: '186441714475',
appId: '1:186441714475:web:1e29629ddd39101d83d36e',
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// <----- Additional Changes ---->
const provider = new EmailAuthProvider();
const auth = getAuth(app);
const db = getFirestore(app);
const storage = getStorage(app);
export { provider, auth, storage };
export default db;
❇️ Install react-router-dom. React-router-dom allows you to navigate through various pages of the web application.
npm i react-router-dom
❇️ Install react-to-print library. React-to-print library enables us to print React components.
npm install react-to-print
❇️ Install Redux Toolkit and React-Redux. These libraries enable us to use the Redux state management library more efficiently.
npm install @reduxjs/toolkit react-redux
❇️ Optional: Install Tailwind CSS and its dependencies. You can use any UI library you prefer.
npm install -D tailwindcss postcss autoprefixer
❇️ Create a tailwind.config.js
and postcss.config.js
by running the code below:
npx tailwindcss init -p
❇️ Edit the tailwind.config.js
file
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'], //Changes made
theme: {
extend: {},
},
plugins: [],
};
❇️ Open src/index.css
and add the following to the file.
@tailwind base;
@tailwind components;
@tailwind utilities;
Congratulations! 🎈 We can now start coding the web application.
Creating the Authentication page with Firebase Auth
In this section, we will create an email and password sign-in and registration page using our Firebase app as the backend service.
❇️ Create a components folder and create Login.js and SignUp.js files.
❇️ Make the SignUp.js file the register page and Login.js the sign in page.
//In Login.js
import React, { useState } from 'react';
const Login / SignUp = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('Clicked');
};
return (
<main className="w-full flex items-center justify-center min-h-screen">
<form
className="w-full flex flex-col items-center justify-center mt-12"
onSubmit={handleSubmit}
>
<label htmlFor="email" className="mb-2 font-semibold">
Email Address
</label>
<input
id="email"
type="email"
className="w-2/3 mb-4 border p-3 rounded"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor="password" className="mb-2 font-semibold">
Password
</label>
<input
id="password"
type="password"
className="w-2/3 mb-3 border p-3 rounded"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="submit"
className="w-[200px] h-[45px] rounded bg-blue-400 text-white"
>
SIGN IN / REGISTER
</button>
</form>
</main>
);
};
export default Login/SignUp;
To enable users to sign in via Firebase, we will need the Firebase sign-in functions
❇️ Add Firebase login by changing the handleSubmit
function in the Login.js file.
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../firebase';
const handleSubmit = (e) => {
//Firebase function that allows users sign-in via Firebase
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
const user = userCredential.user;
console.log(user);
})
.catch((error) => {
console.error(error);
});
};
❇️ Add Firebase sign-up function into the SignUp.js file by copying the code below
import { createUserWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../firebase';
const handleSubmit = (e) => {
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
console.log(user);
// ...
})
.catch((error) => {
console.error(error);
// ..
});
};
- From the code snippet above, the
user
variable contains all the user's information, such as the user id, email id, and many more.
Adding Redux Toolkit for state management
Here, you will learn how to store users' information temporarily in a React application using Redux Toolkit. Redux Toolkit will enable us to allow only authenticated users to perform the specific tasks of the web application.
To add Redux Toolkit to a React application, do the following:
❇️ Create a Redux store in src/redux/store.js
. The store contains the state of the web application, and every component has access to it.
// In src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {},
});
❇️ Make the store available to the React application by copying the code below
//In index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { store } from './redux/store'; // The store
import { Provider } from 'react-redux'; // The store provider
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
❇️ Create the Redux state for the user in src/redux/user.js
// In src/redux/user.js
import { createSlice } from '@reduxjs/toolkit';
export const userSlice = createSlice({
name: 'user',
initialState: {
user: {},
},
reducers: {
setUser: (state, action) => {
state.user = action.payload;
},
},
});
// Action creators are generated for each case reducer function
export const { setUser } = userSlice.actions;
export default userSlice.reducer;
- From the code snippet above:
- I imported the
createSlice
function that allows us to create the state, actions, and reducers as a single object. - If you are not familiar with Redux Toolkit, read the documentation or watch this short video
- I imported the
You've successfully set up Redux Toolkit in your React application. Now, let's see how to save the user's details in the Redux state after signing in.
Saving users' details in the Redux State
❇️ Edit the Login.js
and SignUp.js
files by adding the useDispatch() hook from React-Redux.
//For example in SignUp.js
import { useDispatch } from 'react-redux';
import { setUser } from '../redux/user';
const SignUp = () => {
......
const dispatch = useDispatch();
const handleSubmit = (e) => {
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
dispatch(setUser({ id: user.uid, email: user.email })); //Substitute the console.log with this
// ...
})
.catch((error) => {
console.error(error);
// ..
});
}
return (
.......
......
)
};
export default SignUp;
- From the code snippet above:
-
useDispatch()
is a hook provided by React Redux which enables us to save the user's details in the store by accepting the reducer as a parameter. -
setUser
is the reducer that changes the state of the web application.
-
Congratulations! You've just set up Firebase Email and Password Authentication. Next, let's learn how to work with Firestore by creating the business registration page.
Creating the business registration page for first-time users
In this section, you will learn how to do the following:
- create the business registration page for first-time users
- work with Firebase Firestore
- create private routes that prevents unauthorized users from viewing pages in your web applications
First of all, let's create a business registration form for first-time users
After a user signs in, we check if the user has created a business profile, if not the user is redirected to the business profile creation page.
❇️ Create a simple form that accepts the business details from the user
import React, { useState } from 'react';
const BusinessProfile = () => {
const [businessName, setBusinessName] = useState('');
const [businessAddress, setBusinessAddress] = useState('');
const [accountName, setAccountName] = useState('');
const [accountNumber, setAccountNumber] = useState('');
const [bankName, setBankName] = useState('');
const [logo, setLogo] = useState(
'https://www.pesmcopt.com/admin-media/images/default-logo.png'
);
{
/* The handleFileReader function converts the business logo (image file) to base64 */
}
const handleFileReader = () => {};
{
/* The handleSubmit function sends the form details to Firestore */
}
const handleSubmit = () => {};
return (
<div className="w-full md:p-8 md:w-2/3 md:shadow mx-auto mt-8 rounded p-3 my-8">
<h3 className="text-center font-bold text-xl mb-6">
Setup Business Profile
</h3>
<form className="w-full mx-auto flex flex-col" onSubmit={handleSubmit}>
{/* The handleSubmit function sends the form details to Firestore */}
<input
type="text"
required
className="py-2 px-4 bg-gray-100 w-full mb-6 capitalize rounded"
id="businessName"
value={businessName}
placeholder="Business Name"
onChange={(e) => setBusinessName(e.target.value)}
/>
<input
type="text"
required
className="py-2 px-4 bg-gray-100 w-full mb-6 capitalize rounded"
id="businessAddress"
value={businessAddress}
placeholder="Business Address"
onChange={(e) => setBusinessAddress(e.target.value)}
/>
<input
type="text"
required
className="py-2 px-4 bg-gray-100 w-full mb-6 capitalize rounded"
id="accountName"
value={accountName}
placeholder="Account Name"
onChange={(e) => setAccountName(e.target.value)}
/>
<input
type="number"
required
className="py-2 px-4 bg-gray-100 w-full mb-6 rounded"
id="accountNumber"
value={accountNumber}
placeholder="Account Name"
onChange={(e) => setAccountNumber(e.target.value)}
/>
<input
type="text"
required
className="py-2 px-4 bg-gray-100 w-full mb-6 capitalize rounded"
id="bankName"
value={bankName}
onChange={(e) => setBankName(e.target.value)}
placeholder="Bank Name"
/>
<div className="flex items-center space-x-4 w-full">
<div className="flex flex-col w-1/2">
<img src={logo} alt="Logo" className=" w-full max-h-[300px]" />
</div>
<div className="flex flex-col w-full">
<label htmlFor="logo" className="text-sm mb-1">
Upload logo
</label>
<input
type="file"
accept="image/*"
required
className="w-full mb-6 rounded"
id="logo"
onChange={handleFileReader}
/>
</div>
</div>
<button className="bg-blue-800 text-gray-100 w-full p-5 rounded my-6">
COMPLETE PROFILE
</button>
</form>
</div>
);
};
export default BusinessProfile;
- From the code snippet above, I created a form layout that accepts the business information such as the name, address, logo, account number, account name, and bank name of the user. This information is going to be shown on the invoice issued by the business.
Once, that's completed let's work on the handleFileReader
and handleSubmit
functions
How to Upload Images to Firebase Storage
❇️ Edit the handleFileReader
function, by copying the code below:
const handleFileReader = (e) => {
const reader = new FileReader();
if (e.target.files[0]) {
reader.readAsDataURL(e.target.files[0]);
}
reader.onload = (readerEvent) => {
setLogo(readerEvent.target.result);
};
};
- The code snippet above is a JavaScript function that runs when a user uploads the logo and then converts the image to a base64 data URL.
❇️ Edit the handleSubmit
function to save the details to Firestore
import { useNavigate } from 'react-router-dom';
import { getDownloadURL, ref, uploadString } from '@firebase/storage';
import { storage } from '../firebase';
import {
addDoc,
collection,
doc,
updateDoc,
onSnapshot,
query,
where,
} from '@firebase/firestore';
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault(); //prevents the page from refreshing
const docRef = await addDoc(collection(db, 'businesses'), {
user_id: user.id,
businessName,
businessAddress,
accountName,
accountNumber,
bankName,
});
const imageRef = ref(storage, `businesses/${docRef.id}/image`);
if (logo !== 'https://www.pesmcopt.com/admin-media/images/default-logo.png') {
await uploadString(imageRef, logo, 'data_url').then(async () => {
//Gets the image URL
const downloadURL = await getDownloadURL(imageRef);
//Updates the docRef, by adding the logo URL to the document
await updateDoc(doc(db, 'businesses', docRef.id), {
logo: downloadURL,
});
//Alerts the user that the process was successful
alert("Congratulations, you've just created a business profile!");
});
navigate('/dashboard');
}
};
- From the code snippet above:
-
useNavigate
is a hook fromreact-router-dom
that allows us to move from one page to another.navigate("/dashboard")
takes the user to the dashboard page immediately after a business profile is created. -
addDoc
is a function provided by Firebase which allows us to create collections, and add a document containing the id of the collection, user id, business name, etc as stated in thedocRef
variable above in the Firestore. Collections contain documents, and each document contains data....(check modular firebase). -
docRef
is a reference to the newly created business profile -
imageRef
accepts two arguments, the Firebase storage related to the Firebase app and the URL you want the logo to have. Here, the URL isbusinesses/<the document id>/image
, this enables each logo URL to be unique and different from one another. - The if state checks, if the logo is not the same as the default value before the logo, is uploaded to the Firebase storage.
- Learn more about Firebase storage and performing CRUD operations.
-
So, how do we check if a user is a first-time user or not? Let's find out below.
How to check if a user has created a business profile
In this section, you will learn how to
- query data from the Firestore
- retrieve data from Redux Toolkit
- protect unauthorized users from viewing specific pages of your web application.
To check if the user is authenticated (signed-in) and whether they have created a business profile, we are going to make use of the useEffect
hook provided by React.
import {useEffect} from React
import { useSelector } from 'react-redux';
import db from '../firebase';
const user = useSelector((state) => state.user.user);
useEffect(() => {
if (!user.id) return navigate('/login');
try {
const q = query(
collection(db, 'businesses'),
where('user_id', '==', user.id)
);
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const business = [];
querySnapshot.forEach((doc) => {
business.push(doc.data().name);
});
if (business.length > 0) {
navigate('/dashboard');
}
});
return () => unsubscribe();
}
catch (error) {
console.log(error);
}
}, [navigate, user.id]);
- From the code snippet above:
-
useSelector
is a hook that fetches the user state from redux, and if the user does not have an id property this means the user is not authenticated. The user is then redirected to the login page. - In the
try
block, we are querying the business collection to check if there is auser_id
property whose value is equal to the id of the current user. - If the length of the array of data returned is less than 0, this means the user has no business profile record, then the user can go create one. Otherwise, the user is redirected to the dashboard page.
- Learn more about querying Firestore collections here.
-
Buildng the invoice creation page
Here, you will create a Firebase collection of containing the invoices.
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import CreateInvoiceTable from './components/CreateInvoiceTable';
import { useSelector } from 'react-redux';
import { addDoc, collection, serverTimestamp } from '@firebase/firestore';
import db from '../firebase';
const CreateInvoice = () => {
const [customerName, setCustomerName] = useState('');
const [customerAddress, setCustomerAddress] = useState('');
const [customerEmail, setCustomerEmail] = useState('');
const [itemName, setItemName] = useState('');
const [currency, setCurrency] = useState('');
const [itemCost, setItemCost] = useState(0);
const [itemQuantity, setItemQuantity] = useState(1);
const [itemList, setItemList] = useState([]);
const navigate = useNavigate();
const user = useSelector((state) => state.user.user);
useEffect(() => {
if (!user.id) return navigate('/login');
}, [navigate, user.id]);
const addItem = (e) => {
e.preventDefault();
if (itemName.trim() && itemCost > 0 && itemQuantity >= 1) {
setItemList([
...itemList,
{
itemName,
itemCost,
itemQuantity,
},
]);
}
setItemName('');
setItemCost('');
setItemQuantity('');
};
const createInvoice = async (e) => {
e.preventDefault();
};
return (
<div className="w-full p-3 md:w-2/3 shadow-xl mx-auto mt-8 rounded my-8 md:p-8">
<h3 className="text-center font-bold text-xl mb-4">Create an invoice</h3>
<form className="w-full mx-auto flex flex-col" onSubmit={createInvoice}>
<input
type="text"
required
id="customerName"
placeholder="Customer's Name"
className="py-2 px-4 bg-gray-100 w-full mb-6"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
/>
<input
type="text"
required
id="customerAddress"
className="py-2 px-4 bg-gray-100 w-full mb-6"
value={customerAddress}
placeholder="Customer's Address"
onChange={(e) => setCustomerAddress(e.target.value)}
/>
<input
type="email"
required
id="customerEmail"
className="py-2 px-4 bg-gray-100 w-full mb-6"
value={customerEmail}
placeholder="Customer's Email"
onChange={(e) => setCustomerEmail(e.target.value)}
/>
<input
type="text"
required
maxLength={3}
minLength={3}
id="currency"
placeholder="Payment Currency"
className="py-2 px-4 bg-gray-100 w-full mb-6"
value={currency}
onChange={(e) => setCurrency(e.target.value)}
/>
<div className="w-full flex justify-between flex-col">
<h3 className="my-4 font-bold ">Items List</h3>
<div className="flex space-x-3">
<div className="flex flex-col w-1/4">
<label htmlFor="itemName" className="text-sm">
Name
</label>
<input
type="text"
id="itemName"
placeholder="Name"
className="py-2 px-4 mb-6 bg-gray-100"
value={itemName}
onChange={(e) => setItemName(e.target.value)}
/>
</div>
<div className="flex flex-col w-1/4">
<label htmlFor="itemCost" className="text-sm">
Cost
</label>
<input
type="number"
id="itemCost"
placeholder="Cost"
className="py-2 px-4 mb-6 bg-gray-100"
value={itemCost}
onChange={(e) => setItemCost(e.target.value)}
/>
</div>
<div className="flex flex-col justify-center w-1/4">
<label htmlFor="itemQuantity" className="text-sm">
Quantity
</label>
<input
type="number"
id="itemQuantity"
placeholder="Quantity"
className="py-2 px-4 mb-6 bg-gray-100"
value={itemQuantity}
onChange={(e) => setItemQuantity(e.target.value)}
/>
</div>
<div className="flex flex-col justify-center w-1/4">
<p className="text-sm">Price</p>
<p className="py-2 px-4 mb-6 bg-gray-100">
{Number(itemCost * itemQuantity).toLocaleString('en-US')}
</p>
</div>
</div>
<button
className="bg-blue-500 text-gray-100 w-[150px] p-3 rounded my-2"
onClick={addItem}
>
Add Item
</button>
</div>
{itemList[0] && <CreateInvoiceTable itemList={itemList} />}
<button
className="bg-blue-800 text-gray-100 w-full p-5 rounded my-6"
type="submit"
>
CREATE INVOICE
</button>
</form>
</div>
);
};
export default CreateInvoice;
- From the code snippet above:
- I created some states that represent the customer's name, email, address, and the items to be purchased.
- The function
addItem
makes sure that item fields are not empty before adding each item to the items list. - The
<CreateInvoiceTable/>
component displays the list of the items in a table before adding them to Firestore.
❇️ View the <CreateInvoiceTable/>
component
import React from 'react';
const CreateInvoiceTable = ({ itemList }) => {
return (
<table>
<thead>
<th>Name</th>
<th>Cost</th>
<th>Quantity</th>
<th>Amount</th>
</thead>
<tbody>
{itemList.reverse().map((item) => (
<tr key={item.itemName}>
<td className="text-sm">{item.itemName}</td>
<td className="text-sm">{item.itemCost}</td>
<td className="text-sm">{item.itemQuantity}</td>
<td className="text-sm">
{Number(item.itemCost * item.itemQuantity).toLocaleString(
'en-US'
)}
</td>
</tr>
))}
</tbody>
</table>
);
};
export default CreateInvoiceTable;
- From the code above, the component accepts the items list as a prop, reverses the array then maps each item to the UI created.
❇️ Submit the invoice to Firestore by editing the createInvoice
button
const createInvoice = async (e) => {
e.preventDefault();
await addDoc(collection(db, 'invoices'), {
user_id: user.id,
customerName,
customerAddress,
customerCity,
customerEmail,
currency,
itemList,
timestamp: serverTimestamp(),
})
.then(() => navigate('/dashboard'))
.catch((err) => {
console.error('Invoice not created', err);
});
};
- From the code snippet above:
- I created a new collection called invoices, which contains all the invoices created by every user. Each invoice also has the user's id property which helps retrieve invoices created by a specific user.
-
serverTimestamp()
returns the time each invoice was created.
So far, we have authenticated users, created business profiles, and invoices for each user. Now, let's create a simple dashboard where users can create, view, and delete their invoices.
Creating a Dashboard page for authenticated users
In this section, you will learn how to fetch and delete data from Firestore.
❇️ Let's create a simple dashboard
import React, { useEffect, useState } from 'react';
import Table from './components/Table';
import { useNavigate } from 'react-router-dom';
const Dashboard = () => {
const navigate = useNavigate();
const user = useSelector((state) => state.user.user);
const [invoices, setInvoices] = useState([]);
return (
<div className="w-full">
<div className="sm:p-6 flex items-center flex-col p-3 justify-center">
<h3 className="p-12 text-slate-800">
Welcome, <span className="text-blue-800">{user.email}</span>
</h3>
<button
className=" h-36 py-6 px-12 border-t-8 border-blue-800 shadow-md rounded hover:bg-slate-200 hover:border-red-500 bg-slate-50 cursor-pointer mb-[100px] mt-[50px] text-blue-700"
onClick={() => navigate('/new/invoice')}
>
Create an invoice
</button>
{invoices.length > 0 && <Table invoices={invoices} />}
</div>
</div>
);
};
export default Dashboard;
- From the code snippet above:
- The h3 tag welcomes the user by accessing the email stored in the Redux state.
- The button links the user to the invoice creation page
- If the user has one or more invoices created, the invoices are displayed in a table.
❇️ Let's fetch the user's invoices from Firestore using useEffect hook
useEffect(() => {
if (!user.id) return navigate('/login');
try {
const q = query(
collection(db, 'invoices'),
where('user_id', '==', user.id)
);
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const firebaseInvoices = [];
querySnapshot.forEach((doc) => {
firebaseInvoices.push({ data: doc.data(), id: doc.id });
});
setInvoices(firebaseInvoices);
return () => unsubscribe();
});
} catch (error) {
console.log(error);
}
}, [navigate, user.id]);
- The code snippet above queries the
invoices
collection and returns an array of invoices matching the user's id. The<Table/>
component then accepts the collection of invoices.
❇️ Let's examine the <Table/>
component
import React from 'react';
import DeleteIcon from './DeleteIcon.svg';
import ViewIcon from './ViewIcon.svg';
import { doc, deleteDoc } from 'firebase/firestore';
import db from '../firebase';
const Table = ({ invoices }) => {
const convertTimestamp = (timestamp) => {
const fireBaseTime = new Date(
timestamp.seconds * 1000 + timestamp.nanoseconds / 1000000
);
const day =
fireBaseTime.getDate() < 10
? `0${fireBaseTime.getDate()}`
: fireBaseTime.getDate();
const month =
fireBaseTime.getMonth() < 10
? `0${fireBaseTime.getMonth()}`
: fireBaseTime.getMonth();
const year = fireBaseTime.getFullYear();
return `${day}-${month}-${year}`;
};
async function deleteInvoice(id) {
try {
await deleteDoc(doc(db, 'invoices', id));
alert('Invoice deleted successfully');
} catch (err) {
console.error(err);
}
}
return (
<div className="w-full">
<h3 className="text-xl text-blue-700 font-semibold">Recent Invoices </h3>
<table>
<thead>
<tr>
<th className="text-blue-600">Date</th>
<th className="text-blue-600">Customer</th>
<th className="text-blue-600">Actions</th>
</tr>
</thead>
<tbody>
{invoices.map((invoice) => (
<tr key={invoice.id}>
<td className="text-sm text-gray-400">
{convertTimestamp(invoice.data.timestamp)}
</td>
<td className="text-sm">{invoice.data.customerName}</td>
<td>
<ViewIcon
onClick={() => navigate(`/view/invoice/${invoiceId}`)}
/>
<DeleteIcon onClick={() => deleteInvoice(invoice.id)} />
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default Table;
- From the code snippet above:
- The
<Table/>
component accepts the invoices as props and then maps each item into the table layout. - The
convertTimestamp()
function converts the timestamp received from Firebase into a readable format for users. - Every invoice displayed has a delete and view icon. The delete icon deletes the invoice, and the view icon is a link to view and print the details of the invoice.
- The function
deleteInvoice()
receives the id of the particular invoice and deletes the invoice from the collection via its id.
- The
Creating the print invoice page
In this section, you will learn how to use the React-to-print library and build the design of your invoice. The React-to-print library allows you to print the contents of a React component without tampering with the component CSS styles.
From the <Table/>
component, we have a view icon that takes the user to the invoice page, where the user can view all the data related to a particular invoice in a printable format.
<ViewIcon onClick={() => navigate(`/view/invoice/${invoiceId}`)} />
Next,
❇️ Create a component whose layout is similar to a printable invoice or copy my layout.
❇️ Fetch all the business and customer's details from Firestore.
import { useParams } from 'react-router-dom';
let params = useParams();
useEffect(() => {
if (!user.id) return navigate('/login');
try {
const q = query(
collection(db, 'businesses'),
where('user_id', '==', user.id)
);
onSnapshot(q, (querySnapshot) => {
const firebaseBusiness = [];
querySnapshot.forEach((doc) => {
firebaseBusiness.push({ data: doc.data(), id: doc.id });
});
setBusinessDetails(firebaseBusiness[0]);
});
// params.id contains the invoice id gotten from the URL of the page
if (params.id) {
const unsub = onSnapshot(doc(db, 'invoices', params.id), (doc) => {
setInvoiceDetails({ data: doc.data(), id: doc.id });
});
return () => unsub();
}
} catch (error) {
console.error(error);
}
}, [navigate, user.id]);
- From the code snippet:
-
useParams
is a React Router hook that enables us to retrieve data from the URL of a page. Since the URL of the page is/view/invoice/:id
, thenparams. id
will retrieve the invoice id. - I then queried Firestore to get the business details using the user id and the invoice details via the
params. id
. -
onSnapshot
is a real-time listener. It's a super-fast way of fetching data from Firestore. - To learn more about
onSnapshot
, click here
-
Printing the Invoice component with React-to-print
❇️ Wrap the contents of the printable invoice with React forwardRef and add the ref prop to the parent element of the contents as shown below
//In ViewInvoice.jsx
export const ComponentToPrint = React.forwardRef((props, ref) => {
.............
...........
// functions stay here
return (
<div ref={ref}>
{/* UI contents state in here */}
</div>
)
.............
............
}
❇️ Below the componentToPrint
component, create another component, this component is a higher order component because it returns the componentToPrint
component
//In ViewInvoice.jsx
import { useReactToPrint } from 'react-to-print';
export const ViewInvoice = () => {
const ComponentRef = useRef();
const handlePrint = useReactToPrint({
content: () => ComponentRef.current,
});
return (
<>
<button onClick={handlePrint}> PRINT </button>
<ComponentToPrint ref={ComponentRef} />
</>
);
};
- From the code snippet above:
- I imported
useReactToPrint
to enable the print functionality in the React-to-print library. - The
ViewInvoice
returns all the contents of the webpage. -
ComponentToPrint
is the previously created component that contains all the contents of the webpage. -
handlePrint
is the function that triggers the print functionality.
- I imported
Adding React lazy loading for clean navigation
Here, you will learn how to optimize the web application by adding lazy loading. Lazy loading is helpful in cases where the data takes a short time to be available.
❇️ Install React spinner. It's a library that contains different types of icon animations.
npm i react-spinners
❇️ Open App.js
and wrap the imports with the lazy function, just as below.
import React, { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const CreateInvoice = lazy(() => import('./pages/CreateInvoice'));
❇️ Wrap all the Routes with the Suspense component
<Suspense fallback={<Loading />}>
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/new/invoice" element={<CreateInvoice />} />
<Route path="/view/invoice/:id" element={<ViewInvoice />} />
<Route path="/profile" element={<SetupProfile />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</Suspense>
❇️ Create the Loading component using any of the React-spinners available. For example:
import React from 'react';
import RingLoader from 'react-spinners/RingLoader';
const Loading = () => {
return (
<main className="w-full min-h-screen bg-gray-200 flex flex-col items-center justify-center">
<RingLoader />
</main>
);
};
export default Loading;
❇️ Add conditional rendering to all pages that a short time to retrieve its data. The ` component can be shown when the data is unavailable.
Conclusion
In this article, you've learned how to perform CRUD operations in Firestore, upload images using Firebase storage, and add authentication to your Firebase apps by building a full-stack invoice management system.
Firebase is a great tool that provides everything you need to build a full-stack web application. If you want to create a fully-fledged web application without any backend programming experience, consider using Firebase.
Thank you for reading thus far!
Next Steps & Useful Resources
❇️ You can try building this project using Next.js, so users' logged-in status can be persistent, even when the user refreshes the browser.
❇️ You may add the ability for users to send invoices via e-mails to clients.
❇️ Live Demo
Posted on May 14, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 6, 2024