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.
The code for this lesson is available at https://github.com/flaviocopes/airbnb-clone-react-nextjs/commit/a755342ec14e62318306f13023d05eb897801f54