How I built an event ticketing system with Next.js and Firebase

This tutorial will teach you how to use Next.js and Firebase to build an event ticketing system. You'll leverage Firebase's awesome features like real-time database, authentication, and file storage to create advanced web applications with 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 users to create events and generate a registration link to share with friends or the public.
  • when someone registers via your invite link, they receive the event ticket in their email, including a passcode which you can use to verify their event ticket from your dashboard.
  • The application also allows you as a user to view the number of people who registered for your event, validate an attendee's ticket via the passcode they receive, and also disable the registration link when you have enough attendees. You can also delete an event after it has occurred.

Event ticket system application workflow

💡 Check out the live version of the application.

The Design process

Here, I'll walk you through creating the required pages for the web application.

First of all, you need a homepage for the application. The home page should have a link where new users can create an account and another for existing users to log into their accounts.

Home page preview

Next, create the sign-in and sign-up page. In this article, I'll use the Email and Password authentication method.

Login/Signup page preview

After successful login, users can create new events, view existing events, and log out of the application on a single page, called the dashboard page.

Dashboard page preview

Next, you need to allow users to view the attendees for each event, disable registration, validate the user's ticket at the venue, and delete an event.

Therefore, you need to create a route for each event. You may adapt my method, where I made each event clickable redirecting users to another page containing every detail of the particular event.

Each event page
From the image above, users can disable the registration link for the event and view and validate the attendees' list.

Finally, create the event registration page. Before showing this page to a visitor, you need to check if the event registration link has not been disabled.

Registration closed

After validation, users should supply their name and email, be added to the attendees' list, and receive an email containing the event details.

Tickets disbursement page

Since you've learnt how to build the pages of the application. Let's code.💪🏾

In the upcoming sections, you'll learn how to use the various features provided by Firebase.

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:

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.

Create Firebase App

Provide the name of your app and register the app.

Firebase Web App

Install the Firebase SDK by running the code snippet below.

npm install firebase

Create a firebase.js 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: "********",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

Finally, update the firebase.js to contain some required modules for Firebase Authentication, Storage, and Database.

import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { EmailAuthProvider } from "firebase/auth";
import { getAuth } from "firebase/auth";
import { getStorage } from "firebase/storage";

const firebaseConfig = {...<your_config>...};

// 👇🏻 Initializing Firebase in Next.js
let app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
//👇🏻 Initializing the Email/Password Auth
const provider = new EmailAuthProvider();
//👇🏻 Firebase File Storage
const storage = getStorage(app);
//👇🏻 Firebase Data Storage
const db = getFirestore(app);
//👇🏻 Firebase Auth
const auth = getAuth(app);

export { provider, auth, storage };
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.

Firebase Authentication

Click the Get Started button, enable the Email/Password method, and click Save.

Enable Email and Password Auth

If successful, your screen should display this:
Successfully added email and password

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.

Create Firestore Test mode

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.

Publish changes

Congratulations!🎉 Your Firebase Database is ready.

Setting up Firebase Storage

Select Storage from the left-hand side menu, and create the storage in test mode using the default location settings.

Update the timestamp for the Firebase Storage as done in the Firebase Firestore above.

Firebase Storage Initialization

Finally, you've set up your Firebase console. Next, I will walk you through how to communicate with Firebase by building the event ticketing system.

Communicating with Firebase: Authenticating users

In this section, I'll walk you through the authentication aspect of the event ticketing system.

You can create a utils folder containing the functions and import them into the required components.

Signing up new users

This function is executed on the SignUp page. It accepts the user's email and password and creates an account with the credentials.

import { createUserWithEmailAndPassword } from "firebase/auth";
import { useRouter } from "next/router";
import { auth } from "./firebase";

const router = useRouter();

export const firebaseCreateUser = (email, password, router) => {
    createUserWithEmailAndPassword(auth, email, password)
        .then((userCredential) => {
            const user = userCredential.user;
            successMessage("Account created 🎉");
            router.push("/login");
        })
        .catch((error) => {
            console.error(error);
            errorMessage("Account creation declined ❌");
        });
};

The code snippet above accepts the useRouter hook and the user's email and password then creates an account for the user.

With the createUserWithEmailAndPassword function, Firebase handles the authentication process. If successful, the user is notified and redirected to the login page; otherwise displays an error message.

Signing in existing users

This function allows existing users to access the application. It accepts the user's email and password and returns a user object containing all the user's information.

import { signInWithEmailAndPassword } from "firebase/auth";
import { useRouter } from "next/router";
import { auth } from "./firebase";

const router = useRouter();

export const firebaseLoginUser = (email, password, router) => {
    signInWithEmailAndPassword(auth, email, password)
        .then((userCredential) => {
            const user = userCredential.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

Firebase also provides a signOut function that enables users to log out of the application.

Here is how it works:

import { signOut } from "firebase/auth";
import { useRouter } from "next/router";
import { auth } from "./firebase";

const router = useRouter();

export const firebaseLogOut = (router) => {
    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 information object to a state after logging in or use the Firebase onAuthStateChanged hook.

Using the onAuthStateChanged hook:

import { onAuthStateChanged } from "firebase/auth";
import React, { useEffect, useCallback } from "react";
import { useRouter } from "next/router";
import { auth } from "./firebase";

const router = useRouter();

const isUserLoggedIn = useCallback(() => {
    onAuthStateChanged(auth, (user) => {
        if (user) {
            setUser({ email: user.email, uid: user.uid });
            //👉🏻 Perform an authenticated request
        } else {
            return router.push("/register");
        }
    });
}, []);

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 routes containing protected data.

Communicating with Firebase: Interacting with the database and storage

In this section, you'll learn how to interact with the Firestore Database by creating, updating, and deleting events and uploading images to the Firestore Storage.

Creating new events

After authenticating the users, they should be able to create new events via a link on the dashboard page. Create a form field that accepts similar details, as shown below.

Create Event

export const addEventToFirebase = async (
    id,
    title,
    date,
    time,
    venue,
    description,
    note,
    flier,
    router
) => {
    console.log({
        id,
        title,
        date,
        time,
        venue,
        description,
        note,
        flier,
    });
};

The function above accepts all the data related to each event. The id attribute in the code snippet above refers to the user's id, not the event's id. Adding the user's id to each event data enables you to query the events on the database via the id.

Since the form field in the image above accepts the event's flier via image upload. Therefore, you need to save the image to Firebase and attach it to the event on the database.

How do you do this? Let's see 💪🏾

Create a function handleFileReader within the Create Event component, as done below.

const handleFileReader = (e) => {
    const reader = new FileReader();
    if (e.target.files[0]) {
        reader.readAsDataURL(e.target.files[0]);
    }
    reader.onload = (readerEvent) => {
        setFlier(readerEvent.target.result);
    };
};

The handleFileReader function takes an event parameter, uses the FileReader object to read the image in base64 string format then saves it into the setFlier state.

Execute the handleFileReader function when the uploaded file changes.

<input
    name='flier'
    type='file'
    className='border-[1px] py-2 px-4 rounded-md mb-3'
    accept='image/*'
    onChange={handleFileReader}
/>

Since you've been able to convert the file to a base64 string, next, upload the event's data to Firebase when a user clicks the Create Event button.

import db, { storage } from "./firebase";
import { addDoc, collection, doc, updateDoc } from "@firebase/firestore";
import { getDownloadURL, ref, uploadString } from "@firebase/storage";

export const addEventToFirebase = async (
    id,
    title,
    date,
    time,
    venue,
    description,
    note,
    flier,
    router
) => {
    const docRef = await addDoc(collection(db, "events"), {
        user_id: id,
        title,
        date,
        time,
        venue,
        description,
        note,
        slug: createSlug(title),
        attendees: [],
        disableRegistration: false,
    });

    const imageRef = ref(storage, `events/${docRef.id}/image`);

    if (flier !== null) {
        //👇🏻 User uploaded a file
    } else {
        //👇🏻 No flier uploaded
    }
};

The Event object

  • From the code snippet above,
    • The addDoc function adds the event data to a newly-created Firebase collection called events.
    • docRef is the event document on Firebase containing the object data.
    • I added three more properties - (slug, attendees, disableRegistration) to the event document.
    • The attendees array will contain the list of people who registered for the event.
    • The slug enables us to generate a human-readable URL for each event.
    • The disableRegistration attribute enables us to differentiate events accepting registration from others.
    • The imageRef variable creates a reference between the uploaded flier and the event document. It establishes a one-one relationship between the image and the event document.
    • Lastly, since users can either choose to upload an event flier or not, you need to ensure that both cases are successful.

Before we proceed, create the createSlug function. It accepts the event title and creates a human-readable URL string format.

export const createSlug = (sentence) => {
    let slug = sentence.toLowerCase().trim();
    slug = slug.replace(/[^a-z0-9]+/g, "-");
    slug = slug.replace(/^-+|-+$/g, "");
    return slug;
};

Finally, update the conditional statement within the addEventToFirebase function.

//👇🏻 Database reference to the image
const imageRef = ref(storage, `events/${docRef.id}/image`);

if (flier !== null) {
    await uploadString(imageRef, flier, "data_url").then(async () => {
        //👇🏻 Gets the image URL
        const downloadURL = await getDownloadURL(imageRef);
        //👇🏻 Updates the docRef, by adding the flier URL to the document
        await updateDoc(doc(db, "events", docRef.id), {
            flier_url: downloadURL,
        });

        //Alerts the user that the process was successful
        successMessage("Event created! 🎉");
        router.push("/dashboard");
    });
} else {
    successMessage("Event created! 🎉");
    router.push("/dashboard");
}

The conditional statement checks if the user uploaded an image. If so, it uploads the image to Firebase Storage and updates the event document with a new property called flier_url.

The flier_url property contains the image URL hosted on Firebase Storage. If there is no image, none of this process is required, and the user gets notified that the event has been created.

Getting all the events created by a user

On the Dashboard page, you'll need to display all the events created by the current user.

To do this, query the events collection on Firebase and return only events whose user_id attribute matches the current user's ID.

import { collection, doc, onSnapshot, query, where } from "@firebase/firestore";
import db from "./firebase";

export const getEvents = (id, setEvents) => {
    try {
        const q = query(collection(db, "events"), where("user_id", "==", id));

        const unsubscribe = onSnapshot(q, (querySnapshot) => {
            const firebaseEvents = [];
            querySnapshot.forEach((doc) => {
                firebaseEvents.push({ data: doc.data(), id: doc.id });
            });
            setEvents(firebaseEvents);

            return () => unsubscribe();
        });
    } catch (error) {
        console.error(error);
    }
};

The code snippet above accepts the user's id, filters the events via the id, and returns only events whose user_id attribute matches the current user.

Deleting events

Allowing users to delete an event is one of the application's features. The function below shows how you can achieve that:

import { doc, deleteDoc } from "@firebase/firestore";
import { ref, deleteObject } from "@firebase/storage";
import db, { storage } from "./firebase";

export const deleteEvent = async (id) => {
    await deleteDoc(doc(db, "events", id));

    const imageRef = ref(storage, `events/${id}/image`);
    deleteObject(imageRef)
        .then(() => {
            console.log("Deleted successfully");
        })
        .catch((error) => {
            console.error("Image does not exist");
        });
};

The deleteDoc function deletes an event via its ID, and the deleteObject deletes the flier attached to the event using the imageRef.

Disabling new registrations

The application is incomplete if the user who created an event cannot disable registration for the event when the registration deadline has passed or if they have enough guests (attendees).

import { doc, updateDoc } from "@firebase/firestore";
import db from "./firebase";

export const updateRegLink = async (id) => {
    const number = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
    const eventRef = doc(db, "events", id);
    updateDoc(eventRef, {
        disableRegistration: true,
    });
};

The code snippet above accepts the event's id, generates a random number, and updates the event document by setting the disableRegistration property to true.

When users receive the invite to an event with its disableRegistration property set to true, the page does not display a registration form; instead, it shows that registration for that event is closed.

Handling attendees registration

Before you add an attendee to an event, you have to do three things:

  1. Fetch the event document via its ID attached to the invite link.
  2. Validate the attendee's email with the existing array of attendees at the event.
  3. If the email does not exist on the list, add the user to the list. Otherwise, return an error showing that the user has already registered for the event.
import { getDoc, doc, updateDoc, arrayUnion } from "@firebase/firestore";

//👇🏻 generates a unique passcode for the attendee
export const generatePasscode = () =>
    Math.random().toString(36).substring(2, 10);

export const registerAttendee = async (name, email, event_id) => {
    const passcode = generatePasscode();
    const eventRef = doc(db, "events", event_id);
    const eventSnap = await getDoc(eventRef);
    let firebaseEvent = {};
    if (eventSnap.exists()) {
        firebaseEvent = eventSnap.data();
        //👇🏻 gets the attendees' list
        const attendees = firebaseEvent.attendees;
        //👇🏻 filter the list
        const result = attendees.filter((item) => item.email === email);
        //👇🏻 if registration is valid
        if (result.length === 0 && firebaseEvent.disableRegistration === false) {
            //👇🏻 adds the attendee to the list
            await updateDoc(eventRef, {
                attendees: arrayUnion({
                    name,
                    email,
                    passcode,
                }),
            });
            // 👉🏻 sendEventTicketViaEmail()
            successMessage("User registered successfully! ✅");
        } else {
            errorMessage("User already registered ❌");
        }
    }
};

💡 The arrayUnion function provided by Firebase enables us to push an item to an array attribute on a document.

The code snippet above fetches an event via its ID, gets its attendees' list, and checks whether the user hasn't registered for the event before adding the user to the list of attendees.

The generatePasscode function generates a random sequence of unique numbers assigned to every user for validating each user's ticket on the event creator's dashboard.

On the event details page, you can show the attendees' list for the event and an input field that searches for attendees via their passcode.

Creating and sending event tickets to attendees

After an attendee has successfully registered for an event, you need to send an email containing the event details and their passcode to the event.

To do this, you can use a JavaScript library that supports email notifications, such as EmailJS, Novu, and Sendgrid.

How to send emails via EmailJS

Here, I'll guide you through how you can add EmailJS to the application.

Install EmailJS to the Next.js application by running the code below:

npm install @emailjs/browser

Create an EmailJS account and add an email service provider to your account.

Add an email template as done in the image below. The words in curly brackets represent variables that can hold dynamic data.

email template

Dear {{name}},

We hope this message finds you bursting with excitement because you are about to embark on a journey like no other! We are thrilled to present your personal ticket details for the most incredible event of the year!

Event Name: {{title}}

Event Description: {{description}}

Time: {{time}}

Date: {{date}} (Save the date)

PS: {{note}}


Please keep your invitation code secret - {{passcode}}. This shows you're part of the attendees.

DOWNLOAD EVENT FLIER - {{flier_url}}

Congratulations! You can send the event tickets using the email template above. Feel free to beautify yours.🔥

Sending the event tickets to attendees with EmailJS

To send the event ticket to the newly registered attendee, update the if block within the registerAttendee function, as done below.

if (result.length === 0) {
    await updateDoc(eventRef, {
        attendees: arrayUnion({
            name,
            email,
            passcode,
        }),
    });
    const flierURL = firebaseEvent.flier_url
        ? firebaseEvent.flier_url
        : "No flier for this event";
    sendEmail(
        name,
        email,
        firebaseEvent.title,
        firebaseEvent.time,
        firebaseEvent.date,
        firebaseEvent.note,
        firebaseEvent.description,
        passcode,
        flierURL,
        setSuccess,
        setLoading
    );
} else {
    setLoading(false);
    errorMessage("User already registered ❌");
}

The code snippet checks if there is no existing user with the same email before adding the new user to the attendees' list. If there is no flier attached to the event, it takes note of that and passes all the event's information as parameters to the sendEmail function.

Create the sendEmail function to send the event details to the newly-registered attendee's email.

import emailjs from "@emailjs/browser";

const sendEmail = (
    name,
    email,
    title,
    time,
    date,
    note,
    description,
    passcode,
    flier_url,
    setSuccess,
    setLoading
) => {
    emailjs
        .send(
            process.env.NEXT_PUBLIC_SERVICE_ID,
            process.env.NEXT_PUBLIC_TEMPLATE_ID,
            {
                name,
                email,
                title,
                time,
                date,
                note,
                description,
                passcode,
                flier_url,
            },
            process.env.NEXT_PUBLIC_API_KEY
        )
        .then(
            (result) => {
                setSuccess(true);
                //👉🏻 Email sent ✅
            },
            (error) => {
                alert(error.text);
            }
        );
};

The sendEmail function accepts all the required parameters and sends the email using the EmailJS library.

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, Storage, and Database, and
  • how to build an event ticketing system.

I've built a live version of the application, check it out - https://eventtiz.vercel.app. 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! 🎉

No comments:

Post a Comment