Airbnb clone, adding a new house

Join the 2022 Full-Stack Web Dev Bootcamp!


This post is part of a new series where we build a clone of Airbnb with Next.js. See the first post here.

In this lesson I want to add the ability to add a new house.

In the previous lesson I added a link to the /host/new URL, when we don’t have houses yet.

I am going to also add the link to the components/Header.js file, to make it easy to find, even though in a real app you’d move it somewhere less prominent, I think, as it’s not a functionality that’s used many times:

components/Header.js

<li>
  <Link href='/host/new'>
    <a>Add House</a>
  </Link>
</li>

Now create a new file pages/host/new.js

In this file we’re going to create a form to add a new house.

We start, by adding one house field, title, and we we submit the form to the server, on the host/new server-side route:

import { useState } from 'react'
import Head from 'next/head'

import Layout from '../../components/Layout'

const NewHouse = () => {
  const [title, setTitle] = useState('')

  return (
    <Layout
      content={
        <div>
          <Head>
            <title>Add a new house</title>
          </Head>

          <form
            onSubmit={async (event) => {
              event.preventDefault()
              try {
                const response = await axios.post('/api/host/new', {
                  house: {
                    title,
                  },
                })
                if (response.data.status === 'error') {
                  alert(response.data.message)
                  return
                }

                console.log(response)
                goto('/host')
              } catch (error) {
                alert(error.response.data.message)
                return
              }
            }}
          >
            <input
              id='title'
              type='text'
              placeholder='House title'
              onChange={(event) => setTitle(event.target.value)}
            />
            <button>Add house</button>
          </form>

          <style jsx>{``}</style>
        </div>
      }
    />
  )
}

export default NewHouse

We use hooks to store the state of each item in our form.

Now create a new endpoint POST /api/host/new in the server.js file, that uses the Sequelize model to add the house to the database:

server.post('/api/host/new', async (req, res) => {
  const houseData = req.body.house

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

    return
  }

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

I basically check if the user is logged in, and then add its id to the house data, before storing it.

If you try it now, in the terminal you’ll get a long list of errors from Sequelize:

Unhandled rejection SequelizeValidationError: notNull Violation: house.picture cannot be null,
notNull Violation: house.type cannot be null,
notNull Violation: house.town cannot be null,
notNull Violation: house.price cannot be null,
notNull Violation: house.superhost cannot be null,
notNull Violation: house.guests cannot be null,
notNull Violation: house.bedrooms cannot be null,
notNull Violation: house.beds cannot be null,
notNull Violation: house.baths cannot be null,
notNull Violation: house.wifi cannot be null,
notNull Violation: house.kitchen cannot be null,
notNull Violation: house.heating cannot be null,
notNull Violation: house.freeParking cannot be null,
notNull Violation: house.entirePlace cannot be null
//...

because we set those fields to be NOT NULL in the database.

Now that we have a basic form set up, let’s add all the fields we need to avoid this error!

import { useState } from 'react'
import Head from 'next/head'
import axios from 'axios'
import Router from 'next/router'

import Layout from '../../components/Layout'

const NewHouse = () => {
  const [title, setTitle] = useState('')
  const [town, setTown] = useState('')
  const [price, setPrice] = useState(0)
  const [picture, setPicture] = useState('')
  const [description, setDescription] = useState('')
  const [guests, setGuests] = useState(0)
  const [bedrooms, setBedrooms] = useState(0)
  const [beds, setBeds] = useState(0)
  const [baths, setBaths] = useState(0)
  const [wifi, setWifi] = useState(false)
  const [kitchen, setKitchen] = useState(false)
  const [heating, setHeating] = useState(false)
  const [freeParking, setFreeParking] = useState(false)
  const [entirePlace, setEntirePlace] = useState(false)
  const [type, setType] = useState('Entire house')

  const houseTypes = ['Entire house', 'Room']

  return (
    <Layout
      content={
        <div>
          <Head>
            <title>Add a new house</title>
          </Head>

          <form
            onSubmit={async (event) => {
              event.preventDefault()
              try {
                const response = await axios.post('/api/host/new', {
                  house: {
                    title,
                    town,
                    price,
                    picture,
                    description,
                    guests,
                    bedrooms,
                    beds,
                    baths,
                    wifi,
                    kitchen,
                    heating,
                    freeParking,
                    entirePlace,
                    type,
                  },
                })
                if (response.data.status === 'error') {
                  alert(response.data.message)
                  return
                }

                Router.push('/host')
              } catch (error) {
                alert(error.response.data.message)
                return
              }
            }}
          >
            <p>
              <label>House title</label>
              <input
                required
                onChange={(event) => setTitle(event.target.value)}
                type='text'
                placeholder='House title'
              />
            </p>
            <p>
              <label>Town</label>
              <input
                required
                onChange={(event) => setTown(event.target.value)}
                type='text'
                placeholder='Town'
              />
            </p>
            <p>
              <label>Price per night</label>
              <input
                required
                onChange={(event) => setPrice(event.target.value)}
                type='number'
                placeholder='Price per night'
                value={price}
              />
            </p>
            <p>
              <label>House picture URL</label>
              <input
                required
                onChange={(event) => setPicture(event.target.value)}
                type='text'
                placeholder='House picture URL'
              />
            </p>
            <p>
              <label>House description</label>
              <textarea
                required
                onChange={(event) => setDescription(event.target.value)}
              ></textarea>
            </p>

            <div>
              <div>
                <p>
                  <label>Number of guests</label>
                  <input
                    required
                    onChange={(event) => setGuests(event.target.value)}
                    type='number'
                    placeholder='Number of guests'
                    value={guests}
                  />
                </p>
                <p>
                  <label>Number of bedrooms</label>
                  <input
                    required
                    onChange={(event) => setBedrooms(event.target.value)}
                    type='number'
                    placeholder='Number of bedrooms'
                    value={bedrooms}
                  />
                </p>
                <p>
                  <label>Number of beds</label>
                  <input
                    required
                    onChange={(event) => setBeds(event.target.value)}
                    type='number'
                    placeholder='Number of beds'
                    value={beds}
                  />
                </p>
                <p>
                  <label>Number of baths</label>
                  <input
                    required
                    onChange={(event) => setBaths(event.target.value)}
                    type='number'
                    placeholder='Number of baths'
                    value={baths}
                  />
                </p>
              </div>

              <div>
                <p>
                  <label>Does it have Wifi?</label>
                  <select
                    onChange={(event) => setWifi(event.target.value)}
                    value={wifi}
                  >
                    <option value='true'>Yes</option>
                    <option value='false'>No</option>
                  </select>
                </p>
                <p>
                  <label>Does it have a kitchen?</label>
                  <select
                    onChange={(event) => setKitchen(event.target.value)}
                    value={kitchen}
                  >
                    <option value='true'>Yes</option>
                    <option value='false'>No</option>
                  </select>
                </p>
                <p>
                  <label>Does it have heating?</label>
                  <select
                    onChange={(event) => setHeating(event.target.value)}
                    value={heating}
                  >
                    <option value='true'>Yes</option>
                    <option value='false'>No</option>
                  </select>
                </p>
                <p>
                  <label>Does it have free parking?</label>
                  <select
                    onChange={(event) => setFreeParking(event.target.value)}
                    value={freeParking}
                  >
                    <option value='true'>Yes</option>
                    <option value='false'>No</option>
                  </select>
                </p>
                <p>
                  <label>Is it the entire place?</label>
                  <select
                    onChange={(event) => setEntirePlace(event.target.value)}
                    value={entirePlace}
                  >
                    <option value='true'>Yes</option>
                    <option value='false'>No</option>
                  </select>
                </p>
                <p>
                  <label>Type of house</label>
                  <select
                    onChange={(event) => setType(event.target.value)}
                    value={type}
                  >
                    {houseTypes.map((item, key) => (
                      <option value={item} key={key}>
                        {item}
                      </option>
                    ))}
                  </select>
                </p>
              </div>
            </div>

            <button>Add house</button>
          </form>

          <style jsx>{`
            input[type='number'],
            select,
            textarea {
              display: block;
              padding: 20px;
              font-size: 20px !important;
              width: 100%;
              border: 1px solid #ccc;
              border-radius: 4px;
              box-sizing: border-box;
              margin-bottom: 10px;
            }
          `}</style>
        </div>
      }
    />
  )
}

export default NewHouse

I imported the Next.js Router:

import Router from 'next/router'

so that after the form is successfully submitted, we can call Router.push('/host') to redirect the user to the list of houses.

I used the houseTypes array in one select to show you how to dynamically add data to a form.

The house picture is a path, either an absolute URL or relative to the base path.

I also set all the fields as required, frontend-side, so we get automatic validation by the browser.

Great! The form should show up nicely now.

Next part: Airbnb clone, edit houses

Want to become a better Web Developer? Join the 2022 Web Development Bootcamp!

⭐️⭐️⭐️ Join the 2022 Web Development Bootcamp ⭐️⭐️⭐️