DEV Community

Cover image for Build an invoice management system using React & Firebase
David Asaolu
David Asaolu

Posted on • Originally published at beginnerfriendly.hashnode.dev

Build an invoice management system using React & Firebase

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!

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.

Create a Firebase app

❇️ 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);
Enter fullscreen mode Exit fullscreen mode

Adding Firebase Email & Password Authentication

To make use of the Firebase Email and Password authentication.

❇️ Select Authentication on the sidebar of your screen.

Select Firebase Authentication

❇️ 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.

Select Firestore

❇️ 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.

Add Firebase Storage

❇️ Enable Firebase Storage by changing the rules from allow read, write: if false; to allow read, write: if true.

Enable Firebase Storage

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
Enter fullscreen mode Exit fullscreen mode

❇️ Cd into the react-invoice directory and install Firebase:

npm i firebase
Enter fullscreen mode Exit fullscreen mode

❇️ 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);
Enter fullscreen mode Exit fullscreen mode

❇️ 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;
Enter fullscreen mode Exit fullscreen mode

❇️ Install react-router-dom. React-router-dom allows you to navigate through various pages of the web application.

npm i react-router-dom
Enter fullscreen mode Exit fullscreen mode

❇️ Install react-to-print library. React-to-print library enables us to print React components.

npm install react-to-print
Enter fullscreen mode Exit fullscreen mode

❇️ 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
Enter fullscreen mode Exit fullscreen mode

❇️ Optional: Install Tailwind CSS and its dependencies. You can use any UI library you prefer.

npm install -D tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode

❇️ Create a tailwind.config.js and postcss.config.js by running the code below:

npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

❇️ Edit the tailwind.config.js file

module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'], //Changes made
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

❇️ Open src/index.css and add the following to the file.

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
    });
};
Enter fullscreen mode Exit fullscreen mode

❇️ 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);
      // ..
    });
};
Enter fullscreen mode Exit fullscreen mode
  • 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: {},
});
Enter fullscreen mode Exit fullscreen mode

❇️ 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>
);
Enter fullscreen mode Exit fullscreen mode

❇️ 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;
Enter fullscreen mode Exit fullscreen mode
  • 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

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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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);
  };
};
Enter fullscreen mode Exit fullscreen mode
  • 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');
  }
};
Enter fullscreen mode Exit fullscreen mode
  • From the code snippet above:
    • useNavigate is a hook from react-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 the docRef 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 is businesses/<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]);
Enter fullscreen mode Exit fullscreen mode
  • 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 a user_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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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);
    });
};
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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]);
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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.

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}`)} />
Enter fullscreen mode Exit fullscreen mode

Next,

❇️ Create a component whose layout is similar to a printable invoice or copy my layout.

Firebase Invoice sample

❇️ 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]);
Enter fullscreen mode Exit fullscreen mode
  • 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, then params. 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>
  )
  .............
  ............
}
Enter fullscreen mode Exit fullscreen mode

❇️ 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} />
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 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.

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
Enter fullscreen mode Exit fullscreen mode

❇️ 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'));
Enter fullscreen mode Exit fullscreen mode

❇️ 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>
Enter fullscreen mode Exit fullscreen mode

❇️ 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;
Enter fullscreen mode Exit fullscreen mode

❇️ 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!

Buy-me-a-coffee

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.

❇️ Firebase v9 Documentation

❇️ Live Demo

❇️ Github Repository

Discussion (0)