Disabling booked dates on the client
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.
Gameplan#
We built the /listing/:id
page a few modules before which is the page that surfaces and displays information about a certain listing. In the <Listing />
component rendered in the /listing/:id
page, we retrieve the value of the dynamic id
parameter available in the URL which we use as an argument for the listing
query that allows us to retrieve information about a certain listing.
We also set up the component called <ListingCreateBooking />
, rendered as a child of <Listing />
, as the surface area where we allow a user to select the dates to check-in and check-out of a listing. We haven't built functionality further than that.
There are a few things we want to achieve here. When the user selects the check-in and check-out dates for a booking and clicks the "Request to book!"
button, we'll want to surface a modal. This modal will be the confirmation modal where the user can confirm their booking and the dates they want to be booked. A summary of the total price it will cost them for the booking is to be displayed as well as an element where the user can provide their credit or debit card information to book their listing.

The element where the user will be able to provide their card information will be a component that we'll get and use from Stripe itself. There are a few reasons why this is helpful:
The card payment component we'll use will ensure that the user is to provide valid credit or debit card information. If invalid information is presented, the component would have client-side UI to reflect what is incorrect. This won't be handled by us through custom means since it'll be taken care of by the component we'll use from Stripe.
More importantly, when someone is to provide their payment information - it is sensitive information. By using elements provided to us from Stripe, we can create the capability for users to provide their information without us having to worry about handling sensitive card data on the client.
When the listing is confirmed to be booked through the "Book"
action in the modal, this is where we'll fire the createBooking
mutation and pass in the variables the createBooking
mutation expects. The createBooking
mutation expects an input that contains the id
of the listing being booked, the source
of the payment (which we'll get from the Stripe element), and the checkIn
and checkOut
dates being booked.
This are some of the main remaining things we want to handle. Additionally, there are 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. We'll also want to provide some client-side validations as well to prevent the user from even launching the booking modal if they shouldn't be booking the listing in the first place. For example, we can disable the check-in and check-out datepickers and the "Request to Book"
button when:
A user is not signed in to our application.
A user attempts to book a listing of their own.
A user attempts to book a listing where the host of the listing has disconnected from Stripe. In this context, we'll be unable to facilitate the payment to the host so we shouldn't allow someone to make a booking.
When bookings have been made to a listing, the listing bookingsIndex
object will be updated to reflect which bookings have been made for it. We'll need to update the check-in and check-out datepicker inputs to prevent users from booking dates that have already been booked!
Prevent user from booking a listing#
Since there are a couple of things for us to do, we'll take it step by step. We'll first look to handle the client-side checks for disabling the check-in/check-out datepickers and the `"Request to book!" button when the user should not be able to book a listing.
In the <ListingCreateBooking />
component in the src/sections/Listing/components/ListingCreateBooking/index.tsx
file, we'll create a constant element called buttonMessage
that we'll provide a value for "You won't be charged yet"
. This will be the message we want to show under the "Request to book!"
button when a user can book a listing but is how we'll convey that this won't confirm the booking yet. We'll place the buttonMessage
element within a <Text />
component that has the "secondary"
type and a mark
prop.
const { Paragraph, Text, Title } = Typography;
// ...
// ...
let buttonMessage = "You won't be charged yet";
export const ListingCreateBooking = ({
price,
checkInDate,
checkOutDate,
setCheckInDate,
setCheckOutDate
}: Props) => {
return (
<div className="listing-booking">
<Card className="listing-booking__card">
<div>{/* ... */}</div>
<Divider />
<Button
disabled={buttonDisabled}
size="large"
type="primary"
className="listing-booking__card-cta"
>
Request to book!
</Button>
<Text type="secondary" mark>
{buttonMessage}
</Text>
</Card>
</div>
);
};
Prevent booking when user is not logged in#
We'll look to prevent a user from booking a listing if they aren't logged in. For this, we'll need access to the viewer
object we have in our client app that keeps context of the status of the viewer (i.e. the user viewing the app). The parent <Listing />
component doesn't have the viewer
object available so we'll need to pass it two levels down from the root parent <App />
component.
In the parent <App />
component, we'll employ the render props pattern to render the <Listing />
component for its route and we'll pass an additional viewer
prop down.
<Route
exact
path="/listing/:id"
render={props => <Listing {props} viewer={viewer} />}
/>
In the <Listing />
component file, we'll declare that it is to expect the viewer
prop object.
import { Viewer } from "../../lib/types";
interface Props {
viewer: Viewer;
}
We'll declare the viewer prop as a value of the props
argument and we'll pass it further down to the child <ListingCreateBooking />
component.
export const Listing = ({ viewer, match }: Props & RouteComponentProps<MatchParams>) => {
// ...
const listingCreateBookingElement = listing ? (
<ListingCreateBooking
viewer={viewer}
price={listing.price}
checkInDate={checkInDate}
checkOutDate={checkOutDate}
setCheckInDate={setCheckInDate}
setCheckOutDate={setCheckOutDate}
/>
) : null;
return (
// ...
)
};
In the <ListingCreateBooking />
component, we'll specify it is to also accept a viewer
prop object.
// ...
import { Viewer } from "../../../../lib/types";
interface Props {
viewer: Viewer;
// ...
}
export const ListingCreateBooking = (
viewer
// ...
) => {
// ...
};
With the viewer
object available in the <ListingCreateBooking />
component, we can check to see if the viewer is available by simply seeing if the id
property of the viewer
object is available. We'll set up a checkInInputDisabled
constant in the component that will be true when viewer.id
doesn't exist.
We can then say if the checkInInputDisabled
value is ever true
, so would be the checkOutInputDisabled
value (i.e. if the user can't select a date for check-in, they shouldn't be able to select a date for check-out). If the checkOutInputDisabled
property is ever true, the "Request to book!"
button will be disabled as well. Finally, we'll say if the viewer.id
value doesn't exist, the buttonMessage
property will be updated to say - "You have to be signed in to book a listing!"
.
We'll then add the checkInInputDisabled
property as the value for the disabled
prop for the check-in <DatePicker />
input component.
export const ListingCreateBooking = (/* ... */) => {
// ...
const checkInInputDisabled = !viewer.id;
const checkOutInputDisabled = checkInInputDisabled || !checkInDate;
const buttonDisabled = checkOutInputDisabled || !checkInDate || !checkOutDate;
let buttonMessage = "You won't be charged yet";
if (!viewer.id) {
buttonMessage = "You have to be signed in to book a listing!";
}
return (
<div className="listing-booking">
<Card className="listing-booking__card">
<div>
<Paragraph>{/* ... */}</Paragraph>
<Divider />
<div className="listing-booking__card-date-picker">
<Paragraph strong>Check In</Paragraph>
<DatePicker
value={checkInDate ? checkInDate : undefined}
format={"YYYY/MM/DD"}
showToday={false}
disabled={checkInInputDisabled}
disabledDate={disabledDate}
onChange={dateValue => setCheckInDate(dateValue)}
onOpenChange={() => setCheckOutDate(null)}
/>
</div>
<div className="listing-booking__card-date-picker">{/* ... */}</div>
</div>
<Divider />
{/* ... */}
</Card>
</div>
);
};
If we were to now take a look at the /listing/:id
page of a listing when not logged in, we'll notice the check-in datepicker input is disabled since viewer.id
doesn't exist. The check-out datepicker input and the "Request to book!"
button are also disabled. The text below the confirmation button will say "You have to be signed in to book a listing!"
.

Prevent booking when viewer attempts to book own listing#
The next thing we'll check for is that the viewer isn't attempting to book their own listing. 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 a viewer isn't booking a listing of their own, we can check viewer.id
isn't equal to the host.id
.
In the parent <Listing />
component, we'll pass a prop labeled host
that reflects the listing host.
export const Listing = ({ viewer, match }: Props & RouteComponentProps<MatchParams>) => {
// ...
const listingCreateBookingElement = listing ? (
<ListingCreateBooking
viewer={viewer}
host={listing.host}
price={listing.price}
checkInDate={checkInDate}
checkOutDate={checkOutDate}
setCheckInDate={setCheckInDate}
setCheckOutDate={setCheckOutDate}
/>
) : null;
return (
// ...
)
};
In the <ListingCreateBooking />
component, we'll state that it is to accept a prop labeled host
. We'll describe the shape of the host
prop by importing the shape of the Listing
data interface from the autogenerated types for the listing
query and declare a lookup type to access the type of the host
field within this listing data interface.
import { Listing as ListingData } from "../../../../lib/graphql/queries/Listing/__generated__/Listing";
interface Props {
viewer: Viewer;
host: ListingData["listing"]["host"];
// ...
}
This page is a preview of TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two