Airbnb clone, edit houses

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 the previous lesson we let people add a new house. In this lesson we’ll let them edit houses they already added.

Create a file named pages/host/[id].js.

In there, we’ll create the form to edit the house.

Like we did in the pages/houses/[id].js, we can get the data of the house in the getInitialProps function, where we’ll get the id of the house in the query value.

Now that we have this information, we can ask the server for the house data. We’ll reuse an endpoint we already created, which listens for /api/houses/:id:

pages/host/[id].js

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

const EditHouse = props => {
  return <Layout content={<div>{props.house.title}</div>} />
}

EditHouse.getInitialProps = async ({ query }) => {
  const { id } = query
  const response = await axios.get(`http://localhost:3000/api/houses/${id}`)

  return {
    house: response.data
  }
}

export default EditHouse

We import axios and we make the call to /api/houses/${id} inside getInitialProps, assigning the data we get from that endpoint to the house prop.

Cool! Now we can add the form to the template.

I already envision this will be a lot similar to the one we defined in pages/host/new.js, so it’s best to extract that to its own component, which we store into components/HouseForm.js. It’s very bad, in my opinion, to have the same exact markup twice, and this is the perfect place to introduce a generalized component, where to extract this HTML.

In this file, we get the house data via props, and if this is loaded by the pages/host/[id].js component, which is the one we use to edit the house, this object will be present.

Otherwise, for pages/host/new.js, there will be no props, and we’ll use the default values as before.

We also pass an edit prop to the component, so we know that it’s in editing mode, or in “new” mode.

I also took the opportunity to make the form look nicer, using a grid.

Here it is:

components/HouseForm.js

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

const HouseForm = props => {
  const id = (props.house && props.house.id) || null

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

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

  return (
    <div>
      <form
        onSubmit={async event => {
          event.preventDefault()
          try {
            const response = await axios.post(
              `/api/host/${props.edit ? 'edit' : 'new'}`,
              {
                house: {
                  id: props.edit ? id : null,
                  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'
            value={title}
          />
        </p>
        <p>
          <label>Town</label>
          <input
            required
            onChange={event => setTown(event.target.value)}
            type='text'
            placeholder='Town'
            value={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'
            value={picture}
          />
        </p>
        <p>
          <label>House description</label>
          <textarea
            required
            onChange={event => setDescription(event.target.value)}
            value={description}></textarea>
        </p>

        <div className='grid'>
          <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>

        {props.edit ? <button>Edit house</button> : <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;
        }
        form p {
          display: grid;
        }

        .grid {
          display: grid;
          grid-template-columns: 50% 50%;
        }

        .grid > div {
          padding: 50px;
        }
      `}</style>
    </div>
  )
}

export default HouseForm

Now pages/host/new.js is much smaller:

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

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

const NewHouse = () => {
  return (
    <Layout
      content={
        <>
          <Head>
            <title>Add a new house</title>
          </Head>

          <HouseForm edit={false} />
        </>
      }
    />
  )
}

export default NewHouse

And here is our pages/host/[id].js:

import axios from 'axios'
import Layout from '../../components/Layout'
import HouseForm from '../../components/HouseForm'
import Head from 'next/head'

const EditHouse = props => {
  return (
    <Layout
      content={
        <>
          <Head>
            <title>Edit house</title>
          </Head>

          <HouseForm edit={true} house={props.house} />
        </>
      }
    />
  )
}

EditHouse.getInitialProps = async ({ query }) => {
  const { id } = query
  const response = await axios.get(`http://localhost:3000/api/houses/${id}`)

  return {
    house: response.data
  }
}

export default EditHouse

See? Here we pass the house prop, which contains the house data.

Now we need to define an API that listens on POST /api/host/edit in server.js:

server.js

server.post('/api/host/edit', async (req, res) => {

})

Ind in there we must call the update() function on the Sequelize House model, but before (after checking if the user is logged in) we make sure the current user is also the owner of the house, to disallow editing other people’s houses.

How? We get the user email, we ask the User model to find that user, we check if the user is the host, finally we update the data.

Remember? We have already used the update() method in the Stripe webhook handler.

This is the code:

server.post('/api/host/edit', 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 => {
    House.findByPk(houseData.id).then(house => {
      if (house) {
        if (house.host !== user.id) {
          res.writeHead(403, {
            'Content-Type': 'application/json'
          })
          res.end(
            JSON.stringify({
              status: 'error',
              message: 'Unauthorized'
            })
          )

          return
        }

        House.update(houseData, {
          where: {
            id: houseData.id
          }
        })
          .then(() => {
            res.writeHead(200, {
              'Content-Type': 'application/json'
            })
            res.end(JSON.stringify({ status: 'success', message: 'ok' }))
          })
          .catch(err => {
            res.writeHead(500, {
              'Content-Type': 'application/json'
            })
            res.end(JSON.stringify({ status: 'error', message: err.name }))
          })
      } else {
        res.writeHead(404, {
          'Content-Type': 'application/json'
        })
        res.end(
          JSON.stringify({
            message: `Not found`
          })
        )
        return
      }
    })
  })
})

A lot of which is just for handling the errors and various edge cases.

Next part: Airbnb clone, safe HTML for the house description

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

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