DeveloperHandbook.com

Adding the finishing touches to 'Property Finder'

Published


At the beginning of this project, we created two components. Those components were designed to display key information about a single property and its location. These components were <Map /> and <KeyFeatures />.

Currently, these components are hard coded and not very useful. In fact, they show the exact same information for every property. Not useful at all! Let’s rectify that.

The single best way to learn anything is repeated exposure. For this post we will not be learning anything new, but instead utilising the skills we have learnt in previous tutorials.

When done, our property details page will go from this;

Property Details App - Incomplete

To this;

Property Details App - Completed

Believe it or not, we are 3/4 of the way there, so let’s get cracking!

Note for clarification as this is also a standalone post. This post is part of a mini-series where we are building a real estate property listing website, complete with routing and advanced forms.

Removing hard coded property details logic

Open Details/index.js and recall that we hard coded features for the <KeyFeatures /> component and we hard coded the address for <Map />.

The Details page/component should not care about any specific state/data. Instead, the Details page should retrieve data from the React Context and pass it to another component where that data should be rendered (recall, this is commonly referred to as the Container/Presentation pattern).

Create a new presentation component called propertyDetails inside the components folder and create a new file called index.js. Add the following logic;

import * as React from "react"
import classnames from "classnames"

import KeyFeatures from "../keyFeatures"
import Map from "../map"

function PropertyDetails({ listing }) {
  if (!listing) {
    return null
  }

  const { title, address, description, price, features, details } = listing
  const priceClasses = classnames("text-success", "text-right")

  return (
    <div>
      <div className="columns">
        <div className="column col-9 col-xs-12">
          <h2>{title}</h2>
          <h3 className="text-dark text-small mb-1">{description}</h3>
        </div>
        <div className="column col-3 col-xs-12">
          <h5 className={priceClasses}>
            <small>Priced from</small>
            <br />
            &pound;
            {price}
          </h5>
        </div>
      </div>
      <div className="columns">
        <div className="column col-6 col-xs-12" />
        <div className="column col-6 col-xs-12">
          <KeyFeatures features={features} />
        </div>
      </div>
      <p className="text-bold mt-3">Full Details</p>
      {details.map((detail) => (
        <p key={detail}>{detail}</p>
      ))}
      <p className="text-bold mt-3">Map</p>
      <Map address={address} />
    </div>
  )
}

export default PropertyDetails

There are a few key differences here to our currently hard coded component. Let’s walk through those changes;

How to retrieve a single property/listing

We already know the ID for our property/listing. Observe that when you click on a listing on the home page, the ID for the listing is appended to the URL.

This was achieved because when we configured our router in src/index.js, we used a placeholder :propertyId to indicate that a variable called propertyId was required and that value was not constant but could vary.

<Details path="/details/:propertyId" />

Our PropertyListingsProvider already knows how to fetch data from our ‘server’, so we can re-use this with a couple of small modifications. We need some way of fetching a single listing rather than all of them.

Open PropertyListingsProvider.js and add the following function to PropertyListingsProvider;

getListingByPropertyId = (propertyId) => {
  const { propertyListings } = this.state
  return propertyListings.find((listing) => listing.id === Number(propertyId))
}

This function accepts a propertyId as a parameter, and returns the single listing who’s ID matches that value.

Now we just need to update the PropertyListingsContext.Provider to make this function available to us;

<PropertyListingsContext.Provider
  value={{
    allListings: propertyListings,
    propertyListings: filteredListings,
    updateFilter: this.updateFilter,
    getListingByPropertyId: this.getListingByPropertyId,
  }}
>
  {children}
</PropertyListingsContext.Provider>

Now we can go ahead and update our Details page (Details/index.js) to call the getListingsByPropertyId function with the propertyId we get from @reach/router, as follows;

import * as React from "react"

import {
  PropertyListingsProvider,
  PropertyListingsConsumer,
} from "../../context/PropertyListingsProvider"

import PropertyDetails from "../../components/propertyDetails"

function Details({ propertyId }) {
  return (
    <div className="container">
      <PropertyListingsProvider>
        <PropertyListingsConsumer>
          {({ getListingByPropertyId }) => (
            <PropertyDetails listing={getListingByPropertyId(propertyId)} />
          )}
        </PropertyListingsConsumer>
      </PropertyListingsProvider>
    </div>
  )
}

export default Details

Open the Details page in your browser and you should see that we have made tremendous progress.

Details Page Coming Together

We have two missing features, the image for the listing and the rest of the layout, as we see on the home page.

The ‘server’ we have created returns us a path to an image for each listing. We can use this path to display an image for the property to the user.

Create a new folder in components called gallery, and a new file called index.js.

Add the following code;

import * as React from "react"

function Gallery({ image, title }) {
  return (
    <figure className="figure">
      <img className="img-responsive" src={`/server/${image}`} alt={title} />
      <figcaption className="figure-caption text-center text-small">
        {title}
      </figcaption>
    </figure>
  )
}

export default Gallery

Our <Gallery /> component displays the image, and a caption, both of which need to be supplied via props.

Open propertyDetails/index.js and look for the empty column where the gallery should live, it will look like this;

<div className="column col-6 col-xs-12" />

Expand this tag and drop the <Gallery /> component inside;

<div className="column col-6 col-xs-12">
  <Gallery image={image} title={title} />
</div>

We are already destructuring title from listing, so we only need to add the destructure for image as follows;

const { title, address, description, price, features, details, image } = listing

And remember to add an import for <Gallery /> as follows;

import Gallery from "../gallery"

The image for the property should now be visible.

Finalising the layout

Our page is missing the <Hero /> banner, so let’s correct that. Open Details/index.js and import the <Hero /> component;

import Hero from "../../components/hero"

Then place the <Hero /> component directly above our container div;

function Details({ propertyId }) {
  return (
    <React.Fragment>
      <Hero />
      <div className="container">
        {
          // ...
        }
      </div>
    </React.Fragment>
  )
}

Small details

Feel free to skip this part, as the layout for the page is now basically complete. However, there are a couple of small refinements we can make to make the UI a bit more attractive.

Open <Hero /> and update as follows;

import * as React from "react"
import classnames from "classnames"

import styles from "./styles.module.css"

function Hero({ miniHero }) {
  const classes = classnames(styles.hero, "hero", "mb-3", {
    "hero-sm": miniHero,
    [styles.miniHero]: miniHero,
    "hero-lg": !miniHero,
  })

  return (
    <div className={classes}>
      <div className="hero-body text-center text-light">
        <h1>Premium Property Finder</h1>
        <p className="mb-0">
          Bringing premium property right to your fingertips
        </p>
      </div>
    </div>
  )
}

export default Hero

We are accepting miniHero as a prop. This prop applies a CSS class to shrink the hero banner down so it does not dominate the page as much.

Create a new file in the same directory called styles.module.css and add the following styling;

.hero {
  background-image: url("../../images/hero.jpg");
  background-repeat: no-repeat;
  background-size: cover;
  background-attachment: scroll;
}

@media screen and (min-width: 992px) {
  .miniHero {
    background-position-y: -150px;
  }
}

Finally we need to pass miniHero to our <Hero /> component in Details/index.js as follows;

<Hero miniHero />

That should take care of the visual appearance of the <Hero /> banner.

Summary

Congratulations! You made it to the end! This has been a comprehensive course where we have covered most of the important concepts of creating a React based property listings website. In this series you have learnt how to get started quickly, how to use build tooling (Parcel.js), how to work with forms, how to use React Context API, and so much more. I hope you enjoyed it and more importantly, I hope you take the knowledge gained and apply it to your projects going forward.