DEV Community

Cover image for Building a customer support app with live chat and notifications using Next.js, Novu and Appwrite
David Asaolu
David Asaolu

Posted on

Building a customer support app with live chat and notifications using Next.js, Novu and Appwrite

In this article, I'll walk you through how I built a customer support application with live chatting, in-app, and email notification features using Next.js, Novu, Appwrite, and EmailJS.

Appwrite Cloud handles the authentication, database, and file storage aspects, Novu handles in-app notifications, and EmailJS for email messaging.

Upon completion, you'll be able to build advanced applications that require a chat feature with Appwrite and add email and in-app notifications to your apps via EmailJS and Novu.

💡 PS: This tutorial assumes you have a basic knowledge of React or Next.js

Fully ready

Application Workflow

Before we start coding, let me summarise how the application works. The application does the following:

  • allows customers to create support tickets,
  • authenticates staff and ensures that no one other than the staff can log in to the application,
  • notifies the staff when a customer creates a support ticket,
  • allows both the staff and customers to settle disputes via the live-chatting feature powered by Appwrite Cloud, and
  • sends a confirmation email to the customers when they open a ticket and also when a staff sends them a message regarding the open ticket.

Customer support app with Next.js

Brief Demo

The UI Design Process

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

First, you need to create a home page where customers can create support tickets, and staff can also navigate to the login page via the page.

Home page

Next, create the staff login page that authenticates visitors to ensure they are staff before allowing them access to the dashboard.

Staff Login page
You also need a dashboard page that shows all the tickets based on their status. Each support ticket must be clickable and redirects the staff to its details page.

Dashboard page

Ticket details

Next, create a page that displays all the data related to a particular ticket. Staff can update the status of a support ticket and download its attachment; if available.

Details page

Then, create a chat page where a customer support staff and a customer can communicate in real time about an issue.
The page should not require authentication but an access code sent to the customer's email. The chat URL will be similar to this https://firm-support.vercel.app/chat/<ticket_ID>.

Chat Page

Finally, you need an admin page that enables you to add or remove staff from the application. With this, you can have more than one user manage the support tickets or respond to customers' queries.

Admin Page

Adding Appwrite to a Next.js application

Appwrite is a powerful open-source backend platform that enables you to create secured and scalable (web and mobile) applications. With Appwrite, you don't need to worry about the backend resources of your application because with Appwrite, you - "Build Fast. Scale Big. All in One Place."

Instead of setting up your backend server on your computer, Appwrite Cloud enables you to focus on developing your applications; while, it manages crucial backend functionalities such as user authentication, database management, file storage, and more.

Installation and configuration

To add Appwrite to a Next.js app, follow the steps below:

First of all, create a Next.js project by running the code below.

npx create-next-app customer-support-app
Enter fullscreen mode Exit fullscreen mode

Visit Appwrite's website and create a new account.

Create a new organisation and project. Each project contains all the resources you need to build a fully functional application.

Create Appwrite Project

Next, you need to choose where and how you want to use Appwrite, either as a web or mobile SDK or you need to integrate it with your (existing) server.

Connect to a Web App

Since we are building a Next.js app with Appwrite Cloud, select Web App from the SDK Platform menu and register a new app under the project.

From the image below, I provided a name for the application and used an asterisk as the hostname. After deploying the application on Vercel, you can change the hostname to the URL provided by Vercel.

Connect Appwrite to Next.js

Install the Appwrite Node.js SDK into your Next.js project as done below.

npm install appwrite
Enter fullscreen mode Exit fullscreen mode

Create a .env.local and appwrite.js file at the root of your project.

touch appwrite.js .env.local
Enter fullscreen mode Exit fullscreen mode

Copy the code below into the appwrite.js file.

import { Client, Account, Databases, Storage } from "appwrite";
const client = new Client();

client
    .setEndpoint("https://cloud.appwrite.io/v1")
    .setProject(process.env.NEXT_PUBLIC_PROJECT_ID);

export const account = new Account(client);

export const db = new Databases(client);

export const storage = new Storage(client);
Enter fullscreen mode Exit fullscreen mode

The code snippet above enables us to access and interact with the authentication, database, and file storage features provided by Appwrite.

Copy the code below into the .env.local file.

NEXT_PUBLIC_PROJECT_ID=<your_project_id>
NEXT_PUBLIC_DB_ID=<your_database_id>
NEXT_PUBLIC_TICKETS_COLLECTION_ID=<your_collection_id>
NEXT_PUBLIC_USERS_COLLECTION_ID=<your_collection_id>
NEXT_PUBLIC_BUCKET_ID=<your_bucket_id>
Enter fullscreen mode Exit fullscreen mode

The code snippet above contains environment variables containing all the private keys needed for interacting with Appwrite Cloud.

On your project dashboard, click the Project ID button to copy your project's id and paste it into the .env.local variable.

Project Dashboard ID

Setting up Authentication with Appwrite Cloud

Since you are using the Email and Password authentication method, you don't need to add any configurations on Appwrite Cloud before using the service because it has been configured by default.

However, let's add an extra layer of security to the project by updating the default Session length.

Select Auth from the sidebar menu and switch to the Security tab.

Auth Settings

Scroll down to the Session Length section and change it from 365 days to 1 hour. Users will need to be re-authenticated after an hour of using the application, and, in case a user doesn't log out of our application, they are logged out automatically after an hour.

Session Length

Click Update to add the new setting, and you're ready to go.🚀

Setting up the Appwrite Database

Here, you'll learn how to set up the database on Appwrite Cloud.

Select Database from the sidebar menu to create a new database.

Create Database

Next, you need to create two database collections, one for the support tickets and the other for the staff within the application.
You may be wondering why we need another collection for the users. What about the users saved on the Appwrite Auth?

The reason is that you can not get all the users or delete a user using Appwrite Cloud. It is only possible when you use the Node.js SDK.

However, since we need to view all the users (staff) and add or delete whenever necessary from the Admin page of the application; therefore, you need to create a users database containing similar information as the Auth section.

Create two database collections for the support tickets and users, and copy their IDs into the env.local file.

Database Collections

Select the users collection and create three required attributes, as shown below.

User attributes

Finally, click on the Settings menu under the users and update the permission to allow only users to create, read, and delete users from the collection.

Appwrite Settings

Setting up Appwrite Storage

Select Storage from the sidebar menu and create a new bucket for images attached to each support ticket. Users will be able to upload screenshots of the issues they are facing when creating a support ticket.

Create bucket

Copy the bucket ID and paste it into the .env.local file.

Bucket ID

Communicating with Appwrite: Authenticating users

Unlike conventional applications, this application does not have a sign-up page because it is exclusive to staff. Therefore, you can create the initial account via your dashboard on Appwrite Cloud.

In this section, you'll learn how to set up the authentication process for the application.
You can create a utils folder containing the functions and import them into the required components.

mkdir utils
cd utils
touch functions.js
Enter fullscreen mode Exit fullscreen mode

Add the following imports into the file to enable us to interact with the backend features. We'll make use of them in the upcoming sections.

import { account, db, storage } from "./appwrite";
import { ID } from "appwrite";
Enter fullscreen mode Exit fullscreen mode

Logging into the application

Remember, we have Appwrite Auth and the users collection on the database storing users' details. Therefore to log users into the application, you need to authenticate the user using Appwrite Auth, then filter the users collection to verify if the user's details exist before granting access to the application.

//👇🏻 filters the users' list
const checkUserFromList = async (email, router) => {
    try {
        const response = await db.listDocuments(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_USERS_COLLECTION_ID
        );
        const users = response.documents;
        const result = users.filter((user) => user.email === email);

        //👉🏻 USER OBJECT ==> console.log(result[0])

        if (result.length > 0) {
            successMessage("Welcome back 🎉");
            router.push("/staff/dashboard");
        } else {
            errorMessage("Unauthorized...Contact Management.");
        }
    } catch (error) {
        errorMessage("An error occurred 😪");
        console.error(error);
    }
};

//👇🏻 authenticates the user
export const logIn = async (email, password, router) => {
    try {
        //👇🏻 Appwrite login method
        await account.createEmailSession(email, password);
        //👇🏻 calls the filter function
        await checkUserFromList(email, router);
    } catch (error) {
        console.log(error);
        errorMessage("Invalid credentials ❌");
    }
};

Enter fullscreen mode Exit fullscreen mode

The code snippet above accepts the user's email and password from the form field, authenticates the user using Appwrite Auth, then checks if the user is on the staff list before granting permission to the application.

Logging out

Logging users out still work the conventional way. Appwrite also provides an account.deleteSession() method that enables users to log out of an ongoing session.

export const logOut = async (router) => {
    try {
        await account.deleteSession("current");
        router.push("/");
        successMessage("See ya later 🎉");
    } catch (error) {
        console.log(error);
        errorMessage("Encountered an error 😪");
    }
};
Enter fullscreen mode Exit fullscreen mode

Protecting pages from unauthenticated users

To do this, you can store the user's information object to a state after logging in or use Appwrite's account.get() method.

Using the account.get() method:

export const checkAuthStatus = async (setUser, setLoading, router) => {
    try {
        const response = await account.get();
        setUser(response);
        setLoading(false);
    } catch (err) {
        router.push("/");
        console.error(err);
    }
};
Enter fullscreen mode Exit fullscreen mode

The code snippet above gets all the information related to the currently signed-in users. It 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, such as the Dashboard, Ticket Details, and Admin routes.

Admin page: adding and removing staff

In this section, I'll guide you through how to add, get, and delete staff from the users' collection. We are technically building the backend functionality of the Admin page.

Admin page

Adding new staff

The code snippet below accepts the user's name, email, and password when you submit the form, create an account on Appwrite Auth, and save the email and the name in the users database.

//👇🏻 generates random string as ID
const generateID = () => Math.random().toString(36).substring(2, 24);

export const addUser = async (name, email, password) => {
    try {
        //👇🏻 create a new acct on Appwrite Auth
        await account.create(generateID(), email, password, name);
        //👇🏻 adds the user's details to the users database
        await db.createDocument(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_USERS_COLLECTION_ID,
            ID.unique(),
            { user_id: generateID(), name, email }
        );
        successMessage("User added successfully 🎉");
    } catch (error) {
        console.log(error);
    }
};
Enter fullscreen mode Exit fullscreen mode

Getting the staff list

The code snippet accesses the document on Appwrite Cloud and returns all the data in the users collection.

export const getUsers = async (setUsers) => {
    try {
        const response = await db.listDocuments(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_USERS_COLLECTION_ID
        );
        setUsers(response.documents);
    } catch (error) {
        console.log(error);
    }
};
Enter fullscreen mode Exit fullscreen mode

Removing staff

To do this, you need to pass the ID of the selected staff into the function when you click the Remove button.

export const deleteUser = async (id) => {
    try {
        await db.deleteDocument(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_USERS_COLLECTION_ID,
            id
        );
        successMessage("User removed 🎉"); // Success
    } catch (error) {
        console.log(error); // Failure
        errorMessage("Encountered an error 😪");
    }
};
Enter fullscreen mode Exit fullscreen mode

💡 The user's credentials are still available in Appwrite Auth, but since we verify users' credentials against the users collection when they log in, this method is still valid.

Support ticket: data structure, database and storage

In this section, you'll learn how to create the backend (database) for the customer support application.

Creating a support ticket

When a customer creates a support ticket, you have to retrieve all the details from the form and save them to the database. Therefore, create a function that accepts all the ticket's details and ensures it works perfectly whether or not the customer uploads a screenshot of the problem they are facing.

export const sendTicket = async (name, email, subject, message, attachment) => {
    if (attachment !== null) {
        //👇🏻 Customer attached an image
        console.log({ name, email, subject, message, attachment });
    } else {
        //👇🏻 No attachment
        console.log({ name, email, subject, message });
    }
};
Enter fullscreen mode Exit fullscreen mode

To add the ticket's information to the tickets collection on Appwrite Cloud, add the following attributes to the collection.

Name Required? Type
name yes string
email yes string
subject yes string
status yes string
content yes string
access_code yes string
attachment_url no string
messages string[]

Ticket Attribute

Switch to the Settings tab and update the permission on the tickets collection as shown below.

Support Tickets Permission

Finally, update the sendTicket function to add the event details to Appwrite Cloud.

export const sendTicket = async (name, email, subject, message, attachment) => {
    const createTicket = async (file_url = "https://google.com") => {
        try {
            const response = await db.createDocument(
                process.env.NEXT_PUBLIC_DB_ID,
                process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
                ID.unique(),
                {
                    name,
                    email,
                    subject,
                    content: message,
                    status: "open",
                    messages: [
                        JSON.stringify({
                            id: generateID(),
                            content: message,
                            admin: false,
                            name: "Customer",
                        }),
                    ],
                    attachment_url: file_url,
                    access_code: generateID(),
                }
            );
            //👇🏻 send notification to the customer
            console.log("RESPONSE >>>", response);
            successMessage("Ticket created 🎉");
        } catch (error) {
            errorMessage("Encountered saving ticket ❌");
        }
    };

    if (attachment !== null) {
        try {
            const response = await storage.createFile(
                process.env.NEXT_PUBLIC_BUCKET_ID,
                ID.unique(),
                attachment
            );
            const file_url = `https://cloud.appwrite.io/v1/storage/buckets/${process.env.NEXT_PUBLIC_BUCKET_ID}/files/${response.$id}/view?project=${process.env.NEXT_PUBLIC_PROJECT_ID}&mode=admin`;
            //👇🏻 creates ticket with its image
            createTicket(file_url);
        } catch (error) {
            errorMessage("Error uploading the image ❌");
        }
    } else {
        //👇🏻 creates ticket even without an image (screenshot)
        await createTicket();
    }
};
Enter fullscreen mode Exit fullscreen mode
  • From the code snippet above:
    • The nested function, createTicket accepts all the ticket's attributes and creates a new document on the Appwrite Cloud. Since uploading an attachment is optional, the flier_url attribute has a default URL value.
    • The messages array attribute creates a new structure for the live chatting feature. It converts the content and the customer information into a JSON string and adds it to the messages array.
    • The if and else code block checks if the customer uploaded an image. If true, the code uploads the image to the Cloud Storage, retrieves its URL, and passes it into the createTicket function.
    • Otherwise, the createTicket function uses the default value (https://google.com) as the attachment URL.

Getting the support tickets from Appwrite Cloud

On the Dashboard page, you need to retrieve all the support tickets. The function below retrieves and group them based on their status.

Support Tickets

export const getTickets = async (
    setOpenTickets,
    setInProgressTickets,
    setCompletedTickets
) => {
    try {
        const response = await db.listDocuments(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID
        );
        const tickets = response.documents;
        const openTickets = tickets.filter((ticket) => ticket.status === "open");
        const inProgressTickets = tickets.filter(
            (ticket) => ticket.status === "in-progress"
        );
        const completedTickets = tickets.filter(
            (ticket) => ticket.status === "completed"
        );
        setCompletedTickets(completedTickets);
        setOpenTickets(openTickets);
        setInProgressTickets(inProgressTickets);
    } catch (error) {
        console.log(error); // Failure
    }
};
Enter fullscreen mode Exit fullscreen mode

Getting the ticket details

When you click on each support ticket on the Dashboard, it needs to redirect you to another page containing all information related to the support ticket.

Therefore, you need to create a ticket/[id].js file that retrieves the ticket's details via server-side rendering using the id from the page route, as done below.

export async function getServerSideProps(context) {
    let ticketObject = {};
    try {
        const response = await db.getDocument(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
            context.query.id
        );

        ticketObject = response;
    } catch (err) {
        ticketObject = {};
    }

    return {
        props: { ticketObject },
    };
}
Enter fullscreen mode Exit fullscreen mode

Updating the ticket's status

On the details page, you can update the status of a support ticket using the HTML select tag and accept three types of values - "open", "in-progress", and "completed". You can update the status using the function below:

export const updateTicketStatus = async (id, status) => {
    try {
        await db.updateDocument(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
            id,
            { status }
        );
        successMessage("Status updated, refresh page 🎉");
    } catch (error) {
        console.log(error); // Failure
        errorMessage("Encountered an error ❌");
    }
};
Enter fullscreen mode Exit fullscreen mode

Live chatting feature with Next.js and Appwrite

In this section, I'll walk you through adding the chat feature to the application. To do this, create a chat page similar to the image below.

Chat Page

This page should require an access code on page load but doesn't require authentication to access the page. Run the code snippet below when a user sends a message.

export const sendMessage = async (text, docId) => {
    //👇🏻 get the ticket ID
    const doc = await db.getDocument(
        process.env.NEXT_PUBLIC_DB_ID,
        process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
        docId
    );

    try {
        //👇🏻 gets the user's object (admin)
        const user = await account.get();
        const result = await db.updateDocument(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
            docId,
            {
                messages: [
                    ...doc.messages,
                    JSON.stringify({
                        id: generateID(),
                        content: text,
                        admin: true,
                        name: user.name,
                    }),
                ],
            }
        );
        //👇🏻 message was added successfully
        if (result.$id) {
            successMessage("Message Sent! ✅");
            //👉🏻 email the customer with access code and chat URL
        } else {
            errorMessage("Error! Try resending your message❌");
        }
    } catch (error) {
        //👇🏻 means the user is a customer
        const result = await db.updateDocument(
            process.env.NEXT_PUBLIC_DB_ID,
            process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
            docId,
            {
                messages: [
                    ...doc.messages,
                    JSON.stringify({
                        id: generateID(),
                        content: text,
                        admin: false,
                        name: "Customer",
                    }),
                ],
            }
        );
        if (result.$id) {
            successMessage("Message Sent! ✅");
            //👉🏻 notify staff via notifications
        } else {
            errorMessage("Error! Try resending your message❌");
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

The code snippet above checks if the user is a staff or a customer before adding the message to the messages array.

Real-time messaging

To display the chat messages immediately you send them.

Create an event listener on the tickets collection and update the messages array with the changes, as done below.

//👉🏻 "client" is from your appwrite.js file
useEffect(() => {
    const unsubscribe = client.subscribe(
        `databases.${process.env.NEXT_PUBLIC_DB_ID}.collections.${process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID}.documents`,
        (data) => {
            const messages = data.payload.messages;
            setMessages(messages.map(parseJSON));
        }
    );
    return () => {
        unsubscribe();
    };
}, []);
Enter fullscreen mode Exit fullscreen mode

You can learn more about real-time listeners in Appwrite.

Auto-scroll feature

Add an empty div below the chat messages element.

<div className='chat__container'>
    //👇🏻 chat messages element
    {/* {messages.map((message) => (
                                        <div>{message}</div>
                                    ))} */}
    <div ref={lastMessageRef} />
</div>
Enter fullscreen mode Exit fullscreen mode

Create a reference to the div element and shift the mouse focus when there is a new message, as done below.

const lastMessageRef = useRef(null);

useEffect(() => {
    // 👇️ scroll to bottom every time messages change
    lastMessageRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
Enter fullscreen mode Exit fullscreen mode

Adding in-app and email notifications with Novu and EmailJS

Here, you'll learn how to add in-app notifications with Novu and send emails using EmailJS.
Recall that we need the in-app notification when a customer sends a message via the chat page and when they create a new ticket.
The email notification is needed when the customer support staff sends a message and when a customer creates a new ticket.

Setting up in-app notifications with Novu in Next.js

Novu is the first open-source notification infrastructure that manages all forms of communications. In this article, we'll make use of its in-app notification feature.

Install Novu Node.js SDK and its Notification center by running the code below.

npm install @novu/node @novu/notification-center
Enter fullscreen mode Exit fullscreen mode

Run npx novu init to create and access your dashboard.

Add your Novu Subscriber ID, App ID, and API Key into the .env.local file.

NEXT_PUBLIC_NOVU_SUBSCRIBER_ID=<subscriber_ID>
NEXT_PUBLIC_NOVU_APP_ID=<your_app_ID>
NEXT_PUBLIC_NOVU_API_KEY=<your_api_key>
Enter fullscreen mode Exit fullscreen mode

Create two notification template on your Novu dashboard and edit its content for the two cases - when a customer creates a ticket and sends a chat message. Add the notification functions to your Next.js app, as shown below.

const { Novu } = require("@novu/node");
const novu = new Novu(process.env.NEXT_PUBLIC_NOVU_API_KEY);

export default async function handler(req, res) {
    const { status, title, username } = req.body;
    const response = await novu
        .trigger("<template_name>", {
            to: {
                subscriberId: process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID,
            },
            payload: {
                status,
                title,
                username,
            },
        })
        .catch((err) => console.error(err));

    res.status(200).json(response.data);
}
Enter fullscreen mode Exit fullscreen mode

Finally, create the notification bell to display the notifications within your application and add to the Nav component within your application.

import {
    NovuProvider,
    PopoverNotificationCenter,
    NotificationBell,
} from "@novu/notification-center";

function Novu() {
    function onNotificationClick(message) {
        if (message?.cta?.data?.url) {
            window.location.href = message.cta.data.url;
        }
    }

    return (
        <NovuProvider
            subscriberId={process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID}
            applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID}
        >
            <PopoverNotificationCenter
                onNotificationClick={onNotificationClick}
                colorScheme='light'
            >
                {({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
            </PopoverNotificationCenter>
        </NovuProvider>
    );
}
export default Novu;
Enter fullscreen mode Exit fullscreen mode

Sending email notifications with EmailJS

Here, you need to send an email to the customer when they create a support ticket and when a staff sends them a message. To do this follow the steps below.

Install EmailJS by running the code below.

npm install @emailjs/browser
Enter fullscreen mode Exit fullscreen mode

Configure your EmailJS account and copy your credentials into the .env.local file.

NEXT_PUBLIC_EMAIL_SERVICE_ID=<your_service_id>
NEXT_PUBLIC_EMAIL_API_KEY=<your_api_key>
NEXT_PUBLIC_TICKET_CREATION_ID=<template_id>
NEXT_PUBLIC_NEW_MESSAGE_ID=<template_id>
Enter fullscreen mode Exit fullscreen mode

Create the templates for both cases and send your customers the email notifications.

Check here for more guidance

The Wrap Up

The source code for the application is available here. Feel free to check it out

Open to work🙂

Did you enjoy this article or need an experienced React Technical Writer & Developer for a remote, full-time, or contract-based role? Feel free to contact me.
GitHub || LinkedIn || Twitter

Buy David a coffee
Thank You

Top comments (11)

Collapse
 
tesfamariamtes4 profile image
Tesfamariam Teshome

Nice ❤️ It

Collapse
 
arshadayvid profile image
David Asaolu

😍😍

Collapse
 
combarnea profile image
Tomer Barnea • Edited

Looks amazing, thanks for the detailed guide!

Collapse
 
arshadayvid profile image
David Asaolu

You're welcome! 😎

Collapse
 
unicodeveloper profile image
Prosper Otemuyiwa

This is really good. Awesome guide!

Collapse
 
arshadayvid profile image
David Asaolu

Thank you. 😊

Collapse
 
nevodavid profile image
Nevo David

David ❤️❤️❤️

Collapse
 
arshadayvid profile image
David Asaolu

❤️❤️❤️

Collapse
 
raotaohub profile image
raotaohub

thinks ,very detailed! 😁

Collapse
 
arshadayvid profile image
David Asaolu

You're welcome! 😎

Collapse
 
binoyanto profile image
Binoy Anto

Hi, Nice Tutorial , would like to test it, can you please share Admin User Name , Password for Online Demo