How to Build a React Booking UI That Blocks Booked Dates
In this lesson, we'll begin to work on the client-side to facilitate the booking of a listing. We'll begin by first disabling any dates in the listing page datepickers that have been previously booked by other users.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
[00:00 - 00:21] A few modules before, we built out the listing page which is the page that surfaces and displays information about a certain listing. We retrieve the dynamic ID parameter available in the URL which we use as a argument for the listing query that's being conducted in this page that retrieves information about a certain listing.
[00:22 - 00:34] We also set up a component called listing create booking which is to allow the user to select the dates to check in and check out of this listing. But we haven't built out the functionality further than that.
[00:35 - 00:44] At this moment when we click check in and check out and select the request to book button, nothing really happens. There's a few things we'll want to achieve here.
[00:45 - 00:58] When the user selects their check in and check out dates and clicks the request to book button, we'll want to surface a modal. This modal will be the confirmation modal where the user is able to confirm their booking and the dates they've selected.
[00:59 - 01:22] The price will be also shown to them for what is actually going to cost them for them to make the booking and then there'll be an element where they're able to provide their credit or debit card information and they'll be able to finally book and confirm their request. Now this element here for the payment details will be a component that will get in use from Stripe itself and there's multiple reasons why this is helpful.
[01:23 - 01:39] In this particular component will ensure that the user is to provide a valid credit or debit card information and if invalid information is presented, this component has some client side UI to reflect what's incorrect. And the good news is it won't be handled by us through custom means.
[01:40 - 02:00] It's already taken care of by the component. However, more importantly, when someone is to provide their payment information , it is sensitive information by using the elements provided to us from Stripe, we can have users provide their information without us having to worry about handling sensitive card data.
[02:01 - 02:28] When the listing is confirmed to be booked through the book action in the modal , this is where we'll fire the create booking mutation and pass in the variables that create booking mutation is to expect, which is an input that is to contain the ID of the listing being booked, the source of the payment that will get from the Stripe element and the check-in and check-out dates being booked. This is sort of the main remaining thing we want to handle.
[02:29 - 02:38] Additionally, however, there's a few other things we'll need to look into as well. In our server, we've provided some capability when a user shouldn't be able to book a listing.
[02:39 - 03:09] We'll also provide some form of client side checks as well to prevent the user from even launching the booking modal if they shouldn't be booking the listing in the first place. We'll disable the check-in and check-out date pickers and the request to book button as well as provide a message when a user is not signed into the application, when the user attempts to book their own listing, and lastly, when a user attempts to book a listing of a host with a host as disconnected from Stripe.
[03:10 - 03:26] In this context, we'll be unable to facilitate the payment to the host so we shouldn't allow someone to make a booking in the first place. And finally, when bookings have been made to a listing, the listing Bookings Index object will be updated to reflect which bookings have been made for it.
[03:27 - 03:39] Our date pickers are currently set up to prevent a user from booking a date before today. We'll also need to update our date pickers to prevent users from booking dates that have already been booked.
[03:40 - 03:54] There's a couple of things for us to do so we'll take it step by step. We'll first look to handle the client side checks for disabling the check-in check-out date pickers and the request to book button when the user shouldn't be able to book a listing.
[03:55 - 04:19] In the listing, create booking component will create a constant element called button message that will provide a value for "you won't be charged yet". And this will be the message we want to show under the request button in the date picker when a user is able to book a listing but is our way of basically saying that clicking this button doesn't confirm the booking yet.
[04:20 - 04:51] We'll place the button message element within a text component that has the secondary type in a mark prop and we'll also be sure to destruct the text component from the typography component from Antesi. When we take a look at the component section, we'll see the message shown below the request to book button.
[04:52 - 05:08] The first thing we'll do here is prevent the user from booking a listing if they aren't logged in. And for this, we'll need the viewer object we have in our client application that keeps context of the status of the viewer or in other words, the user who's viewing the app.
[05:09 - 05:34] The parent listing component doesn't have the viewer object available so we'll need to pass it two levels down from the root's most apparent app component. So when the parent app component will employ the render props pattern to render the listing component for its route and we'll pass an additional viewer prop down.
[05:35 - 05:55] In the listing component file, we'll declare that it is to expect the viewer prop object and we'll pass it down to the child listing create booking component. We'll also import the viewer interface from the lib types file to help describe the shape of this object.
[05:56 - 06:44] In the listing, create booking components will also say it is to expect a viewer prop object and will also import the viewer interface that we have in the lib types file to help describe the shape of this object. With the viewer object, we can check to see if the viewer exists by simply seeing if the ID property of the viewer object has a value.
[06:45 - 07:01] We have constants set up to dictate when the checkout date input and the request button are disabled. Let's set up a check in input disabled constant that will be true when the viewer ID doesn't have a value.
[07:02 - 07:17] We can then say that if check in date input is ever disabled, we'll state that the checkout input disabled property will be true. And if the checkout input is ever disabled, so would be the button where the user actually requests to book.
[07:18 - 07:36] And lastly, we'll also say if the viewer ID doesn't exist, the button message that will show in the entire section would instead say you have to be signed in to book a listing. The checkout input disabled and button disabled constant values are being used in the component return statement.
[07:37 - 07:51] We'll simply need to add the check in input disabled as the value for the disabled field prop for the check in date picker. Now when we take a look at our app, we'll see that it appears as normal when we 're logged in.
[07:52 - 08:04] However, if we log out, we'll notice that the check in input is disabled since the viewer ID doesn't exist. And that is connected to the checkout input and the request button since they 're now both disabled as well.
[08:05 - 08:24] And the button text will say you have to be signed in to book a listing. The next thing we'll check for is that the user or viewer isn't booking their own listing.
[08:25 - 08:43] From the GraphQL query we make for the listing information, the host field is to have the user information of the user who owns the listing. To verify if a viewer isn't booking their own listing, we'll simply need to check that the viewer ID isn't equal to the ID of this host.
[08:44 - 08:59] So in the parent listing component, we'll pass a prop just labeled host that reflects the host information. And in the listing create booking component, we'll state that it is to accept a prop called host.
[09:00 - 09:46] And for its type, we can simply import the shape of the listing data from the listing graphQL types and declare a lookup type to access the type of the host field within the listing data interface. We'll create a constant called viewer is host that holds true when the viewer ID matches the host ID.
[09:47 - 10:04] And we'll say the check in input disabled property will be true when viewer is host is ever true. And we'll update the button message property in this case to say you can't book your own listing.
[10:05 - 10:27] Let's try this out. When we go to our profile page and open the listing page of our own recently created listing, we'll see that we're unable to check in or check out and represented with the message you can't book your own listing.
[10:28 - 10:36] Great. The last thing we'll consider here is when a viewer attempts to book a listing where the host has disconnected from Stripe.
[10:37 - 10:49] This will be a fairly simple check to make and we can use the has wallet field within the host object. Remember that the wallet ID field exists for a user on the server.
[10:50 - 11:01] In our GraphQL API, we've mentioned that the client wouldn't necessarily need to know the actual wallet ID value. So we instead return a Boolean called has wallet.
[11:02 - 11:29] We'll have the check in input disabled property ring true if this has wallet field within the host doesn't have a value. And we'll place a message that says the host has disconnected from Stripe and thus won't be able to receive payments.
[11:30 - 11:41] To test this out, we'll need to first have a user create a listing just like my profile already has at this moment. And we'll then have its disconnect from Stripe.
[11:42 - 12:03] We'll then log in with another user account to act as the user interested in making the booking. We'll find the listing in question.
[12:04 - 12:26] And when we launch the listing page, we'll see in the booking section that the inputs are disabled and a message is shown that says the host has disconnected from Stripe. Now if the host was to reconnect with Stripe.
[12:27 - 12:35] When we go back and refresh the page, we'll then be able to book the listing. Awesome.
[12:36 - 12:52] The other thing we'll handle here is to make sure the dates that have already been booked and the listing is going to be disabled in the date picker elements. The bookings index object available in the listing data object in the parent has information about the dates that have been booked.
[12:53 - 13:09] So we'll pass a prop down to the listing create booking component called book ings index where the value is the bookings index field within listing. When the listing create booking component, we'll want to declare that this component is to accept the bookings index prop.
[13:10 - 13:27] We'll specify the type of this prop to be the type of the bookings index field within the listing object from our GraphQL data. Note that this bookings index field is sent as a string from the server to the client.
[13:28 - 13:39] What we'll do in the beginning of the listing create booking component is specify a new constant called bookings index JSON. That is an object representation of what the string is.
[13:40 - 13:52] And we can achieve this with the help of the JSON parse function available in JavaScript. So now instead of being a string, we'll have an actual object that we can access or manipulate.
[13:53 - 14:04] The JSON parse function doesn't tell us what the type of the return property is going to be. To ensure we're going to take advantage of TypeScript, let's look to define a type for this constant.
[14:05 - 14:18] We'll create a types file adjacent to this file. We'll export and create an interface called bookings index that essentially resembles the shape of the booking index object within a listing.
[14:19 - 14:26] It'll be very similar to what we had in the server. It'll be an index signature or key value pair that has two nested objects.
[14:27 - 14:39] The first one will be the bookings index your interface. That is to have another nested key value pair for bookings index month.
[14:40 - 15:05] And the bookings index month interface will be a key value pair where the values would always be Boolean. And in the adjacent index file will import the bookings index interface and assign it to the constant we set up called bookings index JSON.
[15:06 - 15:30] The disabled date function we have is a function that we've set up before to dictate which dates should be disabled in both our check in and check out date pickers. And essentially if we recall, we've mentioned before that the add design component date picker essentially calls this particular component for every single date property available within a certain grid.
[15:31 - 16:02] So what we can do is in our return statement, if the dates being iterated and available exists, we can say it will be disabled when the dates is before end of day or when a date is booked and we'll call a function that will create called data is booked and pass in the actual current date property. And the date is booked function is a function that would accept the current date property which should be of type moment.
[16:03 - 16:21] This date is booked function will be fairly straightforward and we simply want to check that the year month day value of this current date property within the bookings index JSON object, it doesn't have a true value. So it's very similar to how we sort of handled it on a server, but in an easier scale.
[16:22 - 16:31] Similarly, we'll try and get the year month and day value for this current date property. In the server, what we've done is use native JavaScript functions.
[16:32 - 16:52] But here we can actually use the moment utility library to get the year will say moment pass in the current date property and run the year function. For the month, we'll use will say moment pass in the current day property and run the month function, which is similar to the server and starts from a value of zero up to 11.
[16:53 - 17:35] And for the day value will run the moment function and specify dot date. And then we'll simply say if the bookings index JSON year value exists and the bookings index JSON year month value exists, simply return what the Boolean of bookings index JSON year month and day is otherwise return false.
[17:36 - 17:43] Now there's a few ways of doing this context. We're saying if this year month day value exists, this value will be true.
[17:44 - 17:53] The Boolean property and JavaScript of true will return true. If this is undefined or doesn't exist, the Boolean of undefined will return false.
[17:54 - 18:12] So in this context, the date is booked property will say false. If a value for the year or the month can't be found will automatically return false since we'll obviously know that it hasn't been booked either within that year or within that month.
[18:13 - 18:30] So now the dates in our date pickers will be disabled if the date is before today or if a date has already been booked for a listing. There's one other peculiar situation we'll look to handle.
[18:31 - 18:47] So if I go back to a certain listing now and in this moment, we don't have any dates booked. So let's assume within this great example, the dates of the 17th of December 2019 and the 18th of December 2019 have been booked.
[18:48 - 19:01] Later on, when we actually build out the capability to create bookings, we'll see that both of these items here will be disabled. So let's assume a user wants to select a check in date for a day before one of these days.
[19:02 - 19:08] So let's say the 16th. And let's assume they wanted to select a check out date that was after the disabled dates.
[19:09 - 19:18] So let's say 19th, right? The way we have things set up right now, this would be allowed because they're selecting two dates that are enabled.
[19:19 - 19:37] However, it won't really make any sense because we would never want somebody to book dates that overlap somebody else's dates that have already been booked. We can't have someone checking on the 16th, someone else checking on the 17th and then have that person then check out one of the 18th and one of the 19th.
[19:38 - 20:04] So we'll need to have an additional check in our use case and for our application to say if the user attempts to select the dates or two dates that overlap existing booked the dates, we'll need to throw an error and warn the user. So we have a method already called verify and set check out date that we use to provide an additional check to say is the user selecting a valid check out date.
[20:05 - 20:16] And in our use case at this moment, we only have one check to make sure the user isn't selecting a check out date that is before checking. We'll look to add an additional check here as well.
[20:17 - 20:42] And what we'll look to do is we'll need to check that if the user selects a check out date, there is no dates between check in and check out that has already been booked. And in this case, we'll use similar to how we've seen in the server, we'll use a date cursor which will start at the beginning and then we'll simply loop through the dates between check in and check out and try to see if any of these dates already been booked.
[20:43 - 21:09] So we'll use a while loop and we'll say while this date cursor, which will be the check in date value in the very beginning, is before the check out date through a period of days, we'll look to verify that every single date in between is enabled or not disabled. Now notice that we're using the moment library here to basically compare to two dates.
[21:10 - 21:24] In the server, we actually simply compared it by making sure one date is object less than or greater than the other. But with moments, we're able to basically run some validating helper methods like is this dates before another date.
[21:25 - 21:43] Since we won't need to check if the actual initial date cursor value is disabled, since we'll know it's enabled at this point, the very first date we'll check is the day right after the initial date cursor. So we'll update the date cursor at this moment and add a single date.
[21:44 - 22:01] We'll then look to get the year, month and day value for this date cursor. And we'll use the same methods we've seen earlier, the moment year function, the moment month function and the moment date function.
[22:02 - 22:43] And then we'll have an if statement and we'll simply check that if this particular bookings index JSON year, month and day value exists, it means this day has already been booked, we'll use the display error message function we have and return early and to basically tell the user you can't book a period of time that overlaps existing bookings. Please try again.
[22:44 - 22:54] And there we have it. This check over here is very similar to the check we do in the server when we attempt to create a new bookings index object for a listing document.
[22:55 - 23:08] And we want to verify that the user selected valid dates that don't overlap any already booked dates. In terms of what we need to keep in mind for performance, perhaps it's very similar to what we talked about in the last lesson or so.
[23:09 - 23:20] At this very moment in time, we don't have a restriction for how far into the future the user can book a checkout date as opposed to check in. So check in could be today and check out could be 20 years from now.
[23:21 - 23:30] Now that is unintuitive and are very helpful. And this particular method will go for every single day until it hits a point that either returns early or it reaches the very end.
[23:31 - 23:41] To help avoid this or alleviate that issue is to provide a restriction where checkouts can only be a certain time into the future. Other than that, this method isn't that bad.
[23:42 - 23:49] It's a single loop. So regardless of the size of the input or the size of the difference in input, it's directly proportional.
[23:50 - 23:53] So it doesn't get very bad very quickly. Awesome.
[23:54 - 24:06] In this lesson, we did a few different things. One of the first things we did was provide some client side, you know, valid ations to sort of notify the user that they can't book a listing under certain conditions .
[24:07 - 24:15] And this is when the viewer is not logged in. This was when the viewer is the host and this is when the host may have disconnected from Stripe.
[24:16 - 24:40] We also tried to specify and declared that if the dates within the date pickers have already been booked, we'll want them to be disabled in the date pickers. And lastly, we provided an additional check to sort of specify that if the user attempts to pick check in and check out dates that overlap existing bookings that have been made, we'll want to return early and display an error message.
[24:41 - 25:02] So in this moment, we can't test the functionality in our date pickers that cover the fact around bookings that have been made purely for the fact that none of our listings in our app actually have bookings. One way we could go about doing this is going through our Mongo Atlas dashboard and our database and sort of adding the data to a certain listing.
[25:03 - 25:17] But in this context, what we'll do is proceed as is the next few lessons will create the model, will actually conduct the create booking mutation. And then we'll look to investigate if our date pickers behave the way we want it.
[25:18 - 25:18] Great job so far.