Create a Picture Competition Website in Express JS, MEVN

Comments on Competition

Real-time Comments
Real-time Comments

Now we will add functionality that will allow the users to post a comment on competition. The user must be logged in to post a comment.

Add Comment

The first step is adding a comment. Then we will display all the comments. Goto your “competition-detail.ejs” file and replace the following code at the [add a comment] section:

<div class="row">
	<div class="offset-md-3 col-md-6">

		<div v-if="navApp.login">
    		<h2 class="text-center" style="margin-bottom: 20px;">Post a Comment</h2>

    		<form method="POST" v-bind:action="baseUrl + '/doComment'" v-on:submit.prevent="doComment" style="display: contents;">
			    <input type="hidden" name="_id" v-model="id" required />
			 
			    <div class="form-group">
			        <label>Comment</label>
			        <textarea name="comment" class="form-control" required></textarea>
			    </div>
			 
			    <input type="submit" value="Add Comment" name="submit" class="btn btn-info" />
			</form>
		</div>

		<div v-if="!navApp.login">
			<p class="text-center">Please login to post a comment.</p>
		</div>
	</div>
</div>

This will create a form with a hidden input field that contains the ID of the competition. Also, it has a textbox where the user can write their own comment about the competition. And a submit button.

And if the user tries to access this page without being logged in, then we will display an error message that he needs to log in to post a comment.

After that, you need to create the following method in your “competitionDetailApp” object. This function will an AJAX to the server to add the comment from the textbox in that competition’s collection.

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

	// disable the submit button and show "Loading..." text
    form.submit.setAttribute("disabled", "disabled");
    form.submit.value = "Loading...";

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

	myApp.callAjax(form.getAttribute("action"), formData, function (response) {
        // convert the JSON string into Javascript object
        var response = JSON.parse(response);
        // console.log(response);

        // enable the submit button
        form.submit.removeAttribute("disabled");
        form.submit.value = "Add Comment";

        // if the user is created, then redirect to login
        if (response.status == "success") {
        	self.competition.comments.push(response.comment);
        } else {
        	swal("Error", response.message, "error");
        }
	});
},

After the comment has been posted, we will push the new comment in the local “comments” array. This will help us while we are displaying the comments (later in this tutorial). So now we need to create a POST route in “server.js” that will add the comment in the nested array of the competition’s document in Mongo DB.

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

	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 competition = await db.collection("competitions").findOne({
        "_id": ObjectId(_id)
    });
    if (competition == null) {
    	result.json({
            "status": "error",
            "message": "Competition not found."
        });
        return false;
    }

    var commentObject = {
    	"_id": ObjectId(),
    	"comment": comment,
    	"user": {
    		"_id": user._id,
    		"name": user.name,
    		"email": user.email
    	},
    	"createdAt": new Date().getTime()
    };

    await db.collection("competitions").findOneAndUpdate({
    	"_id": competition._id
    }, {
    	$push: {
    		"comments": commentObject
    	}
    });

    result.json({
        "status": "success",
        "message": "Comment has been posted.",
        "comment": commentObject
    });
});

Refresh the page now and try to post a comment. But when you refresh the page again, it will be gone. You can still see it in your database using Mongo DB Compass. But we need to display all the comments when the page loads.

Display all Comments

To display all comments, first, we need to create a months array in our “header.ejs”. Because we will display a proper month name when the comment was posted.

var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

Then we need to create a method in our “competitionDetailApp” object inside “competition-detail.ejs” file. That method will convert the date from timestamp to readable date format using Javascript and return it as a string.

getDateInFormat: function(timestamp) {
	var date = new Date(timestamp);
	var dateStr = "";
	dateStr += date.getDate() + ", " + months[date.getMonth()] + " " + date.getFullYear();
	dateStr += " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();
	return dateStr;
},

We are already getting all the comments from the competition object. So no need to call another AJAX request to fetch the comments. We just need to display them. So the following code goes in the [all comments] section:

<div class="row">
	<div class="col-md-12">
		  <h2 class="text-center">All Comments</h2>

	  <ul class="media-list" id="comments" v-if="competition != null">
        <li class="media comment" v-for="(comment, index) in competition.comments">
            <a class="pull-left" href="#" style="margin-right: 20px;">
              <img class="media-object img-circle" src="https://via.placeholder.com/100" alt="profile" style="border-radius: 50%;" />
            </a>
            <div class="media-body">
              	<div class="well well-lg">
                  	<h4 class="media-heading text-uppercase reviews">
                  		{{ comment.user.name }}

                  		<span style="font-size: 14px; font-weight: normal; text-transform: none; float: right;">{{ getDateInFormat(comment.createdAt) }}</span>
                  	</h4>

                  	<p class="media-comment">
                    	{{ comment.comment }}
                  	</p>

                  	[delete button]
              	</div>              
            </div>
        </li>
      </ul>
	</div>
</div>

If you refresh the page now, you will be able to view all the comments posted on that competition.

Delete Comment

We need to allow the author of the comment to delete a comment. So first we need to add a trash can button with each comment. Replace the following code in the [delete button] section in “competition-detail.ejs”:

<form method="POST" v-bind:action="baseUrl + '/deleteComment'" v-on:submit.prevent="deleteComment" v-if="(navApp.login && comment.user._id == navApp.user._id)">
  <input type="hidden" name="_id" v-model="comment._id" required />
  <button type="submit" name="submit" class="btn btn-danger btn-sm text-uppercase">
    <span class="fa fa-trash"></span>
    <span class="text">Delete</span>
  </button>
</form>

This will create a form which when submit will call an AJAX. Now we need to create a method in our “competitionDetailApp” object.

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

  // disable the submit button and show "Loading..." text
  form.submit.setAttribute("disabled", "disabled");
  form.submit.querySelector(".text").innerHTML = "Loading...";

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

  myApp.callAjax(form.getAttribute("action"), formData, function (response) {
    // convert the JSON string into Javascript object
    var response = JSON.parse(response);
    // console.log(response);

    // enable the submit button
    form.submit.removeAttribute("disabled");
    form.submit.querySelector(".text").innerHTML = "Delete";

    // if the user is created, then redirect to login
    if (response.status == "success") {
      var comment = null;
      for (var a = 0; a < self.competition.comments.length; a++) {
      comment = self.competition.comments[a];
        if (comment._id == form._id.value) {
          self.competition.comments.splice(a, 1);
          break;
        }
      }
    } else {
      swal("Error", response.message, "error");
    }
  });
},

It will call an AJAX request to the server to delete the comment. And when the response is received from the server, it will remove that comment from the local array too. Now we need to create an API in “server.js” to actually delete the comment from the competition’s nested array.

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

  // check if user is logged in
  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;
  }

  // check if comment is added by logged in user
  var competition = await db.collection("competitions").findOne({
      $and: [{
        "comments._id": ObjectId(_id)
      }, {
        "comments.user._id": user._id
      }]
  });
  if (competition == null) {
    result.json({
        "status": "error",
        "message": "Comment not found."
    });
    return false;
  }

  // remove the comment from array
  await db.collection("competitions").findOneAndUpdate({
    "_id": competition._id
  }, {
    $pull: {
      "comments": {
        "_id": ObjectId(_id)
      }
    }
  });

  // send the response back to client (AJAX)
  result.json({
      "status": "success",
      "message": "Comment has been removed."
  });
});

Refresh the page now and you will see a delete button to your posted comment. On clicking will remove the comment from the database and also from the local array.