Hackernoon logoHow to Build Your Own Booking Platform with Velo by@brendanjohnson

How to Build Your Own Booking Platform with Velo

image
Brendan Johnson Hacker Noon profile picture

@brendanjohnsonBrendan Johnson

I am a certified Velo Developer building some cool projects

Hey everybody! I'm Brendan, a certified Velo Developer and after working with Wix's platform and Velo for over a year I have come to find that Wix has the ability to do so many things that you may not even think are possible.

A lot of the development with Velo requires thinking outside of the box in order to get the solution you want, however, just about anything is possible.

For my example here, I recently had a client who was using WixBooking's native platform in order to manage and organize all of their Kayak, Paddleboard, and Pontoon Rentals.

However, this came with limitations inside of the WixBooking's platform- which wouldn't allow them to rent out multiple items at once (Ex: The inability to rent both Kayaks and Paddleboards).

It also wouldn't let them rent out multiple quantities of the items without having to checkout separately with different transactions.

Due to these limitations, I faced the challenging task of building them their own bookings platform.

For the length of this article, I am going to talk about the process I used to build the Kayaks & Paddle Boards section - The process for adding other rentals (such as boats) is the exact same process just using different timeslots.

Velo APIs

Below are the numerous Velo API's that I combined in order to make them all to work together:

Collections

For the Paddle Board and Kayak rentals, I created three collections. One collection holds the Items' information, such as the name, image, cost, and description - this way I have the ability to easily display the product data in a repeater later.

The second collection is an inventory collection, which is a collection that is used to create a static timeslot system. A static timeslot system is when each row in the collection is a timeslot at a one-hour interval for the entirety of the client's open hours.

Through this, I can keep track of all items available per timeslot. Finally, the third collection is used to store the booking data when the user is done checking out.

image

Select Equipment Page

Now that we understand the collections used in this project, I will begin to show off the final project.

The page below is a simple row and column repeater with a filter in order to sort out the items you're searching for. When a user selects a filter option, the system uses WixData.Filter() to sort the items based on the item selected.

image
function filter() {
    let newFilter = wixData.filter();

    if(checkName) {
        preFilter = "false"
        newFilter = newFilter.eq('name', name)
    }

    if( preFilter === "true") {
        $w("#dropdown1").value = filterItem
        newFilter = newFilter.eq('name', filterItem)
    }

    let setFilter = $w("#dataset1").setFilter(newFilter);

    console.log(newFilter);

    Promise.all([setFilter])
        .then(() => {
            $w("#dataset1").setSort(wixData.sort().ascending("cost")).then(() => {
                loadRepeater()
                $w("#repeater1").expand();
            })

        })
        .catch((error) => {
            console.log(error);
        });
}

After the user has found the item that they want, they can select that item by clicking the add button in the item's listing. This will add the items object into an array and disable the button.

This brings up the ability to add a second/third item. Once selected, the repeater filters for all other rental possibilities that the user can pair with the selected item. For this example, I selected a One Hour Paddle Board rental, so it will show me all the other one-hour rental options that are available.

image
function loadRepeater() {
    $w("#repeater1").forEachItem(($item, itemData, index) => {
        $item("#costTxt").text = "$" + numberWithCommas(itemData.cost);

        $item("#bookBtn").onClick(() => {
            $w("#next").enable()
            $item("#bookBtn").label = "Added";
            $item("#bookBtn").disable();
            items.push(itemData);
            filterItems(itemData.hours)
        })
    })
}

Once the items are selected, I then proceed to the next page. In doing so, it will take my items array and set them in the user's session using session.setItem() then using WixLocation.to() I am directed to the next page.

export async function next_click(event) {
    let data = JSON.stringify(items);
    console.log(data)
    await session.setItem("data", data);
    await wixLocation.to("/quantity");
}

Quantity Page

We now have our items selected. Next, the system will bring me to the quantity page, which does exactly as it sounds. The quantity page allows the user to select how many of each selected rental they want.

First, the code grabs the data array that I sent to the user's session in the previous page. Next, it loads the repeater with the quantity options, the names of the rentals the user chose, and loads up the question asking how many of each rental the user wants.

This is a similar process to what the last page did, however, it simply adds on to the itemData object with a key value pair of "quantity": {USER INPUT}. Then using session and wixlocation again, I pass the data and the user to the next page.

image
import { session } from "wix-storage";
import wixLocation from 'wix-location';

let items = []
let quantity
let data = session.getItem("data");
let dataParsed = JSON.parse(data);

$w.onReady(function () {
    console.log(dataParsed);
    loadRepeater()
});

function loadRepeater() {
    $w('#repeater1').data = dataParsed
    $w("#repeater1").forEachItem(($item, itemData, index) => {
        $item("#nameTxt").text = itemData.name + "'s";

        $item("#quantityIpt").onKeyPress(() => {
            let debounceTimer;
            if (debounceTimer) {
                clearTimeout(debounceTimer);
                debounceTimer = undefined;
            }
            debounceTimer = setTimeout(() => {
                quantity = $item("#quantityIpt").value
                itemData.quantity = Number(quantity)
                items.push(itemData)
            }, 200);
        })
    })
}

export function nextBtn_click(event) {
    session.setItem("data", JSON.stringify(items));
    wixLocation.to("/select-timeslot")
}

Select a Timeslot Page

This step is where things begin to get a little complex. The user is now presented with a date picker. onChange, the date picker will filter the Inventory collection which was referenced earlier in the Collections section at the top of this article.

What this does is filters the static timeslots in that collection, filters by the day, and filters the items I chose as well as how many are available. If none are available, the system will hide that timeslot by making it unclickable to the user. In the image below, all timeslots are available for selection.

image
image

Once a timeslot is selected, the repeater will collapse and the user is able to proceed to the next step. Clicking a timeslot uses a similar function as what was explained in the "Selecting Equipment" section of this article.

It adds to the "items" array that we've been passing along from page to page throughout this process. The passing of the "item's array" a running theme all the way to the cart page.

import { session } from "wix-storage";
import wixData from 'wix-data';
import wixLocation from 'wix-location';

let date
let data = session.getItem("data");
let dataParsed = JSON.parse(data);
console.log(dataParsed);
let name = dataParsed[0].name
let quantity = dataParsed[0].quantity
let hours = dataParsed[0].hours
let selectedTimes = [];
let maxHours
let count

$w.onReady(function () {

	if(dataParsed.length > 1) {
		name = "items"
	}
    
});


function loadRepeater() {
    let i = 0
    let hoursArray = []
    dataParsed.forEach(item => {
        hoursArray.push(item.hours);
    });
    maxHours = hoursArray.reduce(function (a, b) {
        return Math.max(a, b);
    });
    if (count === 0) {
        $w("#infoTxt").text = "There are no timeslots available, please select a different date";
        $w("#infoTxt").expand();
    } else {
        $w("#infoTxt").text = "You have selected " + dataParsed.length + " " + name + " For " + maxHours + " hours, Please Select " + maxHours + " available timeslots";
        $w("#infoTxt").expand();
    }

    $w("#repeater1").onItemReady(($item, itemData, index) => {
        $item("#TimeSlot").label = itemData.timeslot;
        let timeslots = []

        $item("#TimeSlot").onClick(() => {
            i++
            $item("#TimeSlot").disable();

            selectedTimes.push(itemData)

            selectedTimes.forEach(timeslot => {
                let time = timeslot.timeslot
                timeslots.push(time)
            });

            timeslots = timeslots.join()
            $w("#timeslotsSelectedTxt").text = "Time selected: " + timeslots

            $w("#timeslotsSelectedTxt").expand();

            if (i === maxHours) {
                $w("#repeater1").collapse();
                $w("#nextBtn").enable();
                $w("#nextBtn").expand();
            }

        })
    })
}

export function datePicker1_change(event) {
	date = $w("#datePicker1").value
	filter(date);
	$w("#headerTxt").text = "Select A Timeslot";
}

function filter(date) {
    let newFilter = wixData.filter();
    console.log(date)
    console.log(dataParsed)
    newFilter = newFilter.eq('date', date)    

    dataParsed.forEach(item => {

        if (item.name === "Paddle Board") {
            newFilter = newFilter.ge('paddleboard', item.quantity)
        } else if (item.name === "Single Kayak") {
            newFilter = newFilter.ge('kayak', item.quantity)
        } else if (item.name === "Tandem Kayak") {
            newFilter = newFilter.ge('tandumKayak', item.quantity)
        } else {
            console.log(item.name)
        }
    });
    

    let setFilter = $w("#dataset1").setFilter(newFilter);

    console.log(newFilter);

    Promise.all([setFilter])
        .then((results) => {
            loadRepeater()
            $w("#repeater1").expand();
        })
        .catch((error) => {
            console.log(error);
        });
}


export function nextBtn_click(event) {
	dataParsed = [
		dataParsed,
		selectedTimes, 
		]
	session.setItem("data", JSON.stringify(dataParsed));
	wixLocation.to("/rental-agreement-new");
}


export function dataset1_currentIndexChanged() {
	count = $w('#dataset1').getTotalCount();
	Number(count);
	console.log(count)
}

Rental Agreement Form Page

I am not going to show this page because it has a bunch of legal jargon that I know nobody wants to see. However, with this specific custom bookings solution, the client required that the user has to have a waiver on file in order to check out.

Once the waiver is filled out, the data is inserted into the signed waivers collection, and the user as well as the user's data is pushed over to the cart page. The data is input using the connect-to-data method rather than using wixData.insert(). I chose this method because it is not always efficient to use code for a task.

If I were to use code, I would have to name each input as a variable, which then would be put into a data object, which then all would be inserted into the collection. Using the connect-to-data method is much simpler and more time-efficient. As a Velo developer, I am always looking for ways to use WIX tools to save time and energy on projects like this one, after all, that's why those tools are there!

Cart Page

The end is where our hard work comes to fruition and all the magic happens! Right on arrival to this page the user is presented with an input to verify their email. If the email is found in the signed waivers collection then it will expand the cart. If the user is not found in the collection, they are redirected back to the rental agreement form. In order to verify, I use WixData.query() to search the database for the email the user has input. If results are found then the code will display the cart.

image
export function checkEmail_click(event) {
    email = $w("#emailIpt").value
    checkEmail(email)
}

function checkEmail(email) {
    wixData.query("SignedLiabilityWaitForm")
        .eq("email", email)
        .find()
        .then((results) => {
            console.log(results)
            if (results.totalCount > 0) {
                customerData = results.items[0];
                dataParsed.push(customerData);
                $w("#waiverTxt").expand();
                $w("#repeater1").expand();
                $w("#text23").expand();
                $w("#box1").expand();
            } else {
                $w("#waiverTxt").expand();
                wixLocation.to("/rental-agreement-new")
            }
        }).catch((err) => {
            console.log(err)
        })
}

Displaying the Cart:

image

In this case, I am simply using the loadRepeater() function that I have been using in previous examples in order to show the items in the cart and dynamically insert the items into the repeater.

function loadRepeater() {
    $w('#repeater1').data = dataParsed[0]
    $w("#repeater1").forEachItem(($item, itemData, index) => {
        $item("#itemNameTxt").text = itemData.name;
        $item("#itemImage").src = itemData.image
        $item("#hoursTxt").text = "Hours: " + itemData.hours
        $item("#costTxt").text = "$" + itemData.cost * itemData.quantity
        $item("#quantityTxt").text = "Quantity: " + itemData.quantity
    })
}

The cart object is built by adding the cost of the processing fee, tax, and total for each item together. This is using simple Javascript math in order to calculate and has nothing to do with Velo. Now when the user clicks on the check out button, this will trigger the wixPay API.

The rental's data is sent from the front end to the back end, where it is manipulated to fit the function options in order for it to work. This will build a checkout page based on the prices and quantities of the rentals that I sent to the function.

Front End: Wix Pay Function

export function payBtn_click(event) {
    createMyPayment(dataParsed)
        .then((payment) => {
            wixPay.startPayment(payment.id, { "showThankYouPage": false }).then(async (result) => {
                if (result.status === "Successful") {
                    updateCollection();
                    createBooking(payment.id, result.status);
                    createContact();
                    sendEmailToContact(email, date, items);
                    sendEmailToStaff(name, date, items);
                    wixWindow.openLightbox("ThankYou", dataParsed);
                } else if (result.status === "Cancelled" || result.status === "Failed" || result.status === "Pending") {
                    
                    createBookingCancelled(payment.id, result.status);
                }
            })
        })
}

Back End: Wix Pay Function pay.jsw

The back end function takes the data, manipulates it, and returns the items for sale.

import wixPayBackend from 'wix-pay-backend';

export function createMyPayment(data) {
    console.log(data)

    let items = []
    let totalAmount = []
    data[0].forEach(item => {
            let price = item.totalPrice
            let quantity = item.quantity
            let product = item.name
            let cost = item.cost
            let amount = price * quantity
            totalAmount.push(amount);
            items.push({
                name: product,
                price: price,
                quantity: quantity,
            })
    });

    totalAmount = getArraySum(totalAmount)
    console.log(items)
    console.log(totalAmount);

    return wixPayBackend.createPayment({
        items: items,
        amount: totalAmount
    }).catch((err) => {
        console.log("Back")
        console.log(err)
    })
}

function getArraySum(a){
    var total=0;
    for(var i in a) { 
        total += a[i];
    }
    return total;
}

Check Out Process:

Once the check out button is clicked and the back end functions are completed, we are presented with a check out modal that takes the customer's personal and payment information. Once completed, this will show a payment status of "Successful". If the payment status is successful, I will then have the ability to run a few extra functions. In the next section, I will go over those.

updateCollection()

Using wixData, I retrieve the quantity of the items rented out and subtract them from the quantity that is currently available in the inventory database. Once subtracted, I use wixData.update() in order to update the collection with the new inventory numbers.

function updateCollection() {
    dataParsed[1].forEach(timeslot => {
        dataParsed[0].forEach(item => {
            let quantity = item.quantity
            let name = item.name
            if (name === "Paddle Board") {
                timeslot.paddleboard = timeslot.paddleboard - quantity
            } else if (name === "Tandem Kayak") {
                timeslot.tandumKayak = timeslot.tandumKayak - quantity
            } else if (name === "Single Kayak") {
                timeslot.kayak = timeslot.kayak - quantity
            }
        });
        wixData.update("DailyInventoryKayaksPaddleboard", timeslot).then((results) => {
            console.log("Collection Updated")
            console.log(results)
        }).catch((err) => {
            console.log(err);
        })
    });
}

createBooking()

This function essentially takes all that session storage data that we've been passing from page to page and stores it into the bookings collection for admin use and display.

SendEmail() functions

These functions use wixCrm in order to send an automated triggered email to the user notifying them that their booking is confirmed. The other notifies the site owner about their new booking.

import wixCrmBackend from 'wix-crm-backend';
import { contacts } from 'wix-crm-backend';

export async function sendEmailToContact(email, date, items) {
    let contactId;
    const emailToFind = email
    const queryResults = await contacts.queryContacts()
        .eq("info.emails.email", emailToFind)
        .find();
    const contactsWithEmail = queryResults.items;
    if (contactsWithEmail.length === 1) {
        console.log('Found 1 contact');
        contactId = contactsWithEmail[0]._id;
    } else if (contactsWithEmail.length > 1) {
        console.log('Found more than 1 contact');
        // Handle when more than one contact is found
    } else {
        console.log('No contacts found');
        // Handle when no contacts are found
    }
    const triggeredEmailTemplate = "<templateID>";
    wixCrmBackend.emailContact(triggeredEmailTemplate, contactId, {
            "variables": {
                "date": date,
                "item": items
            }
        })
        .then(() => {
            console.log('Email sent to contact');
        })
        .catch((error) => {
            console.error(error);
        });
}

Conclusion

Finally, once the checkout is complete, the user is presented with a "thank you" light box confirming their booking. All booking data is stored in a collection and the process is complete. There is also an admin dashboard which queries and displays the bookings from the bookings collection.

I often hear from people saying Velo can't do things that other coding languages can. However, I believe that with a wealth of experience, knowledge, creativity, dedication, and research into the Velo documentation, you will find that Velo is a very diverse language with many opportunities for ingenuity and development. While custom solution have their time and place, WIX offers a simple and flexible way to create a striking web application in a fraction of the time by taking out all of the CSS.

If you have made it this far in the article, I just want to say thank you for taking the time to read and learn about this awesome project.

If you have any questions, or need help with a Velo project, I am always available! Simply email me and let me know how I can be of service.

[email protected]

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.