This content originally appeared on flaviocopes.com and was authored by flaviocopes.com
This post is part of a new series where we build a clone of Airbnb with Next.js. See the first post here.
- Part 1: Let’s start by installing Next.js
- Part 2: Build the list of houses
- Part 3: Build the house detail view
- Part 4: CSS and navigation bar
- Part 5: Start with the date picker
- Part 6: Add the sidebar
- Part 7: Add react-day-picker
- Part 8: Add the calendar to the page
- Part 9: Configure the DayPickerInput component
- Part 10: Sync the start and end dates
- Part 11: Show the price for the chosen dates
- Part 12: Login and signup forms
- Part 13: Activate the modal
- Part 14: Send registration data to the server
- Part 15: Add postgres
- Part 16: Implement model and DB connection
- Part 17: Create a session token
- Part 18: Implement login
- Part 19: Determine if we are logged in
- Part 20: Change state after we login
- Part 21: Log in after registration
- Part 22: Create the models and move data to the db
- Part 23: Use the database instead of the file
- Part 24: Handle bookings
We’re 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.
pages/api/houses/booked.js
import { Booking } from '../../../model.js'
import { Sequelize } from 'sequelize'
const getDatesBetweenDates = (startDate, endDate) => {
let dates = []
while (startDate < endDate) {
dates = [...dates, new Date(startDate)]
startDate.setDate(startDate.getDate() + 1)
}
dates = [...dates, endDate]
return dates
}
export default async (req, res) => {
if (req.method !== 'POST') {
res.status(405).end() //Method Not Allowed
return
}
const houseId = req.body.houseId
const results = await Booking.findAll({
where: {
houseId: houseId,
endDate: {
[Sequelize.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.
As you can see, in that 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:
The POST /api/houses/check
endpoint
Next, we implement another endpoint in the pages/api/houses/check.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: {
[Sequelize.Op.lte]: new Date(endDate)
},
endDate: {
[Sequelize.Op.gte]: new Date(startDate)
}
}
})
return !(results.length > 0)
}
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:
check.js
import { Booking } from '../../../model.js'
import { Sequelize } from 'sequelize'
const canBookThoseDates = async (houseId, startDate, endDate) => {
const results = await Booking.findAll({
where: {
houseId: houseId,
startDate: {
[Sequelize.Op.lte]: new Date(endDate)
},
endDate: {
[Sequelize.Op.gte]: new Date(startDate)
}
}
})
return !(results.length > 0)
}
export default async (req, res) => {
if (req.method !== 'POST') {
res.status(405).end() //Method Not Allowed
return
}
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
})
}
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 (id) => {
try {
const response = await axios.post(
'http://localhost:3000/api/houses/booked',
{ houseId: id }
)
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 getServerSideProps
function:
pages/houses/[id].js
export async function getServerSideProps({ req, res, query }) {
const { id } = query
const cookies = new Cookies(req, res)
const nextbnb_session = cookies.get('nextbnb_session')
const house = await HouseModel.findByPk(id)
const bookedDates = await getBookedDates(id)
return {
props: {
house: house.dataValues,
nextbnb_session: nextbnb_session || null,
bookedDates
}
}
}
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
export default function House({ house, nextbnb_session, bookedDates }) {
//...
<DateRangePicker
datesChanged={(startDate, endDate) => {
setNumberOfNightsBetweenDates(
calcNumberOfNightsBetweenDates(startDate, endDate)
)
setDateChosen(true)
setStartDate(startDate)
setEndDate(endDate)
}}
bookedDates={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:
export default function DateRangePicker({ datesChanged, bookedDates }) {
//...
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!
This content originally appeared on flaviocopes.com and was authored by flaviocopes.com
flaviocopes.com | Sciencx (2021-12-25T05:00:00+00:00) Airbnb clone, handle booked dates. Retrieved from https://www.scien.cx/2021/12/25/airbnb-clone-handle-booked-dates/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.