TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two - PT 6

QUERYING FOR LISTING DATA

Our GraphQL API is now prepared to have us query the listing field to get information for a certain listing. With that said, we’ll begin working on our React client application and create the listing page for when a user visits the /listing/:id route of our app.

The listing page is to show details for a certain listing as well as provide the capability for a user to book the listing within certain dates. The user is to see details of a listing which consist of the listing image, location, title, host, description. For a viewer that views one of their own listings, they’ll be able to also see the bookings that have been made for the listing.

The component breakdown of the /listing/:id page is as follows:

  • The UI that encompasses the details of the listing is to be a component labeled <ListingDetails /> .
  • The <ListingBookings /> component is to show a paginated list of bookings for the listing. The <ListingBookings /> component will only be shown to a user that is viewing their own listing.
  • The capability to create a booking will be part of the <ListingCreateBooking /> component. When the user selects dates in this component and clicks the Request to Book button, a modal will be shown where the user can confirm their booking and provide payment information. We’ll create this modal at a later point in the course.

Here’s how we’ll want the UI for the page shown in the /listing/:id route to look.

When the query for listing information is in the loading state, the <PageSkeleton /> component (which we’ve created before) will be shown.

LISTING QUERY

To begin, we’ll first create the GraphQL query document for the root-level listing field in our API. We’ll create an index.ts file within a Listing/ folder under the src/lib/graphql/queries/ folder.

client/
  src/
    lib/
      graphql/
        queries/
          // ...
          Listing/
            index.ts
        // ...
    // ...

We’ll export the soon to be created LISTING GraphQL const document in the src/lib/graphql/queries/index.ts file.

client/src/lib/graphql/queries/index.ts

export * from "./Listing";

In the src/lib/graphql/queries/Listing/index.ts file, we’ll set-up the LISTING GraphQL const document. The Listing query function will accept an id argument of the listing that is to be queried, a bookingsPage argument to reference the page the user is viewing for their bookings list, and a limit argument to constitute the amount of booking objects to be shown for a single page.

We’ll query for every possible field within the Listing GraphQL object in our API.

client/src/lib/graphql/queries/Listing/index.ts

import { gql } from "apollo-boost";

export const LISTING = gql`
  query Listing($id: ID!, $bookingsPage: Int!, $limit: Int!) {
    listing(id: $id) {
      id
      title
      description
      image
      host {
        id
        name
        avatar
        hasWallet
      }
      type
      address
      city
      bookings(limit: $limit, page: $bookingsPage) {
        total
        result {
          id
          tenant {
            id
            name
            avatar
          }
          checkIn
          checkOut
        }
      }
      bookingsIndex
      price
      numOfGuests
    }
  }
`;

With the GraphQL listing query defined in our client, we’ll auto-generate the corresponding TypeScript definitions of the variables and data related to our new query.

We’ll first run the codegen:schema script to re-generate the schema.json file in our client app.

npm run codegen:schema

We’ll then run the codegen:generate script to re-generate the TypeScript definitions for our GraphQL queries and mutations.

npm run codegen:generate

MAKING THE LISTING QUERY

We’ll now build the /listing/:id page in our app. The <Listing /> section component is the component that’s shown in the /listing/:id route. Within the <Listing /> component, we’ll make the query for listing information similar to how we made the query for user information in the /user/:id page.

In the src/sections/Listing/index.tsx file, we’ll import the useQuery Hook from React Apollo. We’ll import the LISTING GraphQL document from the src/lib/graphql/queries/ folder. We’ll also import the autogenerated TypeScript definitions for the data and variables for the LISTING GraphQL document.

import React from "react";
import { useQuery } from "@apollo/react-hooks";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";

export const Listing = () => {
  return <h2>Listing</h2>;
};

We’ll run the listing query when the <Listing /> component first mounts with the useQuery Hook and attempt to return the loading , data , and error states of our query result. We’ll also pass in the autogenerated type definitions for the data to be returned from the query and the variables that are expected to be passed in.

import React from "react";
import { useQuery } from "@apollo/react-hooks";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";

export const Listing = () => {
  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING);

  return <h2>Listing</h2>;
};

The listing query we make here expects us to pass a few variables such as the id of the listing that is to be queried, the bookingsPage the user is viewing, and the number of bookings to be returned in a page (in other words the limit of our pagination in the bookings list).

The value of the id variable will come from the route that the user accesses. We’ll get the id param from the route from the match prop available in this component since this component is rendered from React Router’s <Route /> component.

We’ll import the RouteComponentProps interface from react-router-dom . We’ll declare the interface for the params in our route as MatchParams which will consist of an id of type string . We’ll access the match prop available in the <Listing /> component and set the id variable of our query as match.params.id .

import React from "react";
import { RouteComponentProps } from "react-router-dom";
import { useQuery } from "@apollo/react-hooks";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";

interface MatchParams {
  id: string;
}

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id
    }
  });

  return <h2>Listing</h2>;
};

React Router has now introduced Hooks! Be sure to check out the React Router Hooks video in Module 15 of the course to highlight how the above can be done with Hooks.

When the <Listing /> component renders for the first time, the page number for the bookings list should be 1 (i.e. should be on the first page of results). To instantiate this value and have it tracked as component state, we’ll import the useState Hook from React. We’ll use the useState Hook at the top of the <Listing /> component function and destruct a bookingsPage state value and a setBookingsPage() function we’ll use to update the bookingsPage state value. We’ll initialize the state bookingsPage value as 1 and declare it as the value of the bookingsPage variable in our query.

import React, { useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useQuery } from "@apollo/react-hooks";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";

interface MatchParams {
  id: string;
}

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage
    }
  });

  return <h2>Listing</h2>;
};

The limit variable in our query is how many bookings we’ll want to be limited for a certain page. We’ll want to have a maximum of three bookings to show for a single page so we’ll define a constant above our <Listing /> component labeled PAGE_LIMIT and provide it a value of 3 . We’ll then declare the value of the limit variable in our query to be PAGE_LIMIT .

import React, { useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useQuery } from "@apollo/react-hooks";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";

interface MatchParams {
  id: string;
}

const PAGE_LIMIT = 3;

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage,
      limit: PAGE_LIMIT
    }
  });

  return <h2>Listing</h2>;
};

While our listing query is in flight (i.e. the query is in the loading state), we’ll want to show the shared <PageSkeleton /> component we’ve set up before. We’ll import the <PageSkeleton /> component from the src/lib/components/ folder and we’ll say that when the listing query is loading, we’ll want the <PageSkeleton /> component to show. We’ll wrap the rendered <PageSkeleton /> component with Ant Design’s <Content /> component from <Layout /> so we’ll be sure to import the <Layout /> component from Ant Design and destruct <Content /> from it.

import React, { useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useQuery } from "@apollo/react-hooks";
import { Layout } from "antd";
import { PageSkeleton } from "../../lib/components";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";

interface MatchParams {
  id: string;
}

const { Content } = Layout;
const PAGE_LIMIT = 3;

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage,
      limit: PAGE_LIMIT
    }
  });

  if (loading) {
    return (
      <Content className="listings">
        <PageSkeleton />
      </Content>
    );
  }

  return <h2>Listing</h2>;
};

If our listing query was to error, we’ll want to show the <PageSkeleton /> component with an error banner placed above. With that said, we’ll import the shared <ErrorBanner /> component we have in the src/lib/components/ folder and render it in addition to <PageSkeleton /> if our query was to ever error.

We’ll place a description of the error along the lines of "This listing may not exist or we've encountered an error. Please try again soon."

import React, { useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useQuery } from "@apollo/react-hooks";
import { Layout } from "antd";
import { ErrorBanner, PageSkeleton } from "../../lib/components";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";

interface MatchParams {
  id: string;
}

const { Content } = Layout;
const PAGE_LIMIT = 3;

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage,
      limit: PAGE_LIMIT
    }
  });

  if (loading) {
    return (
      <Content className="listings">
        <PageSkeleton />
      </Content>
    );
  }

  if (error) {
    return (
      <Content className="listing">
        <ErrorBanner description="This listing may not exist or we've encountered an error. Please try again soon." />
        <PageSkeleton />
      </Content>
    );
  }

  return <h2>Listing</h2>;
};

If our listing query was to finish loading and no error arises, this would mean our query has resolved and data should now be available. At this point, we’ll get the listing object from data and the bookings object from listing .

import React, { useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useQuery } from "@apollo/react-hooks";
import { Layout } from "antd";
import { ErrorBanner, PageSkeleton } from "../../lib/components";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";

interface MatchParams {
  id: string;
}

const { Content } = Layout;
const PAGE_LIMIT = 3;

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage,
      limit: PAGE_LIMIT
    }
  });

  if (loading) {
    return (
      <Content className="listings">
        <PageSkeleton />
      </Content>
    );
  }

  if (error) {
    return (
      <Content className="listings">
        <ErrorBanner description="This listing may not exist or we've encountered an error. Please try again soon!" />
        <PageSkeleton />
      </Content>
    );
  }

  const listing = data ? data.listing : null;
  const listingBookings = listing ? listing.bookings : null;

  return <h2>Listing</h2>;
};

With the expected data available when our query resolves successfully, we can begin to build the child components in our UI and surface the information we want. In the next lesson, we’ll build out the child <ListingDetails /> and <ListingBookings /> components and render it in this parent <Listing /> component.

LISTINGDETAILS & LISTINGBOOKINGS

A sample of the mock listingBookings object used in this lesson can be found - here.

With the <Listing /> section component now prepared in making our GraphQL query for listing information, we can begin building the UI we want to show in the <Listing /> component.

<LISTINGDETAILS />

We’ll first look to build the <ListingDetails /> component responsible in surfacing listing information to the user viewing the page. We’ll create this <ListingDetails /> component within a folder kept in the components/ folder of the src/sections/Listing/ directory.

client/
  // ...
  src/
    sections/
      // ...
      Listing/
        components/
          ListingDetails/
            index.tsx
        index.ts
      // ...
    // ...
  // ...

Within the src/sections/Listing/components/index.ts file, we’ll export the <ListingDetails /> component we’ll shortly create.

client/src/sections/Listing/components/index.ts

export * from "./ListingDetails";

In the <ListingDetails /> component file, we’ll import a few things we’ll need.

  • We’ll import the React library.
  • We’ll import the <Link /> component from React Router.
  • We’ll import all the components we’ll need from Ant Design - <Avatar /> , <Divider /> , <Icon /> , <Tag /> , and <Typography /> .
  • We’ll import the autogenerated typescript definitions of the data that is to be returned from the listing query.
  • We’ll import the iconColor constant we have in the src/lib/utils/ folder of our client project.
import React from "react";
import { Link } from "react-router-dom";
import { Avatar, Divider, Icon, Tag, Typography } from "antd";
import { Listing as ListingData } from "../../../../lib/graphql/queries/Listing/__generated__/Listing";
import { iconColor } from "../../../../lib/utils";

We’ve seen most of these Ant Design components before.

  • <Avatar /> helps display an avatar image.
  • <Divider /> is an element that can help divide sections.
  • <Icon /> gives us the ability to use one of the many Ant Design Icons.
  • <Typography /> helps allow us to use the Text , Title , and Paragraph elements from Ant Design.
  • We haven’t used the <Tag /> component before. The <Tag /> component is an element that can help display tags to categorize information or for presentational purposes.

We’re importing the autogenerated Listing data typescript definition of our listing query since we’ll use it to type check the prop we expect this <ListingDetails /> component is to receive. We expect this component to receive the listing object from the data returned from the query since we’ll use it to display the listing information in our UI. As a result, we’ll define the Props interface for the <ListingDetails /> component and state that this component is to receive a listing prop. The type of the listing prop will be the type of the listing object from the GraphQL data returned and we’ll use Typescript’s capability of indexed access types (i.e. lookup types) to achieve this.

import React from "react";
import { Link } from "react-router-dom";
import { Avatar, Divider, Icon, Tag, Typography } from "antd";
import { Listing as ListingData } from "../../../../lib/graphql/queries/Listing/__generated__/Listing";
import { iconColor } from "../../../../lib/utils";

interface Props {
  listing: ListingData["listing"];
}

We can then begin building the UI for the <ListingDetails /> component. First, we’ll destruct the <Paragraph /> and <Title /> components from <Typography /> . We’ll construct the function for the <ListingDetails /> component and state that it expects a listing prop. At the beginning of our component function, we’ll destruct all the properties we’ll need from the listing prop - title , description , image , type , address , city , numOfGuests , and host .

import React from "react";
import { Link } from "react-router-dom";
import { Avatar, Divider, Icon, Tag, Typography } from "antd";
import { Listing as ListingData } from "../../../../lib/graphql/queries/Listing/__generated__/Listing";
import { iconColor } from "../../../../lib/utils";

interface Props {
  listing: ListingData["listing"];
}

const { Paragraph, Title } = Typography;

export const ListingDetails = ({ listing }: Props) => {
  const { title, description, image, type, address, city, numOfGuests, host } = listing;
};

We’ll now build the template of the <ListingDetails /> component. The template will have:

  • A <div /> element with a background-image style applied to it that is to display the listing image.
  • A <Paragraph /> section to display the listing city , address , and title .
  • A section to display the host avatar and name .
  • Finally, we’ll have a section to highlight the listing type , the numOfGuests the listing can have, and the description of the listing.

We’ll be sure to use the iconColor constant we have imported to color the one <Icon /> element we have in <ListingDetails /> .

For the section that displays host information, we’ll wrap the section with React Router’s <Link /> component and make it have a target path of /user/${host.id} - which will be the user page of the host.

With all this implemented, the <ListingDetails /> component file will look like the following:

client/src/sections/Listing/components/ListingDetails/index.tsx

import React from "react";
import { Link } from "react-router-dom";
import { Avatar, Divider, Icon, Tag, Typography } from "antd";
import { Listing as ListingData } from "../../../../lib/graphql/queries/Listing/__generated__/Listing";
import { iconColor } from "../../../../lib/utils";

interface Props {
  listing: ListingData["listing"];
}

const { Paragraph, Title } = Typography;

export const ListingDetails = ({ listing }: Props) => {
  const { title, description, image, type, address, city, numOfGuests, host } = listing;

  return (
    <div className="listing-details">
      <div
        style={{ backgroundImage: `url(${image})` }}
        className="listing-details__image"
      />

      <div className="listing-details__information">
        <Paragraph type="secondary" ellipsis className="listing-details__city-address">
          <Link to={`/listings/${city}`}>
            <Icon type="environment" style={{ color: iconColor }} /> {city}
          </Link>
          <Divider type="vertical" />
          {address}
        </Paragraph>
        <Title level={3} className="listing-details__title">
          {title}
        </Title>
      </div>

      <Divider />

      <div className="listing-details__section">
        <Link to={`/user/${host.id}`}>
          <Avatar src={host.avatar} size={64} />
          <Title level={2} className="listing-details__host-name">
            {host.name}
          </Title>
        </Link>
      </div>

      <Divider />

      <div className="listing-details__section">
        <Title level={4}>About this space</Title>
        <div className="listing-details__about-items">
          <Tag color="magenta">{type}</Tag>
          <Tag color="magenta">{numOfGuests} Guests</Tag>
        </div>
        <Paragraph ellipsis={{ rows: 3, expandable: true }}>{description}</Paragraph>
      </div>
    </div>
  );
};

The markup and actual styling of how we build our UI isn’t the important takeaway here. You’re welcome to build the presentation UI as you please and this is the setting/styling we’ve come up with. The key takeaway is how we receive the necessary data from the parent component and display it in <ListingDetails /> .

Let’s now render the <ListingDetails /> component in the <Listing /> component. In the <Listing /> section component file, we’ll import the child <ListingDetails /> component from the adjacent components/ folder.

import { ListingDetails } from "./components";

We’ll also import the <Col /> and <Row /> components from Ant Design with which we’ll use to help structure how our child components will appear in <Listing /> .

client/src/sections/Listing/index.tsx

import { Col, Layout, Row } from "antd";

We’ll construct an element labeled listingDetailsElement that is to be the <ListingDetails /> component if listing information from GraphQL data is available. Otherwise, listingDetailsElement will be null .

// ...

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage,
      limit: PAGE_LIMIT
    }
  });

  // ...

  const listing = data ? data.listing : null;
  const listingBookings = listing ? listing.bookings : null;

  const listingDetailsElement = listing ? <ListingDetails listing={listing} /> : null;

  return <h2>Listing</h2>;
};

We’ll have the return() statement of the <Listing /> component return the <Content /> component as a wrapper. Within, we’ll use the <Row /> element from Ant Design to constitute that we’re to display a row of columns and apply some gutter spacing. We’ll construct a column that’ll take the entire width in extra-small viewports and a little more than half the width from large viewports (and upwards), and display the listingDetailsElement .

// ...

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage,
      limit: PAGE_LIMIT
    }
  });

  // ...

  const listing = data ? data.listing : null;
  const listingBookings = listing ? listing.bookings : null;

  const listingDetailsElement = listing ? <ListingDetails listing={listing} /> : null;

  return (
    <Content className="listings">
      <Row gutter={24} type="flex" justify="space-between">
        <Col xs={24} lg={14}>
          {listingDetailsElement}
        </Col>
      </Row>
    </Content>
  );
};

Ant Design’s grid column structure work with 24 columns. By specifying an xs value of 24 for a column, we’ll want the element within to take the entire width of the viewport (i.e. all the columns). By specifying a lg value of 14 for a column, we say we want the listingDetailsElement to take a little more than half the viewport spacing.

With our server and client Webpack server running, let’s launch our app in the browser and navigate to a valid /listing/:id route.

http://localhost:3000/listing/5d378db94e84753160e08b48
# id of mock listing is 5d378db94e84753160e08b48

We’ll see the <ListingDetails /> component which presents information about the listing image, the city the listing is located in, its address, the title, the host information, and so on.

If we try to navigate to a certain /listing/:id route in which the listing id is invalid (i.e. doesn’t really exist), the query would fail and we’re notified of this in our UI.

Great!

<LISTINGBOOKINGS />

:eyes: Since this section involves copying code over from the <UserBookings /> component and making changes after, it might be easier to follow along by watching the lesson video for this section first.

With the listing details prepared and shown in the /listing/:id page, we’ll now set up the small component that’s responsible for displaying a paginated list of bookings that exist within a listing. The bookings section to be shown in the /listing/:id page will be very similar to the bookings or listings section shown in the /user/:id page. As a result, we won’t spend as much time building the paginated list of bookings but look to replicate what was done in the /user/:id page.

First, we’ll create a ListingBookings/ folder that is to have an index.tsx file in the src/sections/Listing/components/ folder.

client/
  // ...
  src/
    sections/
      // ...
      Listing/
        components/
          ListingBookings/
            index.tsx
          // ...
        index.ts
      // ...
    // ...
  // ...

In the src/sections/Listing/components/index.ts file, we’ll have the soon to be created <ListingBookings /> component exported from the adjacent ListingBookings/ folder.

client/src/sections/Listing/components/index.ts

export * from "./ListingBookings";

In the src/sections/Listing/components/ListingBookings/index.tsx file, we’ll copy over the contents of the <UserBookings /> component (from the src/sections/User/components/UserBookings/index.tsx file) and make the necessary changes.

We’ll import some other components we’ll need from Ant Design such as the <Avatar /> and <Divider /> components.

client/src/sections/Listing/components/ListingBookings/index.tsx

import { Avatar, Divider, List, Typography } from "antd";

We’ll remove the import of the <ListingCard /> component since we don’t intend for it to be shown within the <ListingBookings /> component.

We’ll import the Listing data TypeScript definition for the listing field query.

client/src/sections/Listing/components/ListingBookings/index.tsx

import { Listing } from "../../../../lib/graphql/queries/Listing/__generated__/Listing";

We’ll expect <ListingBookings /> to receive mostly all the same props as <UserBookings /> . However, the main prop change is that the <ListingBookings /> component should expect the bookings from the listing object queried from GraphQL (not the user object). So we’ll update the first prop to be passed down to the <ListingBookings /> component to be listingBookings and we’ll say its type is the bookings object within the listing object from our GraphQL data.

client/src/sections/Listing/components/ListingBookings/index.tsx

interface Props {
  listingBookings: Listing["listing"]["bookings"];
  bookingsPage: number;
  limit: number;
  setBookingsPage: (page: number) => void;
}

We’ll remove the import of the <Paragraph /> component from <Typography /> since we won’t need it.

client/src/sections/Listing/components/ListingBookings/index.tsx

const { Text, Title } = Typography;

We’ll update the name of the component function here to be ListingBookings and the first prop should be listingBookings . We’ll get the total and result from the listingBookings prop object and we’ll rename any reference to userBookings to now be listingBookings .

export const ListingBookings = ({
  listingBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {
  const total = listingBookings ? listingBookings.total : null;
  const result = listingBookings ? listingBookings.result : null;
};

We’ll remove the <Paragraph /> section in the final element we want to be displayed. We’ll add a <Divider /> between the top of the parent <div /> element and wrap the rest within a <div /> section, and we’ll update the classes accordingly.

// ...

export const ListingBookings = ({
  listingBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {
  // ...

  const listingBookingsElement = listingBookingsList ? (
    <div className="listing-bookings">
      <Divider />
      <div className="listing-bookings__section">
        <Title level={4}>Bookings</Title>
      </div>
      {listingBookingsList}
    </div>
  ) : null;

  return listingBookingsElement;
};

client/src/sections/Listing/components/ListingBookings/index.tsx

  const listingBookingsElement = listingBookingsList ? (
    <div className="listing-bookings">
      <Divider />
      <div className="listing-bookings__section">
        <Title level={4}>Bookings</Title>
      </div>
      {listingBookingsList}
    </div>
  ) : null;

We’ll remove the listing card that’s to be shown for each rendered list item, and instead for every rendered item within the list , we’ll intend to show an avatar of the tenant who’s made the booking below the bookingHistory dates. We’ll import the <Link /> component from react-router-dom and make the avatar of the tenant within each list item a link to that tenant’s user page.

// ...
import { Link } from "react-router-dom";
// ...

export const ListingBookings = ({
  listingBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {
  const total = listingBookings ? listingBookings.total : null;
  const result = listingBookings ? listingBookings.result : null;

  const listingBookingsList = listingBookings ? (
    <List
      // ...
      renderItem={listingBooking => {
        const bookingHistory = (
          <div className="listing-bookings__history">
            <div>
              Check in: <Text strong>{listingBooking.checkIn}</Text>
            </div>
            <div>
              Check out: <Text strong>{listingBooking.checkOut}</Text>
            </div>
          </div>
        );

        return (
          <List.Item className="listing-bookings__item">
            {bookingHistory}
            <Link to={`/user/${listingBooking.tenant.id}`}>
              <Avatar src={listingBooking.tenant.avatar} size={64} shape="square" />
            </Link>
          </List.Item>
        );
      }}
    />
  ) : null;

  // ...
};

The rest of the <List /> element will be practically the same. The only changes we’ll make is that we’ll remove the positioning at the top that we’ve done for the list in the <UserBookings /> section and we’ll update the empty text if shown to be "No bookings have been made yet!" . Finally, we’ll update the grid layout for large viewports to be 3 elements to show at a time.

Ensuring all our variables are to now reference bookings within a listing as opposed to a user (e.g. listingBookings instead of userBookings ) and our element className 's have all been updated, our <ListingBookings /> component file will look like the following:

client/src/sections/Listing/components/ListingBookings/index.tsx

import React from "react";
import { Link } from "react-router-dom";
import { Avatar, Divider, List, Typography } from "antd";
import { Listing } from "../../../../lib/graphql/queries/Listing/__generated__/Listing";

interface Props {
  listingBookings: Listing["listing"]["bookings"];
  bookingsPage: number;
  limit: number;
  setBookingsPage: (page: number) => void;
}

const { Text, Title } = Typography;

export const ListingBookings = ({
  listingBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {
  const total = listingBookings ? listingBookings.total : null;
  const result = listingBookings ? listingBookings.result : null;

  const listingBookingsList = listingBookings ? (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 3
      }}
      dataSource={result ? result : undefined}
      locale={{ emptyText: "No bookings have been made yet!" }}
      pagination={{
        current: bookingsPage,
        total: total ? total : undefined,
        defaultPageSize: limit,
        hideOnSinglePage: true,
        showLessItems: true,
        onChange: (page: number) => setBookingsPage(page)
      }}
      renderItem={listingBooking => {
        const bookingHistory = (
          <div className="listing-bookings__history">
            <div>
              Check in: <Text strong>{listingBooking.checkIn}</Text>
            </div>
            <div>
              Check out: <Text strong>{listingBooking.checkOut}</Text>
            </div>
          </div>
        );

        return (
          <List.Item className="listing-bookings__item">
            {bookingHistory}
            <Link to={`/user/${listingBooking.tenant.id}`}>
              <Avatar src={listingBooking.tenant.avatar} size={64} shape="square" />
            </Link>
          </List.Item>
        );
      }}
    />
  ) : null;

  const listingBookingsElement = listingBookingsList ? (
    <div className="listing-bookings">
      <Divider />
      <div className="listing-bookings__section">
        <Title level={4}>Bookings</Title>
      </div>
      {listingBookingsList}
    </div>
  ) : null;

  return listingBookingsElement;
};

We’ve now updated the <ListingBookings /> component to behave as we expect it to for the listing bookings list we want to show in the /listing/:id page. Let’s now have the parent <Listing /> component render <ListingBookings /> as a child component when bookings are available. In the <Listing /> component file, we’ll first import the <ListingBookings /> component from the adjacent components/ folder.

client/src/sections/Listing/index.tsx

import { ListingBookings, ListingDetails } from "./components";

In the <Listing /> component, we’ll create a listingBookingsElement constant that will be the <ListingBookings /> component when listing bookings exist. In the rendered <ListingBookings /> component, we’ll pass in the listingBookings prop, the page prop, the limit prop, and the setBookingsPage() function prop.

// ...

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage,
      limit: PAGE_LIMIT
    }
  });

  // ...

  const listing = data ? data.listing : null;
  const listingBookings = listing ? listing.bookings : null;

  const listingDetailsElement = listing ? <ListingDetails listing={listing} /> : null;

  const listingBookingsElement = listingBookings ? (
    <ListingBookings
      listingBookings={listingBookings}
      bookingsPage={bookingsPage}
      limit={PAGE_LIMIT}
      setBookingsPage={setBookingsPage}
    />
  ) : null;

  return (
    <Content className="listings">
      <Row gutter={24} type="flex" justify="space-between">
        <Col xs={24} lg={14}>
          {listingDetailsElement}
        </Col>
      </Row>
    </Content>
  );
};

We’ll then look to render listingBookingsElement right below the listingDetailsElement in the return() statement of the <Listing /> component. With all these changes made, the src/sections/Listing/index.tsx file in the complete state will look like the following:

client/src/sections/Listing/index.tsx

import React, { useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useQuery } from "@apollo/react-hooks";
import { Col, Layout, Row } from "antd";
import { ErrorBanner, PageSkeleton } from "../../lib/components";
import { LISTING } from "../../lib/graphql/queries";
import {
  Listing as ListingData,
  ListingVariables
} from "../../lib/graphql/queries/Listing/__generated__/Listing";
import { ListingBookings, ListingDetails } from "./components";

interface MatchParams {
  id: string;
}

const { Content } = Layout;
const PAGE_LIMIT = 3;

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  const [bookingsPage, setBookingsPage] = useState(1);

  const { loading, data, error } = useQuery<ListingData, ListingVariables>(LISTING, {
    variables: {
      id: match.params.id,
      bookingsPage,
      limit: PAGE_LIMIT
    }
  });

  if (loading) {
    return (
      <Content className="listings">
        <PageSkeleton />
      </Content>
    );
  }

  if (error) {
    return (
      <Content className="listings">
        <ErrorBanner description="This listing may not exist or we've encountered an error. Please try again soon!" />
        <PageSkeleton />
      </Content>
    );
  }

  const listing = data ? data.listing : null;
  const listingBookings = listing ? listing.bookings : null;

  const listingDetailsElement = listing ? <ListingDetails listing={listing} /> : null;

  const listingBookingsElement = listingBookings ? (
    <ListingBookings
      listingBookings={listingBookings}
      bookingsPage={bookingsPage}
      limit={PAGE_LIMIT}
      setBookingsPage={setBookingsPage}
    />
  ) : null;

  return (
    <Content className="listings">
      <Row gutter={24} type="flex" justify="space-between">
        <Col xs={24} lg={14}>
          {listingDetailsElement}
          {listingBookingsElement}
        </Col>
      </Row>
    </Content>
  );
};

Our <Listing /> component should now be rendering the <ListingBookings /> component when bookings exist within the listing object queried from our API.

SURVEYING <LISTINGBOOKINGS />

When we look at the listing of any other user than ourselves, we won’t see any bookings information. In the server, we’ve only authorized the bookings field within the listing object to return information if bookings exist for the listing and the viewer is viewing their own listing page .

We don’t have any bookings created within our application and we’re unable to create them just yet. To observe how the bookings section in the <Listing /> page is to behave, we can try to mock the existence of bookings in our client just for this use case.

What we’ll do is take a mock data object we’ve prepared (shown below) and we can say the listingBookings constant in the <Listing /> component is to have this value.

const listingBookings = {
  total: 4,
  result: [
    {
      id: "5daa530eefc64b001767247c",
      tenant: {
        id: "117422637055829818290",
        name: "User X",
        avatar:
          "https://lh3.googleusercontent.com/a-/AAuE7mBL9NpzsFA6mGSC8xIIJfeK4oTeOJpYvL-gAyaB=s100",
        __typename: "User"
      },
      checkIn: "2019-10-29",
      checkOut: "2019-10-31",
      __typename: "Booking"
    },
    {
      id: "5daa530eefc64b001767247d",
      tenant: {
        id: "117422637055829818290",
        name: "User X",
        avatar:
          "https://lh3.googleusercontent.com/a-/AAuE7mBL9NpzsFA6mGSC8xIIJfeK4oTeOJpYvL-gAyaB=s100",
        __typename: "User"
      },
      checkIn: "2019-11-01",
      checkOut: "2019-11-03",
      __typename: "Booking"
    },
    {
      id: "5daa530eefc64b001767247g",
      tenant: {
        id: "117422637055829818290",
        name: "User X",
        avatar:
          "https://lh3.googleusercontent.com/a-/AAuE7mBL9NpzsFA6mGSC8xIIJfeK4oTeOJpYvL-gAyaB=s100",
        __typename: "User"
      },
      checkIn: "2019-11-05",
      checkOut: "2019-11-09",
      __typename: "Booking"
    },
    {
      id: "5daa530eefc64b001767247f",
      tenant: {
        id: "117422637055829818290",
        name: "User X",
        avatar:
          "https://lh3.googleusercontent.com/a-/AAuE7mBL9NpzsFA6mGSC8xIIJfeK4oTeOJpYvL-gAyaB=s100",
        __typename: "User"
      },
      checkIn: "2019-11-10",
      checkOut: "2019-11-11",
      __typename: "Booking"
    }
  ]
} as any;

When we temporarily save the changes made in the <Listing /> component, head back to the /listing/:id page in our client application and scroll to the bottom - we’ll now see the bookings section.

Each of the iterated items shown within the bookings list shows the check-in and check-out dates of the booking as well as an avatar of the actual tenant. The list is also a paginated list where if we were to click to navigate to another page, a new set of bookings will be shown.

Great! That will be it for this particular lesson. Before we close, we’ll go back to the <Listing /> component and ensure we’re trying to access the bookings field within the queried listing object and not use the mock data array we’ve just placed to test the appearance of <ListingBookings /> .

THE LISTINGCREATEBOOKING REACT COMPONENT

In the last few lessons, we managed to create the GraphQL query to retrieve information for a single listing in the /listing/:id page of our client. With listing information available, we built the accompanying UI in the /listing/:id page to display listing information ( <ListingDetails /> ) and the bookings that have been made for the listing ( <ListingBookings /> ).

In this lesson, we’re going to build the initial UI of the calendar and date-picker portion of the listing page. We’ll create this section as the <ListingCreateBooking /> component which will be the component responsible in allowing a user to begin the booking creation process.

ANT DESIGN’S <DATEPICKER />

The <ListingCreateBooking /> component will be fairly straightforward except for how we attempt to build and set-up the datepickers in the component. In the <ListingCreateBooking /> component, we would want to have two datepickers shown.

  • In the first datepicker, the user would be able to pick the date of check-in for the booking.
  • In the second datepicker, the user would be able to pick the date of check-out for the booking.

Building the datepickers from scratch would be a little bit of a difficult task (from a UI perspective) since there are a lot of things we’ll have to keep in mind. Fortunately, the UI framework we’re using (Ant Design) gives us a robust datepicker component that can help us quite a bit.

The one thing we’ll have to do to comply with Ant Design’s <DatePicker /> component is provide date values from the moment.js library. moment.js is a fantastic JavaScript library that provides an incredible amount of utility functions that allow us to parse, validate, manipulate, and display dates and times. We’ll primarily use the moment.js library in our client application to comply with how date values can be captured in Ant Design’s <DatePicker /> components.

When we eventually run the mutation to book a listing, we’ll convert the date object values captured from the datepickers to string values before we send it to the server.

Before we begin to build the datepickers, let’s first install the moment.js library as an application dependency on our client.

npm i moment

The moment npm library already has static typing associated with it so we won’t have to install an additional type definitions file.

<LISTINGCREATEBOOKING />

The next thing we’ll do is create a file for the new component that we intend to create. We’ll create a folder called ListingCreateBooking/ that is to have an index.tsx file within the src/sections/Listing/components/ folder.

client/
  src/
    // ...
    sections/
      // ...
      Listing/
        components/
          ListingCreateBooking/
            index.tsx
      // ...
  // ...

In the index file of the src/sections/Listing/components/ folder, we’ll re-export the <ListingCreateBooking /> component we’ll soon create.

client/src/sections/Listing/components/index.ts

export * from "./ListingCreateBooking";

<ListingCreateBooking /> - Initial UI

Before we focus on building the datepicker elements, we’ll first look to build the basic UI of the <ListingCreateBooking /> component which consists of the card, the card titles, and the primary button action. In the <ListingCreateBooking /> component file, we’ll import the React library and we’ll import some of the components we’ll need from Ant Design - <Button /> , <Card /> , <Divider /> , and <Typography /> . We’ll also import the utility formatListingPrice() function from the src/lib/utils/ folder that we’ll use to format the listing price shown in this component.

import React from "react";
import { Button, Card, Divider, Typography } from "antd";
import { formatListingPrice } from "../../../../lib/utils";

We’ll destruct the <Paragraph /> and <Title /> components from <Typography /> .

import React from "react";
import { Button, Card, Divider, Typography } from "antd";
import { formatListingPrice } from "../../../../lib/utils";

const { Paragraph, Title } = Typography;

We’ll create and export the <ListingCreateBooking /> component function. In the return() statement of the component function, we’ll render the <Card/> component which will have a <Paragraph /> at the top responsible for showing the price of the listing (before we show the price, we’ll show a message that says Here will be the price! ). Within the <Card /> component, we’ll also have two other <Paragraph /> elements that will be responsible in showing the check-in and check-out datepickers. We’ll finally show a <Button/> element with the title of Request to book! that the user will eventually use to confirm the booking.

import React from "react";
import { Button, Card, Divider, Typography } from "antd";
import { formatListingPrice } from "../../../../lib/utils";

const { Paragraph, Title } = Typography;

export const ListingCreateBooking = () => {
  return (
    <div className="listing-booking">
      <Card className="listing-booking__card">
        <div>
          <Paragraph>
            <Title level={2} className="listing-booking__card-title">
              Here will be the price!
            </Title>
          </Paragraph>
          <Divider />
          <div className="listing-booking__card-date-picker">
            <Paragraph strong>Check In</Paragraph>
          </div>
          <div className="listing-booking__card-date-picker">
            <Paragraph strong>Check Out</Paragraph>
          </div>
        </div>
        <Divider />
        <Button size="large" type="primary" className="listing-booking__card-cta">
          Request to book!
        </Button>
      </Card>
    </div>
  );
};

We’ll now want to render the <ListingCreateBooking /> component in the parent <Listing /> component. In the <Listing /> component file, we’ll first import the <ListingCreateBooking /> component from the adjacent components/ folder.

client/src/sections/Listing/index.tsx

import { ListingCreateBooking, ListingBookings, ListingDetails } from "./components";

In the <Listing /> component, we’ll have a constant named listingCreateBookingElement that will be the <ListingCreateBooking /> component. In the <Listing /> component’s return() statement, we’ll create a new column in the presented row that’ll take the entire window in xs viewports and will take slightly less width in lg viewports and will contain the <ListingCreateBooking /> component.

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  // ...
  // ...

  // ...

  const listingCreateBookingElement = <ListingCreateBooking />;

  return (
    <Content className="listings">
      <Row gutter={24} type="flex" justify="space-between">
        <Col xs={24} lg={14}>
          {listingDetailsElement}
          {listingBookingsElement}
        </Col>
        <Col xs={24} lg={10}>
          {listingCreateBookingElement}
        </Col>
      </Row>
    </Content>
  );
};

With our changes saved, if we were to take a look at a valid /listing/:id page in our browser, we’ll see the <ListingCreateBooking /> component we’ve prepared.

We’ll now try and get the price of the listing shown within the <ListingCreateBooking /> component. In the parent <Listing /> component, we have the listing data object available to us from the GraphQL query which contains a price field that determines the price of the listing. We’ll check to see if the listing object is present, and if so, pass in a price prop to the <ListingCreateBooking /> component with a value of listing.price . If the listing object doesn’t exist, we’ll have the element be null .

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  // ...
  // ...

  // ...

  const listingCreateBookingElement = listing ? (
    <ListingCreateBooking price={listing.price} />
  ) : null;

  return (
    <Content className="listings">
      <Row gutter={24} type="flex" justify="space-between">
        <Col xs={24} lg={14}>
          {listingDetailsElement}
          {listingBookingsElement}
        </Col>
        <Col xs={24} lg={10}>
          {listingCreateBookingElement}
        </Col>
      </Row>
    </Content>
  );
};

In the <ListingCreateBooking /> component, we’ll say it expects a price prop of type number and we’ll display the price prop value in the <Title /> section with the help of the formatListingPrice() utility function. We’ll make sure to say the price is /day .

import React from "react";
import { Button, Card, Divider, Typography } from "antd";
import { formatListingPrice } from "../../../../lib/utils";

const { Paragraph, Title } = Typography;

interface Props {
  price: number;
}

export const ListingCreateBooking = ({ price }: Props) => {
  return (
    <div className="listing-booking">
      <Card className="listing-booking__card">
        <div>
          <Paragraph>
            <Title level={2} className="listing-booking__card-title">
              {formatListingPrice(price)}
              <span>/day</span>
            </Title>
          </Paragraph>
          <Divider />
          <div className="listing-booking__card-date-picker">
            <Paragraph strong>Check In</Paragraph>
          </div>
          <div className="listing-booking__card-date-picker">
            <Paragraph strong>Check Out</Paragraph>
          </div>
        </div>
        <Divider />
        <Button size="large" type="primary" className="listing-booking__card-cta">
          Request to book!
        </Button>
      </Card>
    </div>
  );
};

If we take a look at the <ListingCreateBooking /> card in our /listing/:id page, we’ll now see the formatted price of the listing shown.

<ListingCreateBooking /> - Datepickers

We’ll now look to use the <DatePicker /> components from Ant Design and create the check-in and check-out date picker inputs.

In the <ListingCreateBooking /> component file, we’ll first import the <DatePicker /> component from the Ant Design library.

client/src/sections/Listing/components/ListingCreateBooking/index.tsx

import { Button, Card, DatePicker, Divider, Typography } from "antd";

We’ll then place a <DatePicker /> element below each of the "Check In" and "Check Out" <Paragraph /> elements.

import React from "react";
import { Button, Card, Divider, Typography } from "antd";
import { formatListingPrice } from "../../../../lib/utils";

const { Paragraph, Title } = Typography;

interface Props {
  price: number;
}

export const ListingCreateBooking = ({ price }: Props) => {
  return (
    <div className="listing-booking">
      <Card className="listing-booking__card">
        <div>
          <Paragraph>
            <Title level={2} className="listing-booking__card-title">
              {formatListingPrice(price)}
              <span>/day</span>
            </Title>
          </Paragraph>
          <Divider />
          <div className="listing-booking__card-date-picker">
            <Paragraph strong>Check In</Paragraph>
            <DatePicker />
          </div>
          <div className="listing-booking__card-date-picker">
            <Paragraph strong>Check Out</Paragraph>
            <DatePicker />
          </div>
        </div>
        <Divider />
        <Button size="large" type="primary" className="listing-booking__card-cta">
          Request to book!
        </Button>
      </Card>
    </div>
  );
};

Just by doing that alone, when we take a look at our page - we’ll see two <DatePicker /> component elements be presented to us!

Though the initial UI work for the datepickers is prepared for us, the datepickers don’t behave exactly the way we want it too. There are certain things we’ll want to handle with our date pickers - some we’ll handle now, and some we’ll handle later when we start to allow users to book listings.

<DatePicker /> Values

The first thing we’ll do is look to track the values of the check-in and check-out dates that the user is to select with the separate datepickers. We could track the states for the check-in and check-out dates within the <ListingCreateBooking /> component however we’ll create these state values in the parent <Listing /> component instead. The reason being is once the user is to click the "Request to book!" button in the <ListingCreateBooking /> component, we’ll surface a modal that we’ll build later that tells the user the dates they’ve picked as well as the price of the booking. When we build this modal, we’ll create it as a child of the <Listing /> component. As a result, we’ll have the check-in and check-out date related state values be created in the parent <Listing /> component and pass them down to both the <ListingCreateBooking /> component and the modal component we’ll create later.

With that said, in the <Listing /> component we’ll use the useState Hook to create two new state properties. We’ll create the state properties checkInDate and checkOutDate and the setter functions responsible in updating these state values - setCheckInDate() and setCheckOutDate() . We’ll define the initial values of the checkInDate and checkOutDate state properties as null . We’ll need to convey that these state properties will be either of a Moment date object or a null value. In the <Listing /> component file, we’ll import the Moment interface from the moment library and use it to help define the type of the checkIn and checkOut state properties as either a Moment date object or null .

// ...
import { Moment } from "moment";
// ...

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  // ...
  const [checkInDate, setCheckInDate] = useState<Moment | null>(null);
  const [checkOutDate, setCheckOutDate] = useState<Moment | null>(null);

  // ...
  // ...

  return (
    // ...
  )
}

As to where the <ListingCreateBooking /> component is being set-up in the <Listing /> component, we’ll pass in the newly created state properties and the functions responsible in updating these states as props - checkInDate , checkOutDate , setCheckInDate() , and setCheckOutDate() .

// ...
import { Moment } from "moment";
// ...

export const Listing = ({ match }: RouteComponentProps<MatchParams>) => {
  // ...
  const [checkInDate, setCheckInDate] = useState<Moment | null>(null);
  const [checkOutDate, setCheckOutDate] = useState<Moment | null>(null);

  const listingCreateBookingElement = listing ? (
    <ListingCreateBooking
      price={listing.price}
      checkInDate={checkInDate}
      checkOutDate={checkOutDate}
      setCheckInDate={setCheckInDate}
      setCheckOutDate={setCheckOutDate}
    />
  ) : null;

  return (
    // ...
  );
}

In the <ListingCreateBooking /> component, we’ll specify that the component is to now expect the new props passed in. We’ll first import the Moment interface from the moment library that we’ll use to define the shape of the props that might be passed in.

import React from "react";
import { Button, Card, DatePicker, Divider, Typography } from "antd";
import { Moment } from "moment";
import { displayErrorMessage, formatListingPrice } from "../../../../lib/utils";

// ...

We’ll state that the checkIn and checkOut props can either be of type Moment or null. The setCheckIn() and setCheckOut() function props will either accept the checkIn and checkOut date values of type Moment | null and will return void .

client/src/sections/Listing/components/ListingCreateBooking/index.tsx

interface Props {
  price: number;
  checkInDate: Moment | null;
  checkOutDate: Moment | null;
  setCheckInDate: (checkInDate: Moment | null) => void;
  setCheckOutDate: (checkOutDate: Moment | null) => void;
}

We’ll destruct these new props from our <ListingCreateBooking /> component function.

// ...

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  // ...
};

In the <DatePicker /> components being rendered within <ListingCreateBooking /> , we’ll now use the new props that are passed in to help capture the values the user may select. The <DatePicker /> components have a value prop that conveys the date value being captured. With that said, we’ll state the value of the first <DatePicker /> as the checkInDate prop and the value of the second <DatePicker /> as the checkOutDate prop.

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker value={checkInDate} />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker value={checkOutDate} />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

Ant Design’s <DatePicker /> components seem to only accept values of a Moment date object or undefined . So we’ll use a ternary statement to convey that if the date state properties don’t exist - we’ll pass in a value of undefined .

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker value={checkInDate ? checkInDate : undefined} />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker value={checkOutDate ? checkOutDate : undefined} />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

The <DatePicker /> components also have an onChange() function prop that gets triggered whenever a change is made in the datepicker. This onChange() function prop receives the value of the date the user selected with which we can pass along to the appropriate state setter functions to update the date state values.

For the first datepicker, we’ll say that when a change is made we’ll trigger the setCheckInDate() function to update the checkInDate state property in the parent. For the second date picker, we’ll say that when a change is made - it’ll update the checkOutDate state property in the parent.

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          onChange={dateValue => setCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

At this moment, the values conveyed in our datepickers are the state values being tracked in the parent <Listing /> component. When a change is made, it’ll update those state values.

<DatePicker /> Format

The <DatePicker /> components also accept a format prop that allows us to format our date strings so we’ll have them as YYYY/MM/DD .

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          format={"YYYY/MM/DD"}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          format={"YYYY/MM/DD"}
          onChange={dateValue => setCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

If we take a look at the datepickers at this moment, we’ll see selected dates now in the format we’ve stated "YYYY/MM/DD" .

<DatePicker /> Disabled Date

There are a few other things we’ll want to handle from a client perspective that’ll make the UI more intuitive to the user. The first thing we need to think about is the fact that a user should not be able to select a date before today for either checking in or for checking out. No one can travel back in time!

With that said, we’ll look to disable dates before the current day’s date (i.e. today’s date). Ant Design’s <DatePicker /> components provide a disabledDate() prop which is a function prop that receives the current date and allows us to return a boolean . The disabledDate() prop function runs for every date element within a visible date grid and when it returns a value of true - the iterated date item in the grid is disabled.

We can give this a try. We can add the disabledDate prop to the first <DatePicker /> element and in the function, place a console.log() to verify when this function runs and have it return true .

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={() => {
            console.log("hello");
            return true;
          }}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          format={"YYYY/MM/DD"}
          onChange={dateValue => setCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

If we take a look at the UI when we open the check-in date picker, we’ll see a console message for every date element shown at a certain grid.

All the date elements we see are disabled because we have the disabledDate() function return true in all cases.

With the disabledDate() function available to us, let’s prepare the case where the dates before today are to be disabled. We’ll have the disabledDate() function prop call a function in the component of the same name. We’ll specify currentDate as the expected parameter to our component function that will be passed along that could be undefined or of the Moment date object. We’ll have the disabledDate() prop call the disabledDate() component function in both of our <DatePicker /> elements.

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  const disabledDate = (currentDate?: Moment) => {};

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

If the currentDate value for some reason or another doesn’t exist (i.e. is undefined ), we’ll have our component disabledDate() function return false .

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  const disabledDate = (currentDate?: Moment) => {
    if (currentDate) {
    } else {
      return false;
    }
  };

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

If the currentDate value exists, we’ll look to determine at what conditions will the function return true . If the currentDate value is ever a day before today, we’ll want this function to return true (i.e. be disabled). This is where the moment.js library shines because it makes date comparisons really easy . First, we’ll import the actual moment function from the moment library.

client/src/sections/Listing/components/ListingCreateBooking/index.tsx

import moment, { Moment } from "moment";

In our component disabledDate() function, we’ll create a const value called dateIsBeforeEndOfDay that’ll be a boolean to dictate if the currentDate being assessed is before the end of today. We can check for this by using the isBefore() function from moment.js to check if the currentDate value is before today’s date (which we can get by simply running the moment() function). We’ll then return the value for the dateIsBeforeEndOfDay constant.

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  const disabledDate = (currentDate?: Moment) => {
    if (currentDate) {
      const dateIsBeforeEndOfDay = currentDate.isBefore(moment());

      return dateIsBeforeEndOfDay;
    } else {
      return false;
    }
  };

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

If we take a look at the datepickers in our UI, we’ll see every date before the current date and the current date (i.e. today) included is disabled!

Great! The current date (i.e. today) might also be disabled because of how the time values between the currentDate value passed in and today’s date differs. We’ll want to confirm that today can never be booked and only the day starting tomorrow will be the first day able to be booked. To do this, we’ll compare the currentDate with today’s date and we’ll append .endOf('day') which says we want this date object to represent the end of today .

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  const disabledDate = (currentDate?: Moment) => {
    if (currentDate) {
      const dateIsBeforeEndOfDay = currentDate.isBefore(moment().endOf("day"));

      return dateIsBeforeEndOfDay;
    } else {
      return false;
    }
  };

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

Now, regardless of what time the currentDate object is, if it falls on today’s date - it’ll still probably be before the end of day which we’ll prevent the user from booking.

Another limitation one could add is how far the user can book into the future. For example, we can say users can only book within a certain year, or within a certain month, or only within a certain week.

We won’t handle this but if you were interested in adding the client-side guard, it’ll be added here where we can check to see if the iterated date objects are after today’s date by a certain margin (e.g. disable all dates a year after today’s date).

<DatePicker /> Verify Check-Out Date

We’ll add a few more UI tweaks before we close this lesson. One important thing we’ll need to do is prevent the user from picking a check-out date that is before the check-in date since this wouldn’t make any sense at all (i.e. user can’t book a check-out time before the day they check-in).

The way we can handle this is by giving the user the capability to do so in the UI but showing an error message and preventing them from setting the state in the check-out value if they were ever to select a check-out date before check-in. And we’ll make this check primarily in the check-out datepicker element.

For the <DatePicker /> displayed to capture the check-out date, we’ll update the onChange() prop function to call another function from our component that we’ll label as verifyAndSetCheckOutDate() . The verifyAndSetCheckOutDate() function will receive the selected checkOutDate value or null from the check-out datepicker element.

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  const disabledDate = (currentDate?: Moment) => {
    if (currentDate) {
      const dateIsBeforeEndOfDay = currentDate.isBefore(moment().endOf("day"));

      return dateIsBeforeEndOfDay;
    } else {
      return false;
    }
  };

  const verifyAndSetCheckOutDate = (selectedCheckOutDate: Moment | null) => {};

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          disabledDate={disabledDate}
          format={"YYYY/MM/DD"}
          onChange={dateValue => verifyAndSetCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

We have a displayErrorMessage() utility function we’ve created before that helps display an error message in our UI. With that said, we’ll import this function from the src/lib/utils file.

client/src/sections/Listing/components/ListingCreateBooking/index.tsx

import { displayErrorMessage, formatListingPrice } from "../../../../lib/utils";

In the verifyAndSetCheckOutDate() function of our component, we’ll check if the checkOutDate value selected is before the checkInDate state property. If so, we’ll return early and prevent the update of the checkOutDate state property as well as display an error message that says "You can't book date of check out to be prior to check in!" . If the checkOutDate date selected is after the checkInDate state property, we’ll simply call the setCheckOutDate() function and pass the selected date along.

This will look something like the following:

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  const disabledDate = (currentDate?: Moment) => {
    if (currentDate) {
      const dateIsBeforeEndOfDay = currentDate.isBefore(moment().endOf("day"));

      return dateIsBeforeEndOfDay;
    } else {
      return false;
    }
  };

  const verifyAndSetCheckOutDate = (selectedCheckOutDate: Moment | null) => {
    if (checkInDate && selectedCheckOutDate) {
      if (moment(selectedCheckOutDate).isBefore(checkInDate, "days")) {
        return displayErrorMessage(
          `You can't book date of check out to be prior to check in!`
        );
      }
    }

    setCheckOutDate(selectedCheckOutDate);
  };

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          format={"YYYY/MM/DD"}
          disabledDate={disabledDate}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          disabledDate={disabledDate}
          format={"YYYY/MM/DD"}
          onChange={dateValue => verifyAndSetCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

In the verifyAndSetCheckOutDate() function, we check to see if the selectedCheckOutDate value is before the checkInDate state property by a period of days and we do this with the moment.js isBefore() function. If the selected check-out date is before the captured check-in date, we display an error message and we return early. If not - we simply set the check out date as the state value in the parent component.

If we now tried to book a check-out date that is before the check-in date, we’ll get the error message and the state value for our check-out date input will not be updated!

<DatePicker /> - showToday

This is mostly where we’ll end for this lesson. We’ll add a few more UI tweaks before we close.

First, we won’t want to see the Today tag that’s shown at the footer of each opened datepicker by default. We can prevent them from showing by stating the showToday prop is given a value of false for both of our <DatePickers /> .

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  // ...

  // ...

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          disabledDate={disabledDate}
          format={"YYYY/MM/DD"}
          showToday={false}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          disabledDate={disabledDate}
          format={"YYYY/MM/DD"}
          showToday={false}
          onChange={dateValue => verifyAndSetCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

<DatePicker /> - Disable/enable confirmation

Another thing we’ll want is to have the check-out datepicker input be disabled only until a check-in date is selected. We’ll also want the primary button action ( "Request to book!" ) to be disabled only until the check-in and check-out dates have both been selected.

The date pickers have a disabled prop with which we can use for the check-out datepicker and say it’s disabled if the check-in state property is undefined or null .

We’ll dictate a checkOutInputDisabled property that’ll dictate when the check-out datepicker should be disabled. As of now, it’ll only be disabled when the checkInDate state property doesn’t have a value. We’ll place the checkOutInputDisabled property as the value of the disabled prop in our check-out datepicker.

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  // ...

  // ...

  const checkOutInputDisabled = !checkInDate;

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          disabledDate={disabledDate}
          format={"YYYY/MM/DD"}
          showToday={false}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          disabledDate={disabledDate}
          format={"YYYY/MM/DD"}
          showToday={false}
          disabled={checkOutInputDisabled}
          onChange={dateValue => verifyAndSetCheckOutDate(dateValue)}
        />
      </div>
      {/* ... */}
      {/* ... */}
    </div>
  );
};

Similarly, we’ll have a buttonDisabled constant that’ll be true when either the checkIn or checkOut dates aren’t defined. We’ll place this buttonDisabled constant as the value of the "Request to book!" button's disabled` status.

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  // ...

  // ...

  const checkOutInputDisabled = !checkInDate;
  const buttonDisabled = !checkInDate || !checkOutDate;

  return (
    <div className="listing-booking">
      {/* ... */}
      {/* ... */}
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check In</Paragraph>
        <DatePicker
          value={checkInDate ? checkInDate : undefined}
          disabledDate={disabledDate}
          format={"YYYY/MM/DD"}
          showToday={false}
          onChange={dateValue => setCheckInDate(dateValue)}
        />
      </div>
      <div className="listing-booking__card-date-picker">
        <Paragraph strong>Check Out</Paragraph>
        <DatePicker
          value={checkOutDate ? checkOutDate : undefined}
          disabledDate={disabledDate}
          format={"YYYY/MM/DD"}
          showToday={false}
          disabled={checkOutInputDisabled}
          onChange={dateValue => verifyAndSetCheckOutDate(dateValue)}
        />
      </div>
      <Divider />
      <Button
        disabled={buttonDisabled}
        size="large"
        type="primary"
        className="listing-booking__card-cta"
      >
        Request to book!
      </Button>
    </div>
  );
};

If we now check our UI - we’ll notice the check-out date picker is disabled until a check-in date is provided AND the button is disabled until both the check-in and check-out dates are provided. Great!

<DatePicker /> - onOpenChange()

One small UI bug exists at this moment and can be seen if we move the date of check-in to be after the check-out date when a check-out date has been already set . To prevent this bug, the minute the user ever opens the check-in datepicker, we can automatically reset the check-out date picker state value back to null .

We can achieve this by using the onOpenChange() callback function available as a prop to the <DatePicker /> component. The onOpenChange() callback function gets called the moment the datepicker gets opened. For our check-in datepicker, we can use the onOpenChange() callback function and when the check-in datepicker ever gets open, we’ll call the setCheckOutDate() function and set the check-out date state value to null automatically.

With this change and all the changes made to implement the <ListingCreateBooking /> component, the src/sections/Listing/components/ListingCreateBooking/index.tsx will look like the following.

client/src/sections/Listing/components/ListingCreateBooking/index.tsx

import React from "react";
import { Button, Card, DatePicker, Divider, Typography } from "antd";
import moment, { Moment } from "moment";
import { displayErrorMessage, formatListingPrice } from "../../../../lib/utils";

const { Paragraph, Title } = Typography;

interface Props {
  price: number;
  checkInDate: Moment | null;
  checkOutDate: Moment | null;
  setCheckInDate: (checkInDate: Moment | null) => void;
  setCheckOutDate: (checkOutDate: Moment | null) => void;
}

export const ListingCreateBooking = ({
  price,
  checkInDate,
  checkOutDate,
  setCheckInDate,
  setCheckOutDate
}: Props) => {
  const disabledDate = (currentDate?: Moment) => {
    if (currentDate) {
      const dateIsBeforeEndOfDay = currentDate.isBefore(moment().endOf("day"));

      return dateIsBeforeEndOfDay;
    } else {
      return false;
    }
  };

  const verifyAndSetCheckOutDate = (selectedCheckOutDate: Moment | null) => {
    if (checkInDate && selectedCheckOutDate) {
      if (moment(selectedCheckOutDate).isBefore(checkInDate, "days")) {
        return displayErrorMessage(
          `You can't book date of check out to be prior to check in!`
        );
      }
    }

    setCheckOutDate(selectedCheckOutDate);
  };

  const checkOutInputDisabled = !checkInDate;
  const buttonDisabled = !checkInDate || !checkOutDate;

  return (
    <div className="listing-booking">
      <Card className="listing-booking__card">
        <div>
          <Paragraph>
            <Title level={2} className="listing-booking__card-title">
              {formatListingPrice(price)}
              <span>/day</span>
            </Title>
          </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}
              disabledDate={disabledDate}
              onChange={dateValue => setCheckInDate(dateValue)}
              onOpenChange={() => setCheckOutDate(null)}
            />
          </div>
          <div className="listing-booking__card-date-picker">
            <Paragraph strong>Check Out</Paragraph>
            <DatePicker
              value={checkOutDate ? checkOutDate : undefined}
              format={"YYYY/MM/DD"}
              showToday={false}
              disabled={checkOutInputDisabled}
              disabledDate={disabledDate}
              onChange={dateValue => verifyAndSetCheckOutDate(dateValue)}
            />
          </div>
        </div>
        <Divider />
        <Button
          disabled={buttonDisabled}
          size="large"
          type="primary"
          className="listing-booking__card-cta"
        >
          Request to book!
        </Button>
      </Card>
    </div>
  );
};

Great! We’ll stop here for now! What we’ve done could have appeared complicated but is fairly straightforward. In summary,

  • We declare the checkInDate and checkOutDate state properties in the parent <Listing /> component and pass it down to the <ListingCreateBooking /> component.
  • We render two datepickers for where the user will pick a check-in date and a check-out date to book a listing.
  • We prevent users from picking dates before today and we’ve added an extra guard to prevent folks from picking a check-out date before check-in.
  • We enable the check-out datepicker and the "Request to book!" button only after the previous information is provided.

Great job so far! In the next coming module, we’ll focus on setting up the home index page ( / ) of our TinyHouse application.