Create a Picture Competition Website in Express JS, MEVN

Create Competition

Create Competition
Create Competition

So the authentication part is completed. Now we come to the part i.e. “competitions”. First, we need to add an ability for the authenticated user to create competition. So in your “home.ejs” create a link that will redirect the user to the create competition page:

<div class="container margin-container" id="competitionApp">
	<div class="row" style="margin-top: 20px;">
		<div class="col-md-3" style="margin-bottom: 20px;">
			<a v-bind:href="baseUrl + '/createCompetition'" class="btn btn-primary" style="position: relative; top: 50%; color: white;">Create Competition</a>
		</div>

        [sort goes here]
	</div>
</div>

Now we need to initialize this in our “footer.ejs” file:

var competitionApp = new Vue({
	el: "#competitionApp",
	data: {
		baseUrl: mainURL
	}
});

This will create a link to create a competition route.

Create Competition Form

After that, we need to create a GET route in “server.js” that will render the page.

app.route("/createCompetition")
	.get(function (request, result) {
		result.render("create-competition");
	});

Then create a new file named “create-competition.ejs” inside “views” folder. It will have the following content:

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

<style>
	.img-preview {
		width: 500px;
    	margin-top: 10px;
	}
</style>

<div class="container margin-container" id="createCompetitionApp">
	<div class="row" style="margin-bottom: 50px;">
		<div class="col-md-12">
			<h1 class="text-center">Create Competition</h1>
		</div>
	</div>

	<form method="POST" v-bind:action="baseUrl + '/createCompetition'" style="display: contents;" v-on:submit.prevent="createCompetition">

		<div class="row">
			<div class="col-md-6">
				<h2 class="text-center">User 1</h2>

				<div class="form-group">
					<label>Enter name</label>
					<input type="text" name="user_1_name" class="form-control" required />
				</div>

				<div class="form-group">
					<label>Select picture</label>
					<input type="file" name="user_1_picture" accept="image/*" class="form-control" v-on:change="previewImage1" required />

					<img v-bind:src="image1Preview" v-if="image1Preview != ''" class="img-preview" />
				</div>
			</div>

			<div class="col-md-6">
				<h2 class="text-center">User 2</h2>

				<div class="form-group">
					<label>Enter name</label>
					<input type="text" name="user_2_name" class="form-control" v-on:change="previewImage2" required />
				</div>

				<div class="form-group">
					<label>Select picture</label>
					<input type="file" name="user_2_picture" accept="image/*" class="form-control" v-on:change="previewImage2" required />

					<img v-bind:src="image2Preview" v-if="image2Preview != ''" class="img-preview" />
				</div>
			</div>
		</div>

		<div class="row">
			<div class="col-md-6">
				<div class="form-group">
					<label>Hashtags <i>(separate by spaces)</i></label>
					<textarea name="tags" class="form-control" rows="5"></textarea>
				</div>
			</div>
		</div>

		<input type="submit" name="submit" value="Create Competition" class="btn btn-primary" />
	</form>
</div>

<script>
	var createCompetitionApp = new Vue({
		el: "#createCompetitionApp",
		data: {
			baseUrl: mainURL,
			image1Preview: "",
			image2Preview: ""
		},
		methods: {
			previewImage2: function() {
				var self = this;
		        var file = event.target.files;
		        if (file.length > 0) {
		            var fileReader = new FileReader();
		 
		            fileReader.onload = function (event) {
		                self.image2Preview = event.target.result;
		            };
		 
		            fileReader.readAsDataURL(file[0]);
		        }
		    },

			previewImage1: function() {
				var self = this;
		        var file = event.target.files;
		        if (file.length > 0) {
		            var fileReader = new FileReader();
		 
		            fileReader.onload = function (event) {
		                self.image1Preview = event.target.result;
		            };
		 
		            fileReader.readAsDataURL(file[0]);
		        }
		    },

			createCompetition: function () {
				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 = "Create Competition";
 
                    // if the user is created, then redirect to login
                    if (response.status == "success") {
                    	swal("Success", response.message, "success");
                    	form.reset();
                    } else {
                    	swal("Error", response.message, "error");
                    }
				});
			}
		}
	});
</script>

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

It takes input from users for 2 user’s names and pictures separately along with hashtags (optional). User names and pictures will be displayed to the users while hashtags are used to search the competition by user name or hashtag keywords.

On clicking submit will call an AJAX. So we need to create a POST route in “server.js” to actually create the competition in Mongo DB.

Save Competition in Database

As this feature requires uploading a file, so we need to install an NPM module named “file system”. Also, we will need the “request” module because we will be calling an HTTP request from the server-side too. We need to validate that the uploaded picture must not be an adult image. You can install these modules by running the following command:

npm install fs request

After that, you need to include these on your “server.js” file:

const fileSystem = require("fs");
var requestModule = require("request");

The uploaded pictures go in a separate folder. So, create a new folder named “uploads” at the root of your project and write the following line in your “server.js”:

app.use(express.static(__dirname + "/uploads"));

To do the adult image validation, we will be using a PHP function. So you need to download and install XAMPP or WAMP. Create a new folder named “picture-competition-nodejs-mongodb-vuejs” inside your “htdocs” folder. Then we need to create a new file named “class.ImageFilter.php” inside that newly created folder.

Following will be the content of “class.ImageFilter.php” file:

<?php

class ImageFilter
{                              #R  G  B
    var $colorA = 7944996;     #79 3B 24
    var $colorB = 16696767;    #FE C5 BF


    var $arA = array();
    var $arB = array();
    
    function ImageFilter()
    {
        $this->arA['R'] = ($this->colorA >> 16) & 0xFF;
        $this->arA['G'] = ($this->colorA >> 8) & 0xFF;
        $this->arA['B'] = $this->colorA & 0xFF;
        
        $this->arB['R'] = ($this->colorB >> 16) & 0xFF;
        $this->arB['G'] = ($this->colorB >> 8) & 0xFF;
        $this->arB['B'] = $this->colorB & 0xFF;
    }
    
    function GetScore($image)
    {
        $x = 0; $y = 0;
        $img = $this->_GetImageResource($image, $x, $y);
        if(!$img) return false;

        $score = 0;
        
        $xPoints = array($x/8, $x/4, ($x/8 + $x/4), $x-($x/8 + $x/4), $x-($x/4), $x-($x/8));
        $yPoints = array($y/8, $y/4, ($y/8 + $y/4), $y-($y/8 + $y/4), $y-($y/8), $y-($y/8));
        $zPoints = array($xPoints[2], $yPoints[1], $xPoints[3], $y);

        
        for($i=0; $i<=$x; $i++)
        {
            for($j=0; $j<=$y; $j++)
            {
                $color = imagecolorat($img, $i, $j);
                if($color >= $this->colorA && $color <= $this->colorB)
                {
                    $color = array('R'=> ($color >> 16) & 0xFF, 'G'=> ($color >> 8) & 0xFF, 'B'=> $color & 0xFF);
                    if($color['G'] >= $this->arA['G'] && $color['G'] <= $this->arB['G'] && $color['B'] >= $this->arA['B'] && $color['B'] <= $this->arB['B'])
                    {
                        if($i >= $zPoints[0] && $j >= $zPoints[1] && $i <= $zPoints[2] && $j <= $zPoints[3])
                        {
                            $score += 3;
                        }
                        elseif($i <= $xPoints[0] || $i >=$xPoints[5] || $j <= $yPoints[0] || $j >= $yPoints[5])
                        {
                            $score += 0.10;
                        }
                        elseif($i <= $xPoints[0] || $i >=$xPoints[4] || $j <= $yPoints[0] || $j >= $yPoints[4])
                        {
                            $score += 0.40;
                        }
                        else
                        {
                            $score += 1.50;
                        }
                    }
                }
            }
        }
        
        imagedestroy($img);
        
        $score = sprintf('%01.2f', ($score * 100) / ($x * $y));
        if($score > 100) $score = 100;
        return $score;
    }
    
    function GetScoreAndFill($image, $outputImage)
    {
        $x = 0; $y = 0;
        $img = $this->_GetImageResource($image, $x, $y);
        if(!$img) return false;

        $score = 0;

        $xPoints = array($x/8, $x/4, ($x/8 + $x/4), $x-($x/8 + $x/4), $x-($x/4), $x-($x/8));
        $yPoints = array($y/8, $y/4, ($y/8 + $y/4), $y-($y/8 + $y/4), $y-($y/8), $y-($y/8));
        $zPoints = array($xPoints[2], $yPoints[1], $xPoints[3], $y);


        for($i=1; $i<=$x; $i++)
        {
            for($j=1; $j<=$y; $j++)
            {
                $color = imagecolorat($img, $i, $j);
                if($color >= $this->colorA && $color <= $this->colorB)
                {
                    $color = array('R'=> ($color >> 16) & 0xFF, 'G'=> ($color >> 8) & 0xFF, 'B'=> $color & 0xFF);
                    if($color['G'] >= $this->arA['G'] && $color['G'] <= $this->arB['G'] && $color['B'] >= $this->arA['B'] && $color['B'] <= $this->arB['B'])
                    {
                        if($i >= $zPoints[0] && $j >= $zPoints[1] && $i <= $zPoints[2] && $j <= $zPoints[3])
                        {
                            $score += 3;
                            imagefill($img, $i, $j, 16711680);
                        }
                        elseif($i <= $xPoints[0] || $i >=$xPoints[5] || $j <= $yPoints[0] || $j >= $yPoints[5])
                        {
                            $score += 0.10;
                            imagefill($img, $i, $j, 14540253);
                        }
                        elseif($i <= $xPoints[0] || $i >=$xPoints[4] || $j <= $yPoints[0] || $j >= $yPoints[4])
                        {
                            $score += 0.40;
                            imagefill($img, $i, $j, 16514887);
                        }
                        else
                        {
                            $score += 1.50;
                            imagefill($img, $i, $j, 512);
                        }
                    }
                }
            }
        }
        imagejpeg($img, $outputImage);

        imagedestroy($img);

        $score = sprintf('%01.2f', ($score * 100) / ($x * $y));
        if($score > 100) $score = 100;
        return $score;
    }
    
    function _GetImageResource($image, &$x, &$y)
    {
        $info = GetImageSize($image);
        
        $x = $info[0];
        $y = $info[1];
        
        switch( $info[2] )
        {
            case IMAGETYPE_GIF:
                return @ImageCreateFromGif($image);
                
            case IMAGETYPE_JPEG:
                return @ImageCreateFromJpeg($image);
                
            case IMAGETYPE_PNG:
                return @ImageCreateFromPng($image);
                
            default:
                return false;
        }
    }
}

if (isset($_POST["validate_image"]))
{

    $data = $_POST["base_64"];
    $filename = "test.png";

    list($type, $data) = explode(';', $data);
    list(, $data)      = explode(',', $data);
    $data = base64_decode($data);

    file_put_contents($filename, $data);

    $filter = new ImageFilter();
    $score = $filter->GetScore($filename);

    unlink($filename);

    echo $score;
    exit();
}

?>

Then we need to set it’s path in our “server.js” file:

var imageFilterApiPath = "http://127.0.0.1/picture-competition-nodejs-mongodb-vuejs/class.ImageFilter.php";

After that, we need to create a POST route that will save the competition in Mongo DB:

app.route("/createCompetition")
    .get(function (request, result) {
        result.render("create-competition");
    })
    .post(async function (request, result) {
        const accessToken = request.fields.accessToken;
        const createdAt = new Date().getTime();
         
        const user_1_name = request.fields.user_1_name;
        const user_1_picture = request.files.user_1_picture;
         
        const user_2_name = request.fields.user_2_name;
        const user_2_picture = request.files.user_2_picture;
 
        const tags = request.fields.tags;
 
        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;
        }
 
        if (user_1_picture.type.includes("image") && user_2_picture.type.includes("image")) {
            const user1Picture = "images/" + (new Date().getTime()) + "-" + user_1_picture.name;
            const user2Picture = "images/" + (new Date().getTime()) + "-" + user_2_picture.name;
 
            var base64 = await fileSystem.promises.readFile(user_1_picture.path, {encoding: 'base64'});
            base64 = "data:image/png;base64," + base64;
 
            requestModule.post(imageFilterApiPath, {
                formData: {
                    "validate_image": 1,
                    "base_64": base64
                }
            }, async function(err, res, body) {
                if (!err && res.statusCode === 200) {
                    // console.log("body = " + body);
 
                    if (body > 60) {
                        result.json({
                            "status": "error",
                            "message": "Image 1 contains nudity."
                        });
 
                        return false;
                    } else {
                        var base64 = await fileSystem.promises.readFile(user_2_picture.path, {encoding: 'base64'});
                        base64 = "data:image/png;base64," + base64;
 
                        requestModule.post(imageFilterApiPath, {
                            formData: {
                                "validate_image": 1,
                                "base_64": base64
                            }
                        }, function(err, res, body) {
                            if (!err && res.statusCode === 200) {
                                // console.log("body = " + body);
 
                                if (body > 60) {
                                    result.json({
                                        "status": "error",
                                        "message": "Image 2 contains nudity."
                                    });
 
                                    return false;
                                } else {
                                    fileSystem.readFile(user_1_picture.path, function (error, data) {
	                                    fileSystem.writeFile("public/" + user1Picture, data, function (error) {
	 
	                                        fileSystem.readFile(user_2_picture.path, function (error, data) {
	                                            fileSystem.writeFile("public/" + user2Picture, data, async function (error) {
	                                                const data = {
	                                                    "user1": {
	                                                        "name": user_1_name,
	                                                        "picture": user1Picture,
	                                                        "voters": []
	                                                    },
	                                                    "user2": {
	                                                        "name": user_2_name,
	                                                        "picture": user2Picture,
	                                                        "voters": []
	                                                    },
	                                                    "createdBy": {
	                                                        "_id": user._id,
	                                                        "name": user.name,
	                                                        "email": user.email
	                                                    },
	                                                    "comments": [],
	                                                    "tags": tags,
	                                                    "createdAt": createdAt
	                                                };
	                                                await db.collection("competitions").insertOne(data);
	 
	                                                result.json({
	                                                    "status": "success",
	                                                    "message": "Competition has been created.",
	                                                    "competition": data
	                                                });
	                                            });
	 
	                                            fileSystem.unlink(user_2_picture.path, function (error) {
	                                                // 
	                                            });
	                                        });
	 
	                                    });
	                                    fileSystem.unlink(user_1_picture.path, function (error) {
	                                        // 
	                                    });
                                	});
                                }
                            }
                        });
                    }
                }
            });
        }
    });

It will do the adult image validation on both user’s pictures. It will save the images in “uploads” folder.