Building a Venue Booking System using Parse and SashiDo : Part-1

nishkakotian

Nishka Kotian

Posted on August 5, 2021

Building a Venue Booking System using Parse and SashiDo : Part-1

I got to know about Parse some time ago and was keen to build a full-stack web application using it.So, I decided to make a venue booking system and I would like to describe how I went about coding it.

In the first part of this post, I've given a brief introduction to Parse Server, SashiDo and javascript promises.If you're already familiar with these, please skip it.

To get an understanding of how the application works, check out the demo video!

Table Of Contents

Parse Server and SashiDo

Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js.You can set up your own Parse server and dashboard or use a service like SashiDo that hosts Parse for you.Parse has SDKs for various platforms but for this project I have used Javscript.

To connect your application to SashiDo go to the Getting started section in the dashboard and copy the code under the Javascript section and place it at the top of your JS file.
Connect to SashiDo

To integrate the project with the SDK include the following script.

<script src="https://unpkg.com/parse/dist/parse.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

Javscript Promises

A promise is a proxy for a value which may not be know at the time of its creation but will eventually be available.It allows asynchronous functions to return values similar to synchronous ones.So, instead of returning the actual value, a promise is returned.When a promise is created it will be in pending state and ends in either a resolved state by returning a value or in a rejected state by returning an error.The .then() method of a promise object takes two arguments, the first argument is a callback function for the resolved case and the second argument is a callback function for the rejected case.As you go along this tutorial you'll learn more about it.

Project overview

The venue booking application which I named SKED has two main types of users, venue owners and customers.

A customer can :

  • Find details about venues and filter them based on location
  • Check time slots that have already been booked for any day
  • Send a booking request to the owner of that venue

An owner can :

  • Add a venue
  • Get bookings made for the present day
  • Approve booking requests made by customers
  • Delete bookings for events that have finished

Database

Data is stored by creating a Parse.Object with key-value pairs.You can create classes to manage and store different kinds of data.When a class is created, it does not have a schema defined so you can add fields which can have any type of JSON compatible data.But once the first object is saved the data types for any fields that have been set will be locked.For example, if I create a class named Venues and my first object has a field venueName whose valued I set to "Auditorium" then parse recognises that it was of string type.So whenever a new object is added it checks whether the venueName is of string type.If not, it returns an error.

Below is a diagram I created so that you can get a quick idea about the various classes and fields in them.
Schema diagram

There is another class named Role created by default in parse, but since I did not create roles for this project I haven't shown it.Also, I did not put any thought into naming the classes 🤦‍♀️.I really should've named all of them in singular form.

User Registeration and Login

The Parse.User class has built in functions to handle and secure user account information.As there are two types of users I've added a new field called userType to differentiate between them.New fields can be added using the dashboard by clicking on the Add a new column button.

For the frontend, I've created two separate home pages for customers and owners as I wanted to display different messages on them.In each of them there are two buttons ie.Register and Login.On clicking them a modal(bootstrap popup) opens up asking the user to enter the username and password.These two are mandatory.You could optionally ask email too.

<form id="registerform">    
    <label for="username_reg" class="form-label">Username</label>
    <input id="username_reg" name="username_r" class="form-control mb-2" type="text">
    <label for="pswd_reg" class="form-label">Password</label>
     <input id="pswd_reg" class="form-control" name="password_r" type="password">
    <div id="regError" class="mt-2 text-danger"></div>
    <button type="button" id="CustRegbtn" onclick="register(this)" class="btn bg-light-blue text-white my-2">Register</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Customer login

I've created a similar form in the owner's homepage but with a different id for the Register button so as to be able to determine if the request was made by a customer or owner.

Function to register user :

function register(el) {
    const username = document.getElementById("username_reg").value;
    const password = document.getElementById("pswd_reg").value;

    var user_type;
    if (el.id == "OwnerRegbtn") {
        user_type = "Owner";
    }
    else {
        user_type = "Customer";
    }

    if (username.length == 0) {
        document.getElementById("regError").innerHTML = "Username cannot be empty";
    }
    else if (password.length < 8 || password.length > 16) {
        document.getElementById("regError").innerHTML = "Password must be between 8-16 characters long";
    }
    else {
        const user = new Parse.User();
        user.set("username", username);
        user.set("password", password);
        user.set("userType", user_type);

        user.signUp().then(function success() {
            window.location.href = "registered.html";
        }, function error(err) {
            document.getElementById("regError").innerHTML = err.message;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

new Parse.User() will create a new instance of the User class.After creating a new object, you can set the values for its fields using .set which takes two parameters, the name of the column and the value that you want to set it to.To register an user, the .signUp method has to be used.It is an asynchronous function, which returns a promise object which has a .then method that takes two functions for the success and error case.One example for an error case is when the username is already taken by some other user.

Now let's look into the login part.

<form id="loginform">
    <label for="username_login" class="form-label">Username</label>
    <input id="username_login" name="username_l" class="form-control mb-2" type="text">

    <label for="pswd_login" class="form-label">Password</label>
    <input id="pswd_login" class="form-control" name="password_l" type="password">

    <div id="loginError" class="mt-2 text-danger"></div>
    <button type="button" id="CustLoginbtn" onclick="login()"
        class="btn bg-light-blue text-white my-2">Login</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Function to log in user :

function login() {
    var username = document.getElementById("username_login").value;
    var password = document.getElementById("pswd_login").value;

    if (username.length == 0) {
        document.getElementById("loginError").innerHTML = "Please enter the username";
    }
    else if (password.length < 8 || password.length > 16) {
        document.getElementById("loginError").innerHTML = "Passwords are between 8-16 characters long.";
    }
    else {
        Parse.User.logIn(username, password, { usePost: true }).then(function success() {
            const user = Parse.User.current();
            if (user.attributes.userType == "Owner") {
                window.location.href = "owner.html";
            }
            else { /*user.attributes.userType == "Customer"*/
                window.location.href = "customer.html";
            }
        }, function error(err) {
            document.getElementById("loginError").innerHTML = err.message;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

To log in an user, retrieve the input entered in the form and use the .logIn method by passing the username and password.By default it uses GET request but you can add an optional argument to tell Parse to use POST instead.Once the user is logged in, you can use Parse.User.current() to find out the current user.Then, using the attributes property of the object we can find the userType. Alternatively the .get method can also be used like so - user.get("userType").

If the login was successful the user will be taken to their dashboard (owner.html or customer.html) which has all their data.

To provide log out functionality use the .logOut() method.

/*Passing a boolean value true for owner & false for customer 
so that after logout they can be taken to their respective home page */

<button class="btn nav-link btn-link" onclick="logout(true)">Sign out</button> //in owner.html

<button class="btn nav-link btn-link" onclick="logout(false)">Sign out</button> //in customer.html
Enter fullscreen mode Exit fullscreen mode
function logout(isOwner) {
    Parse.User.logOut().then(function gotohome() {
        if (isOwner) {
            window.location.href = "home.html";
        }
        else {
            window.location.href = "home_customer.html";
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Next, we'll be looking into how owners can add a new venue.

Adding a venue

I have created a class named Venues to store venue details.This can be done using the dashboard.Just click on the Create a class button and add all the required columns by clicking on Add a new column.You'll have to give the column a name and specify the type of data you want to store.

dashboard in parse server

If you remember I mentioned that Parse will automatically find the data type from the first object that gets stored and now I'm asking you to specify the data type.What's happening here?

Well, creating a class before hand isn't really necessary in Parse.If you create a subclass using Parse.Object.extend ("class"); and if that class did not exist Parse will create it for you.To enable this feature you'll have to go to App Settings > Security and keys > App permissions and enable client class creation.You can utilise it all through development and disable it before moving to production.

In the owner's dashboard there is a Add a venue button which on clicking opens up a form in which details about the venue must be entered.

Add venue form

Here's an outline of the code for the form.

<form>
    <div class="mb-3">
        <label for="nameOfVenue" class="form-label">Name of the venue</label>
        <input type="text" id="nameOfVenue" name="venueName" class="form-control">
    </div>

    ...

    /* Insert label and input fields for all other details here*/

    ...

    <div id="addVenueError" class="mb-3 text-danger"></div>
    <button type="button" onclick="createVenue()" id="venueSubmitBtn"
        class="btn text-light mb-3">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Once the submit button is clicked, the createVenue function shown below creates a new Venue object containing all the details that were entered.The venue will then show up in the owner's dashboard and also be visible to customers.

function createVenue() {
    document.getElementById("addVenueError").innerHTML = "";
    const venuename = document.getElementById("nameOfVenue").value;
    const address = document.getElementById("addr").value;
    const city = document.getElementById("cityName").value.toLowerCase();
    const daysAvailable = document.getElementById("days").value;
    const topen = document.getElementById("topen").value; /*Venue opening time*/
    const tclose = document.getElementById("tclose").value; /*Venue closing time*/
    const timing = topen + "-" + tclose;

    const image1 = document.getElementById("image1");
    const image2 = document.getElementById("image2");

    const desc = document.getElementById("desc").value;

    //Client side validation to check that all fields are entered
    if (!venuename || !address || !city || !daysAvailable || !topen || !tclose || image1.files.length == 0 || image2.files.length == 0 || !desc) {
        document.getElementById("addVenueError").innerHTML = "Please fill all the fields.";
    }
    else {
        const parseFileImg1 = new Parse.File("img1.jpeg", image1.files[0]);
        const parseFileImg2 = new Parse.File("img2.jpeg", image2.files[0]);

        const owner = Parse.User.current();
        /*create a subclass of the Venues class ie. inherit the properties of the Venues class.*/
        const Venue = Parse.Object.extend("Venues");
        //create an instance of the Venues class
        const venue = new Venue();

        var acl = new Parse.ACL();
        acl.setPublicReadAccess(true);
        acl.setWriteAccess(owner.id, true);

        venue.setACL(acl);
        venue.set("owner", owner); //pointer to owner
        venue.set("venueName", venuename);
        venue.set("address", address);
        venue.set("city", city);
        venue.set("daysAvailable", daysAvailable);
        venue.set("timings", timing);
        venue.set("image1", parseFileImg1);
        venue.set("image2", parseFileImg2);
        venue.set("description", desc);

        venue.save().then(function success(venue) {
            const displayArea = document.getElementById("displayVenues");
            displayVenue(displayArea, venue);
            i += 1;
            if (i == 11) { i = 0; }
            location.reload();
        }, function error(err) {
            alert("Error adding venue : " + err.message);
        });
    }

};
Enter fullscreen mode Exit fullscreen mode

Let's go over what this function is doing.First, I'm retrieving the values entered in the form and checking that no fields were left empty.Then, the images are stored in a Parse.File object which allows storing data that is too large to fit inside a normal Parse.Object. Finally, after setting the fields to their values the .save() method is used to save the object in the database and like I stated before, if the Venues class did not exist Parse will first create it and then save the object.The displayVenue function will just add a new card to display the venue in the owner's dashboard.More about this function can be found in part-2.

One important point to note here is that we need to make sure that only the owner can modify or delete the venue.In order to provide such fine grained security we need to set an ACL (ACL = Access control list) using which we can specify who have permissions to read or write to that particular object. setpublicReadAccess(true) as the name suggests means that any user can read that object and setWriteAccess(owner.id, true) implies that only the owner has write access.The boolean value true specifies that I want to give permission.If instead I wanted to deny access to a user, then I would set that parameter to false.

Conclusion

So far, we have looked at user authentication and adding a new venue.If you would like to learn about querying the database and adding the booking functionality please check part-2 which can be found here.

Useful links

Part 2 - Building a Venue Booking System with Parse and SashiDo - Part-2
Github repo - https://github.com/nishkakotian/SKED
SashiDo - https://www.sashido.io/en/
Parse Javascript SDK documentation - https://docs.parseplatform.org/js/guide/

💖 💪 🙅 🚩
nishkakotian
Nishka Kotian

Posted on August 5, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related