6

How to Build Your Own Booking Platform with Velo

 3 years ago
source link: https://hackernoon.com/how-to-build-your-own-booking-platform-with-velo-7p3r37l4
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

How to Build Your Own Booking Platform with Velo

8
heart.pngheart.pngheart.pngheart.png
light.pnglight.pnglight.pnglight.png
boat.pngboat.pngboat.pngboat.png
money.pngmoney.pngmoney.pngmoney.png

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

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Velo APIs

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

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Select Equipment Page

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

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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!

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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:

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Front End: Wix Pay Function

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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:

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

updateCollection()

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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()

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

SendEmail() functions

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

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.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

[email protected]

0 reactions
heart.png
light.png
money.png
thumbs-down.png
8
heart.pngheart.pngheart.pngheart.png
light.pnglight.pnglight.pnglight.png
boat.pngboat.pngboat.pngboat.png
money.pngmoney.pngmoney.pngmoney.png
by Brendan Johnson @brendanjohnson. I am a certified Velo Developer building some cool projectsRead my stories
Join Hacker Noon

Create your free account to unlock your custom reading experience.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK