In this tutorial, I'll walk you through how I built a sales management application that enables store owners or cashiers to record and track sales using Next.js v13, Typescript, and Firebase.
Upon completion, you'll be able to create advanced real-world web applications using Firebase's amazing features, like real-time database and authentication.
π‘ PS: This tutorial assumes you have a basic knowledge of React or Next.js.
Application Workflow
Before we start coding, let me summarise how the application works. The application does the following:
- authenticates users via Email and Password with Firebase,
- allows a user (cashier or store owner) to create various categories for the products,
- add and delete products from the application, and
- record and track sales made daily.
Here is a brief demo of the application:
The UI Design Process
Here, I'll walk you through creating the required pages for the web application.
First of all, you need to create a Login page. You don't need to create a sign-up page since it's exclusive to the user (cashier/store owner).
Next, a Dashboard page that shows the metrics for the application and also includes navigation to other pages.
You also need a Products page where the user can add new products when available and delete products at any point in time.
Next, create a Categories page to enable users to add or delete categories.
Finally, you need to create the main part of the application - the Sales page, where the user can view previous sales and add new ones.
You can add a date input field on this page to enable the user to fetch the sales made for each day, as shown below.
The Add New Sales component can be a modal, shown when a user clicks the Add New Sales
button.
In the upcoming sections, you'll learn how to create and connect the backend database to the UI and interact with Firebase in a Next.js app.
What is Firebase?
Firebase is a Backend-as-a-Service (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, file storage, cloud functions, hosting services, and many more.
How to add Firebase to a Next.js application
To add Firebase to a Next.js app, follow the steps below:
Create a Next.js 13 application that uses Typescript and the app
router.
npx create-next-app sales-app
Visit the Firebase console and sign in with a Gmail account.
Create a Firebase project once you are signed in.
Select the </>
icon to create a new Firebase web app.
Provide the name of your app and register the app.
Install the Firebase SDK by running the code snippet below.
npm install firebase
Create a firebase.ts
file at the root of your Next.js project and copy the Firebase configuration code for your app into the file.
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
const firebaseConfig = {
apiKey: "*****",
authDomain: "*****",
projectId: "*****",
storageBucket: "*****",
messagingSenderId: "*******",
appId: "********",
measurementId: "********"
};
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
Finally, update the firebase.ts
to contain some required modules for Firebase Authentication and Firestore Database.
import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { EmailAuthProvider } from "firebase/auth";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: "AIzaSyCtmI3jLzqDSr3UIwuUdBa5ocsN5vjzpW8",
authDomain: "stock-taking-19198.firebaseapp.com",
projectId: "stock-taking-19198",
storageBucket: "stock-taking-19198.appspot.com",
messagingSenderId: "228033001185",
appId: "1:228033001185:web:b2020053fb824a87d9a9a0",
measurementId: "G-79BQVKMPSR"
};
// Initialize Firebase
let app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
const provider = new EmailAuthProvider();
const db = getFirestore(app);
const auth = getAuth(app);
export { provider, auth };
export default db;
Congratulations!π You've successfully added Firebase to your Next.js app. Next, let's set up the needed Firebase features.
Setting up Firebase Authentication
Before you can add Firebase Authentication to your application, you need to set it up on your console.
Select Build on the left-hand panel, and click Authentication.
Click the Get Started button, enable the Email/Password method, and click Save.
If successful, your screen should display this:
Setting up Firebase Firestore
Select Firestore Database from the left-hand side menu and create a database.
Create the database in test mode, and use the default Cloud Firestore location settings.
After creating your database, select Usage
from the top menu bar, edit the rules, and publish the changes. This enables you to make requests to the database for a longer period of time.
π‘ PS: If you're not building this as a side project, ensure you switch the database to production mode to prevent your app from cyber attacks.
Finally, create three database collections for the categories, products, and sales data.
Congratulations!π Your database is ready. You'll learn how to interact with it shortly.
User authentication with Firebase Auth
In this section, I'll walk you through the authentication aspect of the sales management system. You'll learn how to log users in and out of the application and how to protect pages containing confidential data from unauthenticated users.
First, create a utils.ts
file that will contain the functions and import them into the required components.
Signing into the application
The function below allows the user to access the application. It accepts the user's email and password and returns a user object containing all the user's information.
You can create a new user on your Firebase console and execute the function below when the sign-in form is submitted.
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context";
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from "./firebase";
export const LoginUser = (email: string, password: string, router: AppRouterInstance) => {
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
const user = userCredential.user;
//ππ» logs user's details
console.log("User >>", user);
successMessage("Authentication successful π");
router.push("/dashboard");
})
.catch((error) => {
console.error(error);
errorMessage("Incorrect Email/Password β");
});
};
The code snippet above validates the user's credentials and returns an object containing all the information related to the user. If the process is successful, it redirects the user to the dashboard page; otherwise returns an error.
Logging users out of the application
Firebase also provides a signOut
function that enables users to log out of the application.
Here is how it works:
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context";
import { signOut } from "firebase/auth";
import { auth } from "./firebase";
export const LogOut = (router: AppRouterInstance) => {
signOut(auth)
.then(() => {
successMessage("Logout successful! π");
router.push("/");
})
.catch((error) => {
errorMessage("Couldn't sign out β");
});
};
The code snippet above logs users out of the application by getting the active user's details and logging them out with the help of the signOut
function.
Protecting pages from unauthenticated users
To do this, you can store the user's details object to a state after logging in or use the Firebase onAuthStateChanged
hook.
Using the onAuthStateChanged
hook:
"use client"
import { auth } from '@/firebase'
import { onAuthStateChanged } from "firebase/auth";
import { useState, useCallback, useEffect } from "react"
const isUserLoggedIn = useCallback(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
setUser({ email: user.email, uid: user.uid });
//ππ» Perform an authenticated request
} else {
return router.push("/");
}
});
}, [router]);
useEffect(() => {
isUserLoggedIn();
}, [isUserLoggedIn]);
The onAuthStateChanged
hook checks if the user is active and returns the object containing all the user's details. You can execute the function on page load for all the routes, except the Login page.
Real-time communication with Firebase: data structure and CRUD operation
In this section, I'll walk you through setting up the data structure for the application and interacting with Firebase.
The Categories collection
The first page to set up in this application is the Categories page because when the user adds a product, it has to be under a category.
Therefore, create your Categories page similar to the image below.
Execute the code snippet below when the user adds a new category.
import { collection, addDoc } from "firebase/firestore";
import db from "./firebase";
export const addCategory = async (name: string) => {
try {
await addDoc(collection(db, "categories"), {
name
})
successMessage(`${name} category added! π`)
} catch (err) {
errorMessage("Error! β")
console.error(err)
}
}
The code snippet above accepts the category name from the input field and creates a new document (category) on Firebase.
To delete a category, run the code snippet below when a user clicks the delete button.
export const deleteCategory = async (id: string, name:string) => {
try {
//ππ» deletes the category
await deleteDoc(doc(db, "categories", id));
//ππ» delets the products within the category
const q = query(collection(db, "products"), where("category", "==", name));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((document) => {
deleteDoc(doc(db, "products", document.id));
});
});
successMessage(`${name} category deleted π`)
} catch (err) {
errorMessage("Encountered an error β")
console.log(err)
}
}
The deleteCategory
function above accepts the ID and name of the selected category, deletes the category from the list, and deletes the products under the selected category.
Finally, you need to get the categories list from Firebase when the page loads. Execute the function below to do this:
export const getCategories = async (setCategories: any) => {
try {
const unsub = onSnapshot(collection(db, "categories"), doc => {
const docs: any = []
doc.forEach((d: any) => {
docs.push( { ...d.data(), id: d.id })
});
setCategories(docs)
})
} catch (err) {
console.error(err)
setCategories([])
}
}
The code snippet above accepts a parameter called setCategories
- the React state containing all the categories. Then sends a request to Firebase to retrieve the categories list and updates the setCategories
state with the data.
The Products collection
After setting up the Categories page, you need to allow users to add and delete products under any available categories.
Create a Products
page similar to the image above and execute the code snippet below when the user adds a new product.
export const addProduct = async (name: string, price: number, category: string) => {
try {
await addDoc(collection(db, "products"), {
name, price, category
})
successMessage(`${name} product added! π`)
}
catch (err) {
errorMessage("Error! β")
console.error(err)
}
}
The code snippet above accepts the product's name, price, and category from the form field and adds it to the products
collection.
Run this function to display the products on page load.
export const getProducts = async (setProducts: any) => {
try {
const unsub = onSnapshot(collection(db, "products"), doc => {
const docs: any = []
doc.forEach((d: any) => {
docs.unshift( { ...d.data(), id: d.id })
});
setProducts(docs)
})
} catch (err) {
console.error(err)
setProducts([])
}
}
Finally, allow the user to delete products from the products
collection. Therefore, execute the function below when a user clicks the delete button.
export const deleteProduct = async (id: string, name:string) => {
try {
await deleteDoc(doc(db, "products", id));
successMessage(`${name} deleted π`)
} catch (err) {
errorMessage("Encountered an error β")
console.log(err)
}
}
The function accepts the product's name and ID, then deletes the product from the collection via its ID.
The Sales collection
Here, you need to create functions that enable the user to add and get sales for a particular day and the total sales made on the platform.
export const addSales = async (customerName: string, customerEmail: string, products: Items[], totalAmount: number, setAddNew: any) => {
try {
await addDoc(collection(db, "sales"), {
customerName, customerEmail, products, totalAmount, timestamp: serverTimestamp()
})
successMessage("Sales recorded! π")
setAddNew(false)
} catch (err) {
console.error(err)
errorMessage("Error! Try again β")
}
}
The function below accepts the customer's and products' details, then adds them to the sales
collection.
To get all the sales, execute the code snippet below on the page load. It queries the sales
collection and returns all the data.
export const getSales = async (setSales: any) => {
try {
const docRef = collection(db, "sales")
const q = query(docRef, orderBy("timestamp"))
onSnapshot(q, (snapshot) => {
const docs: any = []
snapshot.forEach((d: any) => {
docs.unshift( { ...d.data(), id: d.id })
});
setSales(docs)
})
} catch (err) {
console.error(err)
setSales([])
}
}
On the dashboard page, a box shows the total amount made from the sales. The code snippet below returns the total amount.
export const getTotalSales = async (setTotalSales: any) => {
try {
const unsub = onSnapshot(collection(db, "sales"), doc => {
let totalSales:number = 0
doc.forEach((d: any) => {
totalSales += d.data().totalAmount
});
setTotalSales(totalSales)
})
} catch (err) {
console.error(err)
}
}
Finally, on the Products page, you may want to display sales only made on a particular day. The code snippet below queries the sales
collection and returns sales made on a specific day.
You can edit the function to return sales made weekly, monthly, quarterly, or yearly. You just need to update the Date format with the right duration.
export const getSalesForDay = async (date: Date | null, setSales: any) => {
try {
const day = date?.getDate()
const month = date?.getMonth()
const year: number | undefined = date?.getFullYear()
if (day !== undefined && month !== undefined && year !== undefined) {
const startDate = new Date(year, month, day, 0, 0, 0);
const endDate = new Date(year, month, day, 23, 59, 59);
const docRef = collection(db, "sales")
const q = query(docRef, orderBy("timestamp"), where("timestamp", ">=", Timestamp.fromDate(startDate)), where("timestamp", "<=", Timestamp.fromDate(endDate)))
onSnapshot(q, (snapshot) => {
const docs: any = []
snapshot.forEach((d: any) => {
docs.unshift( { ...d.data(), id: d.id })
});
setSales(docs)
})
}
}
catch (err) {
console.error(err)
}
}
Conclusion
Congratulations on making it thus far! You've learnt
- what Firebase is,
- how to add Firebase to a Next.js app,
- how to work with Firebase Auth and Firebase Firestore, and
- how to build a sales management application.
The source code is also available here.
Firebase is a great tool that provides almost everything you need to build a full-stack web application. If you want to create a full-stack web application without any backend programming experience, consider using Firebase.
Thank you for reading! π
Open to workπ
Did you enjoy this article or need an experienced Technical Writer / React Developer for a remote, full-time, or contract-based role? Feel free to contact me.
GitHub || LinkedIn || Twitter
Top comments (4)
Nice work!
Thank you.π
A very sublime project; I will try to interact with your code and come up with something nice.
Thank you for sharing this
You're welcomeπ