Restaurant Edit
Reading
Restaurant Edit
The setup for a restaurant edit will be very similar to creating a new restaurant. In fact, most of the JSX will be the same, except we will first load the information about a restaurant into the state before showing the form itself.
We will begin by creating the basic structure of the restaurant edit in
EditRestaurant.tsx
Creating EditRestaurant.tsx
import React, { useState } from 'react'import { Link, useHistory } from 'react-router-dom'import { useDropzone } from 'react-dropzone'import { authHeader } from '../auth'export function EditRestaurant() {const history = useHistory()const [isUploading, setIsUploading] = useState(false)const [errorMessage, setErrorMessage] = useState()const [restaurant, setRestaurant] = useState({name: '',description: '',address: '',telephone: '',photoURL: '',})const { getRootProps, getInputProps, isDragActive } = useDropzone({onDrop: onDropFile,})function handleStringFieldChange(event) {const value = event.target.valueconst fieldName = event.target.nameconst updatedRestaurant = { ...restaurant, [fieldName]: value }setRestaurant(updatedRestaurant)}async function handleFormSubmit(event) {event.preventDefault()const response = await fetch(`/api/Restaurants/${id}`, {method: 'PUT',headers: { 'content-type': 'application/json', ...authHeader() },body: JSON.stringify(restaurant),})if (response.status === 401) {setErrorMessage('Not Authorized')} else {if (response.status === 400) {const json = await response.json()setErrorMessage(Object.values(json.errors).join(' '))} else {history.push('/')}}}async function onDropFile(acceptedFiles) {// Do something with the filesconst fileToUpload = acceptedFiles[0]console.log(fileToUpload)// Create a formData object so we can send this// to the API that is expecting some form data.const formData = new FormData()// Append a field that is the form upload itselfformData.append('file', fileToUpload)try {setIsUploading(true)// Use fetch to send an authorization header and// a body containing the form data with the fileconst response = await fetch('/api/Uploads', {method: 'POST',headers: {...authHeader(),},body: formData,})setIsUploading(false)// If we receive a 200 OK response, set the// URL of the photo in our state so that it is// sent along when creating the restaurant,// otherwise show an errorif (response.status === 200) {const apiResponse = await response.json()const url = apiResponse.urlsetRestaurant({ ...restaurant, photoURL: url })} else {setErrorMessage('Unable to upload image')}} catch (error) {// Catch any network errors and show the user we could not process their uploadconsole.debug(error)setErrorMessage('Unable to upload image')setIsUploading(false)}}let dropZoneMessage = 'Drag a picture of the restaurant here to upload!'if (isUploading) {dropZoneMessage = 'Uploading...'}if (isDragActive) {dropZoneMessage = 'Drop the files here ...'}return (<main className="page"><nav><Link to="/"><i className="fa fa-home"></i></Link><h2>Edit Restaurant</h2></nav><form onSubmit={handleFormSubmit}>{errorMessage ? <p>{errorMessage}</p> : null}<p className="form-input"><label htmlFor="name">Name</label><inputtype="text"name="name"value={restaurant.name}onChange={handleStringFieldChange}/></p><p className="form-input"><label htmlFor="description">Description</label><textareaname="description"value={restaurant.description}onChange={handleStringFieldChange}></textarea><span className="note">Enter a brief description of the restaurant.</span></p><p className="form-input"><label htmlFor="name">Address</label><textareaname="address"value={restaurant.address}onChange={handleStringFieldChange}></textarea></p><p className="form-input"><label htmlFor="name">Telephone</label><inputtype="tel"name="telephone"value={restaurant.telephone}onChange={handleStringFieldChange}/></p>{restaurant.photoURL ? (<p><img alt="Restaurant Photo" width={200} src={restaurant.photoURL} /></p> : null)}<div className="file-drop-zone"><div {...getRootProps()}><input {...getInputProps()} />{dropZoneMessage}</div></div><p><input type="submit" value="Submit" /></p></form></main>)}
This is essentially a duplication of the NewRestaurant with a few variables
changed. For example newRestaurant becomes restaurant. We also change the
API's use of POST to PUT so we are no longer creating a restaurant but
updating the existing one. We also change the URL to /api/Restaurants/${id}
To fetch the editing restaurant, we'll add this code at the top of the component:
const params = useParams()const id = params.id
This will get the id from the route parameters. Even though we haven't written
the <Route> we know it will have an :id in the parameters.
Next, we add a useEffect to find and load this restaurant. This implementation
is in Restaurant.tsx, so we can essentially copy it from there
useEffect(() => {function fetchRestaurant() {const response = await fetch(`/api/Restaurants/${id}`)if (response.ok) {const apiData = await response.json()setRestaurant(apiData)}}fetchRestaurant()}, [id])
Just before the return of the main page, we can add logic to show the form
only when the restaurant is loaded:
// If we don't have any restaurant ID, return an empty componentif (!restaurant.id) {return <></>}
Update our routes in App.tsx
We'll add the following route to allow a path to the EditRestaurant component.
<Route exact path="/restaurants/:id/edit"><EditRestaurant /></Route>
Using the route from Restaurant.tsx
Next, we will add an Edit button on the Restaurant.tsx page to send us to
the /restaurants/:id/edit URL. We only show this link if the user is logged in
and the user id for this restaurant is the same as the logged-in user.
{isLoggedIn() && restaurant.userId === getUserId() ? (<p><Link className="button" to={`/restaurants/${id}/edit`}>Edit</Link></p>) : null}
Protecting the controller
We should also make a similar change to the controller. The controller should ensure the user is logged in and the current user is the same as the user that created the restaurant itself.
First, add
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] to
the PutRestaurant method.
Before all the other logic in the method, we will add this code to find the restaurant in the database and check its user id. If these do not match, we'll return a custom unauthorized message.
// Find this restaurant by looking for the specific idvar restaurantBelongsToUser = await _context.Restaurants.AnyAsync(restaurant => restaurant.Id == id && restaurant.UserId == GetCurrentUserId());if (!restaurantBelongsToUser){// Make a custom error responsevar response = new{status = 401,errors = new List<string>() { "Not Authorized" }};// Return our error with the custom responsereturn Unauthorized(response);}
With this, we guarantee that the controller only allows authorized users to edit a restaurant.