How to Build a Venue Booking App with SashiDo - Part 1

Nishka Kotian

A few months ago I got to know about Parse and SashiDo during a hackathon and I was very keen to build a full-stack web application using it. So, I decided to make a fun project by building a venue booking system. In the following lines I will describe how I went about coding it so you can follow allong the steps. A basic knowlege of Javascript would suffice to follow this tutorial.

To get an understanding of what comes next and how the application works, check out the demo video!

Table Of Contents

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 value 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.

sked_db_classes

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 and surely that would be a lesson learned for my next projects.

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 i.e. 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 for an 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>

sked_RegisterModal

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;
        });
    }
}

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. The .signUp method has to be used to register a new user. It is an asynchronous function, which returns a promise object which has a .then method that takes two functions for the success and error cases. One example of an error case is when the username is already taken by some other user.

Now, let's a 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>

Function to login 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;
        });
    }
}

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
function logout(isOwner) {
    Parse.User.logOut().then(function gotohome() {
        if (isOwner) {
            window.location.href = "home.html";
        }
        else {
            window.location.href = "home_customer.html";
        }
    });
}

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.

addven

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 beforehand 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 SashiDo Dashbaord > 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.

sked_AddVenueForm

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>

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);
        });
    }

};

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 to 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 of the tutorial.

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 of the tutorial.

Useful links

How to Build a Venue Booking App with SashiDo - Part 2
Github repo
Application Demo Video
Parse Javascript SDK documentation
SashiDo's Getting Started Guide

Nishka Kotian

College student exploring computer science

Find answers to all your questions

Our Frequently Asked Questions section is here to help.