I’m going to implement 2 server HTTP POST endpoints.

The POST /api/houses/booked endpoint

The first endpoint we’re going to build returns the list of the booked dates of a house.

Let me first give you the code, and then we discuss it.

server.js

server.post('/api/houses/booked', async (req, res) => {
  const houseId = req.body.houseId

  const results = await Booking.findAll({
    where: {
      houseId: houseId,
      endDate: {
        [Op.gte]: new Date()
      }
    }
  })

  let bookedDates = []

  for (const result of results) {
    const dates = getDatesBetweenDates(
      new Date(result.startDate),
      new Date(result.endDate)
    )

    bookedDates = [...bookedDates, ...dates]
  }

  //remove duplicates
  bookedDates = [...new Set(bookedDates.map(date => date))]

  res.json({
    status: 'success',
    message: 'ok',
    dates: bookedDates
  })
})

Given a house id, when you call this endpoint you’ll get back the booked dates for the house.

The endpoint makes use of a getDatesBetweenDates() function, which is returns the days contained between 2 dates. This is the implementation:

server.js

const getDatesBetweenDates = (startDate, endDate) => {
  let dates = []
  while (startDate < endDate) {
    dates = [...dates, new Date(startDate)]
    startDate.setDate(startDate.getDate() + 1)
  }
  dates = [...dates, endDate]
  return dates
}

As you can see, in this function we compare JavaScript dates by comparing the Date objects directly: startDate < endDate.

To get the bookings list, we run Booking.findAll() passing a special option [Op.gte]:

const Op = require('sequelize').Op

//...

const results = await Booking.findAll({
  where: {
    houseId: houseId,
    endDate: {
      [Op.gte]: new Date()
    }
  }
})

Which in this context means that the end date is in the future compared to today’s date.

This statement:

bookedDates = [...new Set(bookedDates.map(date => date))]

is used to remove the duplicates using that special statement which adds all items in the array to a Set data structure, and then creates an array from that Set.

Check the explanation on this technique to remove array duplicates on https://flaviocopes.com/how-to-get-unique-properties-of-object-in-array/

You can try to add a few bookings to a house, using the web app, and then hit the http://localhost:3000/api/houses/booked endpoint with this JSON data, using Insomnia, passing this argument:

{
	"houseId": 1
}

You should get an array of dates as a response:

Note that the ordering of the endpoint is important in how you write it into the file. If /api/houses/:id is defined before this endpoint, that is going to catch all and generate an error in this case, as it cannot find a house with id equal to booked.

The source code is available at https://github.com/flaviocopes/airbnb-clone-react-nextjs/commit/020ef4e0d43aba94fd27d7b8fa560431a461d587

The POST /api/houses/check endpoint

Next, we implement another endpoint in the server.js file.

The goal of this endpoint is to check, given a start date, and end date and an house id, if we can book that house on the dates we chose, or if we have other bookings matching those dates.

I’m going to extract the check in this function:

const canBookThoseDates = async (houseId, startDate, endDate) => {
  const results = await Booking.findAll({
    where: {
      houseId: houseId,
      startDate: {
        [Op.lte]: new Date(endDate)
      },
      endDate: {
        [Op.gte]: new Date(startDate)
      }
    }
  })
  return !(results.length > 0)
}

See, I used Op like in the previous lesson.

I searched how to determine whether two date ranges overlap on Google to find this “formula”. Basically, we check if the start date of a booking is after the end date we look for, and if the end date of a booking is before the starting date we want to check.

We then check if this query returns a result, which means the house is busy.

What we must do in our /api/houses/check endpoint is to determine if the house can be booked. If so, we return a ‘free’ message. If not, a ‘busy’ message:

server.js

server.post('/api/houses/check', async (req, res) => {
  const startDate = req.body.startDate
  const endDate = req.body.endDate
  const houseId = req.body.houseId

  let message = 'free'
  if (!(await canBookThoseDates(houseId, startDate, endDate))) {
    message = 'busy'
  }

  res.json({
    status: 'success',
    message: message
  })
})

The source code is available at https://github.com/flaviocopes/airbnb-clone-react-nextjs/commit/458ffe8cea013ea7169b9e11673720f055a09f6e

Remove dates from calendar

Let’s now put the endpoints we created into good use.

I want to remove the already booked dates from the calendar.

In pages/houses/[id].js I am going to define a function that using Axios gets the booked dates. I use HTTP POST, maybe GET would be better from a semantical point of view, but I’d have to switch how to pass parameters and I like to stick to one way:

pages/houses/[id].js

const getBookedDates = async () => {
  try {
    const houseId = house.id
    const response = await axios.post('http://localhost:3000/api/houses/booked', { houseId })
    if (response.data.status === 'error') {
      alert(response.data.message)
      return
    }
    return response.data.dates
  } catch (error) {
    console.error(error)
    return
  }
}

This method returns the dates array.

We call this method inside the House.getInitialProps function, right after we make a call to fetch the houses data, and we return the data we get:

pages/houses/[id].js

House.getInitialProps = async ({ query }) => {
  const { id } = query

  const res = await fetch(`http://localhost:3000/api/houses/${id}`)
  const house = await res.json()

  const bookedDates = await getBookedDates(id)

  return {
    house,
    bookedDates
  }
}

I use axios this time, instead of isomorphic-unfetch fetch. I find it nicer for POST requests. I left this mix to show you the different syntax, and you can choose the one you like the most.

NOTE: We still have to reference the full domain + port in the URL. TODO: fix

So now we have bookedDates passed as a prop to House.

In turn, we pass bookedDates as a prop to the DateRangePicker component:

pages/houses/[id].js

<DateRangePicker
  datesChanged={(startDate, endDate) => {
    setNumberOfNightsBetweenDates(
      calcNumberOfNightsBetweenDates(startDate, endDate)
    )
    setDateChosen(true)
    setStartDate(startDate)
    setEndDate(endDate)
  }}
  bookedDates={props.bookedDates}
/>

and we can switch to editing that component, by opening the components/DateRangePicker.js file.

The bookedDates prop we send to this component is now a list of strings representing dates, like this:

[
  '2019-11-26T00:00:00.000Z',
  '2019-11-27T00:00:00.000Z',
  '2019-11-26T00:00:00.000Z',
  '2019-11-27T00:00:00.000Z',
  '2019-11-28T00:00:00.000Z',
  '2019-11-29T00:00:00.000Z'
]

We need to iterate over each of those strings, and get back Date objects instead. We do so by adding:

bookedDates = bookedDates.map(date => {
  return new Date(date)
})

and now we can add bookedDates to our DayPickerInput components, like this:

components/DateRangePicker.js

<div>
  <label>From:</label>
  <DayPickerInput
    formatDate={formatDate}
    format={format}
    value={startDate}
    parseDate={parseDate}
    placeholder={`${dateFnsFormat(new Date(), format)}`}
    dayPickerProps={{
      modifiers: {
        disabled: [
          ...bookedDates,
          {
            before: new Date()
          }
        ]
      }
    }}
    onDayChange={day => {
      setStartDate(day)
      const newEndDate = new Date(day)
      if (numberOfNightsBetweenDates(day, endDate) < 1) {
        newEndDate.setDate(newEndDate.getDate() + 1)
        setEndDate(newEndDate)
      }
      datesChanged(day, newEndDate)
    }}
  />
</div>
<div>
  <label>To:</label>
  <DayPickerInput
    formatDate={formatDate}
    format={format}
    value={endDate}
    parseDate={parseDate}
    placeholder={`${dateFnsFormat(new Date(), format)}`}
    dayPickerProps={{
      modifiers: {
        disabled: [
          startDate,
          ...bookedDates,
          {
            before: startDate
          }
        ]
      }
    }}
    onDayChange={day => {
      setEndDate(day)
      datesChanged(startDate, day)
    }}
  />
</div>

See? I used ...bookedDates to expand the array, so we pass each single item inside the disabled array to DayPickerInput.

Great! We can now also use this function to see if an end date is selectable, by calling it first thing inside the endDateSelectableCallback() function:

You should now be prevented to choose already booked days!

https://github.com/flaviocopes/airbnb-clone-react-nextjs/commit/9610e3a2a7a4e46f9b80024334b9b310a7030f38

Prevent booking if already booked

Now there’s a problem because in the screenshot for example we can choose Oct 21 as a starting date, and Oct 30 as ending, but all the days between are booked.

We need to make good use of our /houses/check endpoint to check if in the dates we want to book, some are already booked!

We do this by adding this canReserve() function to pages/houses/[id].js:

pages/houses/[id].js

const canReserve = async (houseId, startDate, endDate) => {
  try {
    const houseId = house.id
    const response = await axios.post('http://localhost:3000/api/houses/check', { houseId, startDate, endDate })
    if (response.data.status === 'error') {
      alert(response.data.message)
      return
    }

    if (response.data.message === 'busy') return false
    return true
  } catch (error) {
    console.error(error)
    return
  }
}

This function is calling the server-side /api/houses/check endpoint we created previously, and if we get ‘busy’ as a message in the response, we return false.

We can use this boolean return value to disallow going on with the booking, in the “Reserve” button onClick callback function we already wrote.

That function now is:

pages/houses/[id].js

<button
  className='reserve'
  onClick={async () => {
    try {
      const response = await axios.post(
        '/api/houses/reserve',
        {
          houseId: props.house.id,
          startDate,
          endDate
        }
      )
      if (response.data.status === 'error') {
        alert(response.data.message)
        return
      }
      console.log(response.data)
    } catch (error) {
      console.log(error)
      return
    }
  }}>
  Reserve
</button>

We do it as the first thing:

pages/houses/[id].js

<button
  className='reserve'
  onClick={async () => {
    if (!(await canReserve(props.house.id, startDate, endDate))) {
      alert('The dates chosen are not valid')
      return
    }

    try {...

Now a person could use the API without the frontend, using the Insomnia client for example, and book a place without this check.

So we’ll also implement it server-side, in the server.js file, in the /api/houses/reserve POST endpoint.

This is the code right now:

server.js

server.post('/api/houses/reserve', (req, res) => {
  const userEmail = req.session.passport.user
  User.findOne({ where: { email: userEmail } }).then(user => {
    Booking.create({
      houseId: req.body.houseId,
      userId: user.id,
      startDate: req.body.startDate,
      endDate: req.body.endDate
    }).then(() => {
      res.writeHead(200, {
        'Content-Type': 'application/json'
      })
      res.end(JSON.stringify({ status: 'success', message: 'ok' }))
    })
  })
})

I’m going to add a check before we call all the rest of the logic, by calling the canBookThoseDates() function we defined previously.

Now we can first check if we can’t reserve, and quickly fire a 500 error, and I’m also going to add a check for the user session. Users must be logged in to book, so it makes sense to check to avoid exceptions in our code and bail out:

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

  return
}

Here’s the full endpoint code:

server.post('/api/houses/reserve', async (req, res) => {
  if (!req.session.passport) {
    res.writeHead(403, {
      'Content-Type': 'application/json'
    })
    res.end(
      JSON.stringify({
        status: 'error',
        message: 'Unauthorized'
      })
    )

    return
  }

  if (
    !(await canBookThoseDates(
      req.body.houseId,
      req.body.startDate,
      req.body.endDate
    ))
  ) {
    //busy
    res.writeHead(500, {
      'Content-Type': 'application/json'
    })
    res.end(
      JSON.stringify({
        status: 'error',
        message: 'House is already booked'
      })
    )

    return
  }

  const userEmail = req.session.passport.user
  User.findOne({ where: { email: userEmail } }).then(user => {
    Booking.create({
      houseId: req.body.houseId,
      userId: user.id,
      startDate: req.body.startDate,
      endDate: req.body.endDate
    }).then(() => {
      res.writeHead(200, {
        'Content-Type': 'application/json'
      })
      res.end(JSON.stringify({ status: 'success', message: 'ok' }))
    })
  })
})

Awesome!

The source code is available at https://github.com/flaviocopes/airbnb-clone-react-nextjs/commit/7d5314364278be168ad70c395794275f0be8b50f


Go to the next lesson