Create a Picture Competition Website in Express JS, MEVN

User Notifications

Notifications
Notifications

We will add functionality where users get notified if their competition was deleted from the admin panel. Although the creation of notification is in the admin panel section. In this section, we will discuss how it will be displayed to the user.

Right now, you can add the notification in the user’s document from Mongo DB Compass software directly.

Sample notification object:

{
	"_id": ObjectId(),
	"message": "Any message",
	"isRead": false,
	"createdAt": 1640014325
}

First, go to modules/functions.js and add the following method in it:

getUserWithNotificationsCount: function (user) {
	if (user.notifications !== "undefined") {
		var total = 0;
		user.notifications.forEach(function (notification) {
			if (!notification.isRead) {
				total++;
			}
		});
		user.totalNotifications = total;
	}
	return user;
},

Then we need to call this method in our server.js inside the “/getUser” API.

user = functions.getUserWithNotificationsCount(user);

Now we will be receiving totalNotifications variable along with the user object. Now we need to display it. Replace the [notifications layout goes here] section in your header.ejs to the following:

<li v-if="login" class="nav-item dropdown">
	<a class="nav-link" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
		<i class="fa fa-bell"></i>
		<span class="badge badge-secondary" style="position: relative; bottom: 10px; right: 36px;" v-text="user.totalNotifications"></span>
	</a>

	<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
		<a v-for="(notification, index) in user.notifications" v-bind:class="'dropdown-item' + (notification.isRead ? ' ' : ' active')" v-bind:href="baseUrl + '/notification/' + notification._id" v-text="notification.message"></a>

		<div class="dropdown-divider"></div>

		<a v-bind:href="baseUrl + '/notifications'" class="dropdown-item text-center">Show all notifications</a>
	</div>
</li>

This will show the total number of unread notifications in a Bootstrap badge. Then go to the footer.ejs and replace the [notifications are set here] section with the following code:

self.user.notifications = self.user.notifications.reverse();
self.user.notifications = self.user.notifications.slice(0, 5);

This will set the notifications in reverse order. Also, it will make sure that you only got to see 5 notifications in the dropdown. To see all the notifications, we are adding a link at the bottom of this dropdown that says “Show all notifications”.

So we need to create its GET route in server.js that will display the notification page.

app.get("/notifications", function (request, result) {
	result.render("notifications");
});

After that, create a file named notifications.ejs in your views folder. Following will be the code in this file:

<%- include ("includes/header") %>

<div class="container margin-container" id="notificationApp">
	<div class="row">
		<div class="col-md-12 text-center">
			<h2>Notifications</h2>
		</div>
	</div>

	<div class="row">
		<div class="offset-md-2 col-md-9">
			<table class="table table-bordered">
				<thead>
					<tr>
						<th>Message</th>
						<th>Actions</th>
					</tr>
				</thead>

				<tbody>
					<tr v-for="(notification, index) in notifications">
						<td>{{ notification.message }}</td>
						<td>
							<form method="POST" v-on:submit.prevent="markAsRead" v-if="!notification.isRead">
								<input type="hidden" name="_id" v-model="notification._id.toString()" />
								<input type="submit" name="submit" class="btn btn-primary" value="Read" />
							</form>
						</td>
					</tr>
				</tbody>
			</table>
		</div>
	</div>
</div>

<script>
	const isNotificationsPage = true;

	function getNotifications() {
		var formData = new FormData();
		formData.append("accessToken", localStorage.getItem(accessTokenKey));

		myApp.callAjax(navApp.baseUrl + "/getNotifications", formData, function (response) {
            // convert the JSON string into Javascript object
            var response = JSON.parse(response);
            // console.log(response);

            // if the user is created, then redirect to login
            if (response.status == "success") {
            	notificationApp.notifications = response.notifications;
            } else {
            	swal("Error", response.message, "error");
            }
		});
	}

	var notificationApp = new Vue({
		el: "#notificationApp",
		data: {
			notifications: []
		},
		methods: {
			//
		}
	});
</script>

<%- include ("includes/footer") %>

To call this function, we need to add the following lines in the [logged in functions will be called here] section in footer.ejs:

if (typeof isNotificationsPage !== "undefined" && isNotificationsPage) {
	getNotifications();
}

It simply calls an AJAX and displays all the notifications in a table. It also creates a button to mark each notification as read. But we will come back to that later. First, we need to create an API that will fetch all the notifications of the logged-in user in descending order.

So create a POST route in server.js that will handle this request.

app.post("/getNotifications", async function (request, result) {
	const accessToken = request.fields.accessToken;

	var user = await db.collection("users").findOne({
        "accessToken": accessToken
    });
    if (user == null) {
    	result.json({
            "status": "error",
            "message": "User has been logged out. Please login again."
        });
        return false;
    }

    var notifications = user.notifications.reverse();

    result.json({
        "status": "success",
        "message": "Data has been fetched.",
        "notifications": notifications
    });
});

If you refresh the page now, you will be able to see all the notifications along with a button to mark them as read. So first create a method in notificationApp.methods object that will handle the form submission and call an AJAX to the server.

markAsRead: function () {
	var form = event.target;
	var self = this;

	var formData = new FormData(form);
	formData.append("accessToken", localStorage.getItem(accessTokenKey));

	myApp.callAjax(navApp.baseUrl + "/markNotificationAsRead", formData, function (response) {
        var response = JSON.parse(response);

        // if the user is created, then redirect to login
        if (response.status == "success") {
        	self.notifications.forEach(function (notification) {
        		if (notification._id.toString() == form._id.value) {
        			notification.isRead = true;
        		}
        	});

        	navApp.user.notifications.forEach(function (notification) {
        		if (notification._id.toString() == form._id.value) {
        			notification.isRead = true;
        		}
        	});

        	if (navApp.user.totalNotifications > 0) {
        		navApp.user.totalNotifications--;
        	}
        } else {
        	swal("Error", response.message, "error");
        }
	});
}

This will call an AJAX to the server to mark that notification as read. Also, it will decrement the notifications count from the top navbar. Moreover, it will set the isRead value from local array to true too. This will remove the “Mark as read” button automatically from the HTML table.

Now we need to create an API that will handle this request. So create a POST route in server.js that will mark the notification as read.

app.post("/markNotificationAsRead", async function (request, result) {
	const accessToken = request.fields.accessToken;
	const _id = request.fields._id;

	var user = await db.collection("users").findOne({
        $and: [{
        	"accessToken": accessToken
        }, {
        	"notifications._id": ObjectId(_id)
        }]
    });
    if (user == null) {
    	result.json({
            "status": "error",
            "message": "User has been logged out. Please login again."
        });
        return false;
    }

    await db.collection("users").findOneAndUpdate({
    	$and: [{
    		"_id": user._id
    	}, {
    		"notifications._id": ObjectId(_id)
    	}]
    }, {
    	$set: {
    		"notifications.$.isRead": true
    	}
    });

    result.json({
        "status": "success",
        "message": "Notification has been marked as read."
    });
});

Refresh the page now. And if you click on any notifications now, you will see that the button will be removed automatically. And the notification counter from the top navbar will be decremented too. Also, if you refresh the page again, that notification will no longer have the “Mark as read” button with it.