After the booking and the payment, the person that purchases the holiday at a house is now redirected to the /bookings URL, where we now have a placeholder page.

That’s managed by the pages/bookings.js file.

Right now we added some placeholer code there:

pages/bookings.js

import Layout from '../components/Layout'

const Bookings = () => {
  return <Layout content={<p>TODO</p>} />
}

export default Bookings

We’re going to list all the bookings made by the user.

We replicate a bit what we did in pages/index.js to list houses, except now we list bookings:

pages/bookings.js

import axios from 'axios'
import Head from 'next/head'

import Layout from '../components/Layout'

const Bookings = () => {
  return (
    <Layout
      content={
        <div>
          <Head>
            <title>Your bookings</title>
          </Head>
          <h2>Your bookings</h2>

          <div className='bookings'>
            {props.bookings.map((booking, index) => {
              return (
                <div className='booking' key={index}>
                  <img src={booking.house.picture} alt='House picture' />
                  <div>
                    <h2>
                      {booking.house.title} in {booking.house.town}
                    </h2>
                    <p>
                      Booked from{' '}
                      {new Date(booking.booking.startDate).toDateString()} to{' '}
                      {new Date(booking.booking.endDate).toDateString()}
                    </p>
                  </div>
                </div>
              )
            })}
          </div>

          <style jsx>{`
            .bookings {
              display: grid;
              grid-template-columns: 100%;
              grid-gap: 40px;
            }

            .booking {
              display: grid;
              grid-template-columns: 30% 70%;
              grid-gap: 40px;
            }

            .booking img {
              width: 180px;
            }
          `}</style>
        </div>
      }
    />
  )
}

Bookings.getInitialProps = async ctx => {
  const response = await axios.get('http://localhost:3000/api/bookings/list')

  return {
    bookings: response.data
  }
}

export default Bookings

Let’s now add a /api/bookings/list endpoint, which provides this data. Again, similar to how we provide the list of houses in /api/houses:

server.js

server.get('/api/bookings/list', (req, res) => {
  Booking.findAndCountAll({
    where: {
      paid: true
    },
    order: [['startDate', 'ASC']]
  }).then(async result => {
    const bookings = result.rows.map(booking => booking.dataValues)

    res.writeHead(200, {
      'Content-Type': 'application/json'
    })
    res.end(JSON.stringify(bookings))
  })
})

We list bookings that are paid, and we sort them so the more upcoming ones are listed first.

The bookings table data does not give us much more than the start and end date, and the houseId.

So let’s add more data in list.json.js, so we can display the house name, picture and so on in the bookings listing.

When we get the bookings back, we iterate over them and we fetch, for each one, the house data, using House.findByPk().

server.js

server.get('/api/bookings/list', (req, res) => {
  Booking.findAndCountAll({
    where: {
      paid: true
    },
    order: [['startDate', 'ASC']]
  }).then(async result => {
    const bookings = await Promise.all(
      result.rows.map(async booking => {
        const data = {}
        data.booking = booking.dataValues
        data.house = (await House.findByPk(data.booking.houseId)).dataValues
        return data
      })
    )

    res.writeHead(200, {
      'Content-Type': 'application/json'
    })
    res.end(JSON.stringify(bookings))
  })
})

See how I used await Promise.all() to wrap the map() iteration over the bookings list? I used this to be able to use async inside the map. You can find all the details about this in my post How to use Async and Await with Array.map()

There’s another thing I want to do now. I want to only retrieve the bookings that are upcoming. So I exclude the ones whose end date is in the past, using Op (which we also used before in src/routes/houses/booked.js and in src/routes/houses/_canBookThoseDates.js):

server.js

server.get('/api/bookings/list', (req, res) => {
  Booking.findAndCountAll({
    where: {
      paid: true,
      endDate: {
        [Op.gte]: new Date()
      }
    },
    order: [['startDate', 'ASC']]
  }).then(async result => {
    //...
  })
})

Awesome!

Now all the upcoming bookings should show up:

We can also add a link to this page in the components/Header.js component.

Add:

<li>
  <Link href='/bookings'>
    <a>Bookings</a>
  </Link>
</li>

after the line:

<li className='username'>{user}</li>

Here’s the end result:

Only seeing your own bookings

There’s a big problem here, and I’m sure you already noticed.

This screen prints all the bookings. Our bookings, and all the bookings of other people using the site.

We need to filter bookings to only show ours.

How? By filtering for users in our API, looking at the session data.

This is our booking list code right now:

server.js

server.get('/api/bookings/list', (req, res) => {
  Booking.findAndCountAll({
    where: {
      paid: true,
      endDate: {
        [Op.gte]: new Date()
      }
    },
    order: [['startDate', 'ASC']]
  }).then(async result => {
    const bookings = await Promise.all(
      result.rows.map(async booking => {
        const data = {}
        data.booking = booking.dataValues
        data.house = (await House.findByPk(data.booking.houseId)).dataValues
        return data
      })
    )

    res.writeHead(200, {
      'Content-Type': 'application/json'
    })
    res.end(JSON.stringify(bookings))
  })
})

Let’s add this code at the beginning of the function:

if (!req.session.passport || !req.session.passport.user) {
  res.writeHead(403, {
    'Content-Type': 'application/json'
  })
  res.end(JSON.stringify({
    status: 'error',
    message: 'Unauthorized'
  }))

  return
}

const userEmail = req.session.passport.user
const user = await User.findOne({ where: { email: userEmail }})

We throw a 403 Unauthorized response if we don’t see a user logged in, then we find the user object corresponding to the logged in user.

We then pass userId when we look for bookings:

Booking.findAndCountAll({
  where: {
    paid: true,
    userId: user.id,
    endDate: {
      [Op.gte]: new Date(),
    }
    //...

Oh and since we use await, we must add an async on top:

server.get('/api/bookings/list', (req, res) => {

to

server.get('/api/bookings/list', async (req, res) => {

Great! But now if you try the page in the browser, there’s something odd going on.

If we reload the page, it is not working, we get an error.

But if we go back to the houses list, and then we click the “Bookings” link, we can see it.

What’s happening?

Remember that if we reload the page, we get the response directly from the server. If we click around in the browser, we have client side rendering. Client side rendering makes a request to our API passing the cookie.

On the server-side, we must manually pass the cookie from req.headers.cookie.

This was source of frustration for me, until I found how to fix this:

Bookings.getInitialProps = async ctx => {
  const response = await axios({
    method: 'get',
    url: 'http://localhost:3000/api/bookings/list',
    headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined
  })

  return {
    bookings: response.data
  }
}

See? Here we pass a headers property to Axios, and this makes cookies work on the server side too.

Awesome!

The code for this lesson is available at https://github.com/flaviocopes/airbnb-clone-react-nextjs/commit/94f7de2e8a645debffbc0bfc72c9235bc462b63b


Go to the next module