About the Author:

Gatsby Tutorial Post Part 2: NetlifyCMS and Styling

April 29th, 2019

This is the 2nd part of a tutorial series on building fast and elegant sites with Gatsby, Material-ui, and NetlifyCMS (part 1 here). The previous part of the tutorial showed how to setup Gatsby and create pages manually as well as dynamically using gatsby-node.js. This part of the series will show the following:

  • Creating the Courses Page and Individual Courses
  • Adding Images to the Courses/Posts
  • Install Material-UI plugins and style all pages with Material-UI
  • Installing NetlifyCMS to allow non-technical users to make content changes
  • Adding More Fields Types to the Courses (that work out of the box with NetlifyCMS)

Demo Site (Here’s where we’re going to get to)

Github Repo (If you’d like to jump right into the code)

Creating the Courses Page and Individual Courses

Our demo site features both blogs (which we did previously) and courses (which we’ll do now). Similar to the blogs, this involves creating a parent /courses page, a course template, a courseitem component (for the list on /courses, and the markdown files for the courses. So let’s first create the main /courses page (that will list all of the courses) and pages for each course.

Here’s how I created each of the pages:

/pages/courses.js

import React from "react"
import { Link, StaticQuery, graphql } from "gatsby"

import Layout from "../components/layout"
import SEO from "../components/seo"
import CourseItem from "../components/CourseItem"

class CoursePage extends React.Component {
 render() {
   const { data } = this.props

   const { edges: posts } = data.allMarkdownRemark

   return (
     <div>
       <Layout>
         <SEO title="Courses Page" />
         <h1>Courses Page</h1>

         <div
           style={{
             display: "flex",
             justifyContent: "space-evenly",
             flexWrap: "wrap",
           }}
         >
           {posts &&
             posts.map(({ node: post }) => (
               <CourseItem
                 key={post.id}
                 post={post.frontmatter}
                 style={{ marginRight: 10, width: "50%" }}
                 slug={post.fields.slug}
                 excerpt={post.excerpt}
               />
             ))}
         </div>
       </Layout>
     </div>
   )
 }
}

export default () => (
 <StaticQuery
   query={graphql`
     query CoursePageQuery {
       allMarkdownRemark(
         sort: { order: DESC, fields: [frontmatter___date] }
         filter: { frontmatter: { templateKey: { eq: "single-course" } } }
       ) {
         edges {
           node {
             excerpt(pruneLength: 100)
             id
             fields {
               slug
             }
             frontmatter {
               title
               templateKey
               date(formatString: "MMMM DD, YYYY")
             }
           }
         }
       }
     }
   `}
   render={data => <CoursePage data={data} />}
 />
)

/templates/single-course.js

import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import { Link } from "gatsby"

const CoursePage = ({ data }) => {
 const { markdownRemark: post } = data

 return (
   <Layout>
     <Link to="/courses">
       <p>← Back to Courses</p>
     </Link>

     <h1>{post.frontmatter.title}</h1>
     <h4>{post.frontmatter.date}</h4>
     <p dangerouslySetInnerHTML={{ __html: post.html }} />
   </Layout>
 )
}

export default CoursePage

export const CoursePageQuery = graphql`
 query CoursePage($id: String!) {
   markdownRemark(id: { eq: $id }) {
     html
     frontmatter {
       title
     }
   }
 }
`

/components/courseitem.js

import React from "react"
import { Link } from "gatsby"

function CourseItem(props) {
 const { post, slug, excerpt } = props

 return (
   <div>
     <Link to={slug}>
       <h1>{post.title}</h1>
     </Link>
     <h3>{excerpt}</h3>
   </div>
 )
}

export default CourseItem

/courses/intermediate-react.md

---

templateKey: single-course

title: intermediate React

date: 2019-04-15T16:43:29.834Z

description: An intermediate React course

difficulty: Intermediate

---

what a course, what a course…….

/courses/cool-course.md etc

---

templateKey: single-course

title: Great course on JS

date: 2019-04-15T16:43:29.834Z

description: A great course

difficulty: Beginner

---

what a course, what a course...what a course, what a coursewhat a course, what a course...what a course, what a course

what a course, what a course...what a course, what a course

what a course, what a course...what a course, what a course

what a course, what a course...what a course, what a course

What’s going on here? Let’s review:

Gatsby-node.js creates the course pages using the appropriate template with this section of the code:

...

component: path.resolve(

`src/templates/${String(edge.node.frontmatter.templateKey)}.js`

),

…

In this case, it would be using single-course.js.

Within single-course.js there is a page query at the bottom. This queries for the right markdown file (using the id found in context) so that those specific pages will have the right title and html (from markdown) to display.

Within the courses.js file, we’re using a Static Query to query/search for the courses that have the templateKey of single-course and making a list of the courses (using the simple CourseItem.js component for help). This gives us a very basic (but functional) /courses page like this:

And an individual course page like this:

So these are pretty bland pages, unless you’re a hardcore minimalist. I’ll show you some Gatsby image plugins now that will add some color/life to these posts/courses.

Adding Images to the Courses/Posts

One of the top selling points of Gatsby is speed. Poorly sized or slow loading images are one of the easiest ways to make a website feel sluggish so this is an obvious area to optimize first. Two Gatsby plugins (gatsby-image and gatsby-transformer-sharp) are amazing tools for working with and optimizing Gatsby images. The latter leverages the Sharp image library, which is a very popular node module for resizing images. I’ll show you now to how add images to the course and post pages.

First, we need to install the plugins:

npm install --save gatsby-image gatsby-transformer-sharp gatsby-plugin-sharp

We’re also going to add a plugin that will allow us to access relative image paths in the project:

npm install gatsby-remark-relative-images --save

This will make for working with NetlifyCMS images easier later.

Then add them to the gatsby-config.js, within the big array of plugins like so:

 'gatsby-plugin-sharp',
    'gatsby-transformer-sharp',
    {
      resolve: 'gatsby-transformer-remark',
      options: {
        plugins: [
          {
            resolve: 'gatsby-remark-relative-images',
          },
          {
            resolve: 'gatsby-remark-images',
            options: {
              // It's important to specify the maxWidth (in pixels) of
              // the content container as this plugin uses this as the
              // base for generating different widths of each image.
            
              maxWidth: 590,
            },
          },
          {
            resolve: 'gatsby-remark-copy-linked-files',
            options: {
              destinationDir: 'static',
            },
          },
        ],
      },
    },
{
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/static/img`,
        name: 'uploads',
      },
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/pages`,
        name: 'pages',
      },
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/images`,
        name: 'images',
      },
    },

Now we can add the images to our posts. Create a folder called /static and a folder within that call img. That’s where the images will live. I downloaded some demo images for this fake courses and put them in the folder.

Now within the individual course markdown files, we need to add the images within the frontmatter (—) headings like so:

---

[more items]

image: /img/[imagename].jpeg

---

Now, we’ll edit the single-course.js page to be able to query for this image and render it on the page. Change the GraphQL query at the bottom of the page to include the image query, like so:

export const CoursePageQuery = graphql`
  query CoursePage($id: String!) {
    markdownRemark(id: { eq: $id }) {
      html
      frontmatter {
        title
        image {
          childImageSharp {
            fluid(maxWidth: 500, quality: 100) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
  }
`

This is using the gatsby-plugin-sharp plugin to render a fluid image with a maxWidth of 500 pixels. Right here within the GraphQL query, we’re able to choose how we’d like to receive the image in our component/html. This plugin has extensive documentation that can be found here.

Now, within the the single-course.js, we’ll use Gatsby-image to render the image in the html. It’s a simple import:

import Img from "gatsby-image"

And then a simple enough component to render.

<Img fixed={post.frontmatter.image.childImageSharp.fluid} />

This is how you add the right plugins, the files, and the queries to be able to use plugins within Gatsby.

Installing NetlifyCMS

NetlifyCMS (like an CMS) allows for non-technical users to edit content on a site. This allows people in marketing or content roles the freedom to create and frees up developers from having to do these sorts of small tasks. As was mentioned before, NetlifyCMS is made by static hosting service Netlify, but they can each be used separately from each other. Gatsby can be easily configured to work with WordPress, Contently, and many other data sources. NetlifyCMS has great docs, a generous free tier, and easy-to-use authentication.

NetlifyCMS is easy to configure with Gatsby using, you probably guessed it, gatsby-plugin-netlify-cms. Install it using npm and add it to gatsby-config.js:

npm install gatsby-plugin-netlify-cms --save

// gatsby-node.js

},.......[other plugins]

`gatsby-plugin-netlify-cms`,

….[other plugins] {

NetlifyCMS needs a config.yml file, which tells it which fields can be changed within the CMS. Create a folder called ‘admin’ within the Static folder and put the following config.yml file in it:

backend:
  name: git-gateway
  branch: master

media_folder: static/img
public_folder: /images

collections:
  - name: "pages"
    label: "Pages"
    files:    
      - file: "src/pages/about.md"
        label: "About"
        name: "about"
        fields:
          - {
              label: "Template Key",
              name: "templateKey",
              widget: "hidden",
              default: "about-page",
            }
          - { label: "Title", name: "title", widget: "string" }
          - { label: "Body", name: "body", widget: "markdown" }
  - name: "blog"
    label: "Blog"
    folder: "src/pages/blogs"
    create: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - {
          label: "Template Key",
          name: "templateKey",
          widget: "hidden",
          default: "single-blog",
        }
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Publish Date", name: "date", widget: "datetime" }
      - { label: "Description", name: "description", widget: "text" }
      - { label: "Body", name: "body", widget: "markdown" }

  - name: "courses"
    label: "Courses"
    folder: "src/pages/courses"
    create: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - {
          label: "Template Key",
          name: "templateKey",
          widget: "hidden",
          default: "single-course",
        }
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Publish Date", name: "date", widget: "datetime" }
      - { label: "Description", name: "description", widget: "text" }
      - { label: "Body", name: "body", widget: "markdown" }
      - { label: Image, name: image, widget: image }

This creates 3 collections within the CMS: one for updating the about page, one for the courses, and one for the blogs. The ‘widgets’ are NetlifyCMS widgets and should be self-explanatory in a moment. With the above config.yml, this is what my admin interface looks like:

Within the Courses Collection, I see this:

Take a look, again, at the config.yml.

Notice how the fields with this file line up with what you’re seeing in the CMS image above? The template key is hidden so it doesn’t show. But right there are string widget (aka text field) for Title, Description, and Body. And there’s a datetime widget (a datepicker) for Publish Data. NetlifyCMS is really just an interface for making changes to our markdown files here. The config.yml serves as a way to tell Netlify which fields are available for editing. NetlifyCMS features a wide variety of default widgets and you can even make custom ones.

Source: https://www.netlifycms.org/docs/widgets/

Although this takes some setup work, this workflow is extremely powerful for using Github markdown files as a CMS.

The final thing needed here is making the images hosted in static/img available for NetlifyCMS to use.

Add this to the top of the file within Gatsby-node.js

const { fmImagesToRelative } = require('gatsby-remark-relative-images')

And add this method call within the onCreateNode block:

exports.onCreateNode = ({ node, actions, getNode }) => {

const { createNodeField } = actions

fmImagesToRelative(node)

This plugin was specifically built to make NetlifyCMS play nice with relative image paths. Failure to add this config right will send you to bug hell (e.g. “Field “image” must not have a selection since type “String” has no subfields.”)

.

Styling it all with Material-UI

We’ve built out our posts, blogs, and about page. Now we’ll see how to style out all of the pages using the most popular React UI framework, Material-ui. Material-ui has nearly 50,000 stars on Github and over 1,000 contributors. It is a React implementation of Google’s Material Design principles. Material-ui gives React developers a wide variety of components, styling, and utilities for making their sites aesthetically pleasing and easy to use. If you don’t want your site to look like a generic Googly app, Material-ui is supremely easy to customize to your liking.

There are many ways to add Material-ui to a Gatsby project. Material-ui has a Gatsby starter to use. Another developer made a dedicated Gatsby-starter with Material-ui. But I find the easiest way to add Material-ui to Gatsby is with gatsby-plugin-material-ui. The previous two work but they are much more complicated. Plugins are one of the awesome parts of Gatsby: Just install the plugin, add it to gatsby-config.js and you’re good to go. Here’s how to do just that:

npm install gatsby-plugin-material-ui @material-ui/styles

Edit gatsby-config.js

module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-material-ui`,
      options: {
      },
    },
  ],
};

If you’ve worked with Material-ui before, you’ve probably played around with its theming capabilities. Material-ui themes allow you to establish a global theme that can be called from various child components. This makes it easier to create one coherent look and feel (vs. having pages/components with different styles, which can look sloppy). With the gatsby-plugin-material-ui plugin, this theme will go in that options object in the config above like so:

options: {        
        theme: {
          palette: {
              primary: {
                  main: '#BA3D3B', // new color here 
              } 
          },
      },

Now we can import material-ui components anywhere within the our Gatsby project. Here’s how I changed the single-course.js page:

imports...
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
...
import Avatar from '@material-ui/core/Avatar';

const styles = theme => ({
  root: {
    flexGrow: 1,
  },
  paper: {
    padding: theme.spacing.unit * 2,
    textAlign: 'center',
    color: theme.palette.text.secondary,
  },
  title:{
    marginBottom:'.2em'
  },
  backButton: {
    textDecoration:'none'
  },
  bigAvatar: {
    '& img':{
      margin: 10,
      width: 60,
      height: 60,
    },
    width:100,
    height:100,
    border:'1px solid grey'
  
  },
});

const CoursePage = ({ data, classes }) => {
  const { markdownRemark: post } = data

  return (
    <Layout>
       <Link to='/courses' className={classes.backButton}>
      <p>← Back to Courses</p>

      </Link>
      
      <Grid container spacing={24}>
       
        <Grid item xs={9}>
          <h1 className={classes.title}>{post.frontmatter.title}</h1>
          <h4>{post.frontmatter.date}</h4>
          <p dangerouslySetInnerHTML={{ __html: post.html }}/>  
        </Grid>    
        <Grid item xs={3}>
        <Avatar src={post.frontmatter.image.childImageSharp.fluid.src} className={classes.bigAvatar} />
       
        </Grid>   
      </Grid>
  
    </Layout>
  )
}
export default withStyles(styles)(CoursePage);

[page query removed for brevity]

Here’s how I changed the single-blog template:

...
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
...
import Layout from '../components/layout'
import Img from "gatsby-image"
import { Link } from "gatsby"

const styles = theme => ({
  root: {
    flexGrow: 1,
  },
  paper: {
    padding: theme.spacing.unit * 2,
    textAlign: 'center',
    color: theme.palette.text.secondary,
  },
  title:{
    marginBottom:'.2em'
  },
  backButton: {
    textDecoration:'none'
  }
});

const BlogPage = ({ data, classes }) => {
  const { markdownRemark: post } = data

  return (
    <Layout>
      <div className={classes.root}>
      <Link to='/blog' className={classes.backButton}>
      <p>← Back to Blog</p>

      </Link>
      
      <Grid container spacing={24}>
        <Grid item xs={3}>
          <Img fluid={post.frontmatter.image.childImageSharp.fluid} />
        </Grid>
        <Grid item xs={9}>
          <h1 className={classes.title}>{post.frontmatter.title}</h1>
          <h4>{post.frontmatter.date}</h4>
          <p dangerouslySetInnerHTML={{ __html: post.html }}/>  
        </Grid>       
      </Grid>
      </div>
    
    </Layout>
  )
}


export default withStyles(styles)(BlogPage);

[page query removed for brevity]
`

Here’s the about page:

...
import { withStyles } from '@material-ui/core/styles';

const styles = theme => ({
  heroText: {
    color:'white',
    textAlign: 'center',
    lineHeight:7,
    marginTop:-20
  },
  mainBlogCopy: {
    marginTop: theme.spacing.unit,
  },
  blogText:{
    color:theme.palette.primary.main
  }
});

const AboutPage = ({ data, classes }) => {
  const { markdownRemark: post } = data

  return (
    <Layout>
      <div style={{
        backgroundImage: `url(${post.frontmatter.image.childImageSharp.fluid.src})`,
        height:300
      }}>
      <h1 className={classes.heroText}>{post.frontmatter.title}</h1>
      </div>
      <div className={classes.mainBlogCopy}>
       <p className={classes.blogText} dangerouslySetInnerHTML={{ __html: post.html }}/> 
      </div>
      
    </Layout>
  )
}

AboutPage.propTypes = {
  data: PropTypes.object.isRequired,
}

export default withStyles(styles)(AboutPage);

[page query removed for brevity]

Here’s what I added to the home page to make it look a little more like appendTo:

….
import { Link, StaticQuery, graphql } from "gatsby"
import Grid from "@material-ui/core/Grid"
import BlogItem from "../components/BlogItem"
import { withStyles, withTheme } from "@material-ui/core/styles"
import Button from "@material-ui/core/Button"
import Typography from '@material-ui/core/Typography'
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';


const styles = theme => ({
  mainBlogArea: {
    paddingTop: '20px !important',

  },
  redBox:{
    padding:30,
    paddingTop:50,
    height:200,
    backgroundColor:'#AC4839',
    marginBottom:30
  },
  greyBox:{
    padding:30,
    paddingTop:50,
    height:200,
    backgroundColor:'#D9D8D8'
  },
  blackButton:{
    backgroundColor:'black',
    color:'white'

  },
  redButton:{
    backgroundColor:'#AC4839',
    color:'white'

  },
  TabsSection:{
    marginTop:30,
    backgroundColor:'white',
    border:'1px solid grey',
    height:300,
  },
  Tab:{
      width:10
  }

  
})

const IndexPage = props => {
  const { data, classes } = props
  // const { edges: posts } = data.allMarkdownRemark

  return (
    <Layout>
      <SEO title="appendTo Home" keywords={[`Courses`, `Training`, `react`]} />

      <Grid container spacing={24}  className={classes.mainBlogArea}>
        <Grid item xs={8}>
          <div >
            {data.map(item => (
              <BlogItem
                key={item.id}
                post={item.frontmatter}
                image={item.frontmatter.image.childImageSharp.fluid.src}
                slug={item.fields.slug}
                date={item.frontmatter.date}
              />
            ))}
          </div>
        </Grid>
        <Grid item xs={4}>
          <div className={classes.redBox}>
            <Typography variant="h5" style={{color:'white'}}>
              Custom Private Courses
            </Typography>
            <Button variant="contained" className={classes.blackButton}>
              Get Started
            </Button>
          </div>

          <div className={classes.greyBox}>
          <Typography variant="h5">
              Live Public Courses
            </Typography>
            <Button variant="contained" className={classes.redButton}>
              Sign Up Today
            </Button>
          </div>

          <div className={classes.TabsSection} >
          <AppBar position="static">
            <Tabs>
              <Tab label="Popular" className={classes.Tab} />
              <Tab label="Recent" className={classes.Tab} />
        
            </Tabs>
            </AppBar>


          </div>
        </Grid>
      </Grid>
    </Layout>
  )
}

const StyledUpIndexPage = withStyles(styles)(IndexPage)

export default () => (
  <StaticQuery
    query={graphql`
      query IndexPageQuery {
        allMarkdownRemark(
          sort: { order: DESC, fields: [frontmatter___date] }
          filter: { frontmatter: { templateKey: { eq: "single-blog" } } }
        ) {
          edges {
            node {
              excerpt(pruneLength: 40)
              id
              fields {
                slug
              }
              frontmatter {
                title
                templateKey
                date(formatString: "MMMM DD, YYYY")
                image {
                  childImageSharp {
                    fluid(maxWidth: 1400, quality: 100) {
                      ...GatsbyImageSharpFluid
                    }
                  }
                }
              }
            }
          }
        }
      }
    `}
    render={data => (
      <StyledUpIndexPage
        data={data.allMarkdownRemark.edges.map(item => item.node)}
      />
    )}
  />
)

And finally, here’s what I added to the NavBar to allow for navigating between the pages:

import { Link } from "gatsby"
import React from "react"
import { withStyles } from "@material-ui/core/styles"
import AppBar from "@material-ui/core/AppBar"
import Toolbar from "@material-ui/core/Toolbar"
import Button from "@material-ui/core/Button"

import { StaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"


const styles = {
  root: {
    flexGrow: "1 !important",
  },
  appbar: {
    backgroundColor: "#AC493A",
  },
  grow:{
    flexGrow:1
  },
  link: {
    color: `white`,
    textDecoration: `none`,
  },
}

class Header extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    const { classes } = this.props

    return (
      <div className={classes.root}>
        <AppBar position="static" className={classes.appbar}>
          <Toolbar>
            <div className={classes.grow}>
              <Link to='/' >
              <StaticQuery
                query={graphql`
                  query {
                    file(relativePath: { eq: "appendto_logo.png" }) {
                      childImageSharp {
                        # Specify the image processing specifications right in the query.
                        # Makes it trivial to update as your page's design changes.
                        fixed(width: 150) {
                          ...GatsbyImageSharpFixed_noBase64
                        }
                      }
                    }
                  }
                `}
                render={data => <Img  critical={true} fadeIn fixed={data.file.childImageSharp.fixed} />}
              />
              </Link>
            </div>
            <div>
              <Link to="/about" className={classes.link}>
                <Button color="inherit">About</Button>
              </Link>

              <Link to="/blog" className={classes.link}>
                <Button color="inherit">Blog</Button>
              </Link>

              <Link to="/courses" className={classes.link}>
                <Button color="inherit">Courses</Button>
              </Link>
            </div>
          </Toolbar>
        </AppBar>
      </div>
    )
  }
}

export default withStyles(styles)(Header)

And here’s what we end up with: https://appendtostaticstyled.netlify.com/

We have here a site that we can continue to build upon and style with Material-ui. All users can make tweaks or changes using NetlifyCMS. And finally, this site is blazing fast and consists of static files that can be served for cloud storage service or CDN.

This starter project is a great way to start any project and I hope these two long posts helped you understand how it came together.

Recommended Reading

This tutorial showed you the essentials of Gatsby. That said, even with nearly 5500 words, it had to skip over a number of topics. Here’s some posts I recommend reading to get a better understanding of Gatsby:

About the Author:

Build Fast and Elegant Sites with Gatsby, NetlifyCMS and Material-UI

April 27th, 2019

This tutorial will show you how to use Gatsby, NetlifyCMS, Netlify, and Material-UI to build out a multi-page site that can be updated with an intuitive CMS. Before diving into the code, I’ll first tell you about the the tools we’ll be working with. Part 2 is here (if you need to skip ahead).

Gatsby

Gatsby is React-based framework that has recently gained a lot traction and use in production. AirBnb, Flamingo, and Impossible Foods are all companies using Gatsby to build production sites and apps. Gatsby sites get built with React, GraphQL, Gatsby plugins, and some sort of CMS. Gatsby outputs production assets as HTML/CSS/JS files that can be served from a cloud host like AWS S3 or a Google Cloud Storage Bucket.

Material-UI

Material-UI is a React UI framework that implements Google’s Material Design principles in React. It can be added to Gatsby projects in a variety of ways and we’ll see how to use it simply with a plugin.

Netlify/NetlifyCMS

One of the smaller hosting services that works really well with Gatsby is Netlify. Netlify (the company) authored an excellent CMS called NetlifyCMS that makes it really simple to produce/edit/manage content stored in Github. All in all, the combo of GraphQL, Netlify, and NetlifyCMS makes it really simple for developers (who already know React) to build fast websites with an intuitive CMS. NetlifyCMS can be setup so that non-technical users can easily make changes to the content and have Netlify push the changes live. Gone are the days of managing clunky WordPress sites or plugin catastrophes! Also gone are the days of slow loading heavy client side sites (looking at you Soundcloud and Yelp 👀).

Ok, whoah whoah — I’m mostly kidding. The WordPress ecosystem is actually pretty amazing and I use Soundcloud every dang day. What I mean to say is that Gatsby and the entire JAMstack paradigm represent a very exciting evolution in websites that developers will enjoy working on and people will enjoy using.

This tutorial will show you how to setup Gatsby with NetlifyCMS and build out a small demo site (see below). The goal of this tutorial is help give you a better conceptual framework for how Gatsby sites come together. Gatsby already has a very nicely done 8-part tutorial so this tutorial will seek to build upon that and make it even easier to learn Gatsby.

Tutorial Outline:

  • Part 1
    • What We’ll Build
    • About Gatsby (and why try it)
    • Setting up Gatsby
    • A high-level overview of what’s what in Gatsby
    • Understanding Gatsby’s Magic
    • Creating Basic Static Pages with Gatsby
    • Dynamically Creating Pages with gatsby-node.js
    • Creating the About Page
    • Creating the Blog Page and Blog Posts
  • Part 2
    • Setup NetlifyCMS to make editing the content more accessible
    • Create the Courses Page and Individual Courses
    • Add Images and more fields to the Courses and Blogs
    • Style it all with Material-UI

What We’ll Build

This tutorial will be focused on building a very basic clone of the appendTo site (that you’re on right now). This demo site consists of pages for About, Courses, Home, and the Blog. There will also be individual pages for each Course and Blog Post.

The site can be found here: https://appendtostaticstyled.netlify.com/courses

The content in this site lives in a Github repo and can be updated using NetlifyCMS. This gives developers or non-technical users a nice interface for making changes to the content.

About Gatsby (and why try it)

Gatsby is run by VC-backed Berkeley-based Gatsby Inc., which has almost 30 employees (at this time of writing). Many of their founders and employees have ‘making the web fast’ as their Linkedin tagline so it’s fair to say that speed is one Gatsby’s primary selling points. Definitely take a second to try out of some of the sites listed on the Gatsby showcase page. The speed at which an attractive, image-heavy ecommerce site like ShopFlamingo.com loads should inspire you to pick up Gatsby. There’s a pure simple joy in clicking on something and having it load near instantaneously.

The fact that Gatsby is being run as a business and used by large companies should give developers confidence that Gatsby will be around for a while and support will be above average. I find it confidence-inspiring that there are more than 5,000 closed issues on Github.

Furthermore, I received extremely fast help for anything I struggled with while making this post (via Discord, Github, and StackOverflow).

It’s also worth taking a look at some of the blog posts that Gatsby employees have written about their baby. A very interesting one is Making website building fun by founder Kyle Mathews. The whole essay is worth reading and gives you a sense of their founding principles. Mathews describes using the component React-headroom vs. having to implement the same functionality, from scratch, in HTML/CSS/JS (with many more lines). One of the key lines in that essay is this:

What makes technology fun?

This is a complex philosophical question and I’m writing this on a Saturday afternoon so I’ll cheat a bit and just cut the gordian knot by saying “simplicity is fun” and conversely, “complexity is not fun”.

Every one loves new projects. Why? Because they’re simple! We can dive in and experiment and reliably get something done. The more complex the project, the more things seem to break, the more time gets wasted on various yak shaves, and critically, the gap between thinking up an idea and being able to try it grows larger and larger.

Why am I mentioning this bit or the founder at all? Because Gatsby was designed to make building websites more fun. This likely explains the development of their plugin ecosystem and some of the magic and opinions Gatsby has. There’s some upfront difficulty and complexity in learning to play with Gatsby. But once you get it, you’ll probably find it really fun to use.

If you’re still considering other static site options, compare Gatsby with React-static to see how a more manual and less opinionated React-based static site library can look.

And with this background out of the way, let’s start building!

Setting up Gatsby

Gatsby offers a nifty CLI for working with Gatsby projects. As you probably guessed, here’s the terminal command to get it:

npm install -g gatsby-cli

Like many CLIs, typing gatsby –help in the Terminal shows you some of the new commands you can use:

Let’s use gatsby new now to create a new Gatsby project:

gatsby new appendToGatsby

After that build completes, run gatsby develop in the terminal and open http://localhost:8000/ in your browser. Pull the project folder into your text editor of choice.

Take a look at the files and folders generated by the Gatsby CLI. I don’t know about you, but I often feel a bit overwhelmed when I see a whole bunch of new files and folders that a generator has made for me. I sometimes claw through the files frantically trying to figure out what’s what and how it all wires together. To make this easier for you, I’m going to diagram this out.

You should be seeing a folder for components, pages, images and several .js files that live in the src root. Here’s a quick rundown on what they do:

Here’s what you should be seeing:

The hardest thing about Gatsby (for me) was all the little nuances and small details that make up its functionality. For example, with this starter project, I found myself looking for the React-router files and a base app.js. Don’t do this! Gatsby projects work differently than normal Create-React-App projects. Let’s create a new page and learn more.

Take what’s in page-2.js and create a file called coolnewpage.js (in that same /pages folder)

Mine looks something like this:

import React from "react"
import { Link } from "gatsby"

import Layout from "../components/layout"
import SEO from "../components/seo"

const CoolNewPage = () => (
 <Layout>
   <SEO title="Cool New Page" />
   <h1>Cool New Page</h1>
   <p>Gatsby is Magical but worth learning</p>
   <Link to="/">Go back to the homepage (index.js file)</Link>
 </Layout>
)

export default CoolNewPage

Then re-run gatsby develop in your terminal. Now when I go to http://localhost:8001/coolnewpage, I see this:

Of course this is nothing too special. But consider that you didn’t have to setup the routing or link the components together. Gatsby just detected that a new file was in the pages folder and created the page for it. Try to let some of your React habits go when learning Gatsby (suspend disbelief, if you will). This will make sense with a bit of practice. Let’s now try building something a bit more dynamic.

Building a dynamic About page

Building static pages (by literally placing them in the /pages folder) works fine and would be a good option for really simple ‘brochureware’ sites or a portfolio. But since you’ll likely want to build sites with multiple pages and templates, it makes sense to learn how to use data and templates to build pages dynamically.

We’re next going to build out the About page for this appendTo clone site. Building pages dynamically is quite challenging at first but it’s fairly easy to pick up. Here are the steps:

  1. Create a markdown file (with frontmatter headings) that will hold the page content
  2. Use the Gatsby API methods createPages and onCreateNode to create the page(s) in gatsby-node.js
  3. Create the page template with the appropriate GraphQL query to populate the page with data
  4. Install the appropriate plugins and add them to gatsby-config

It’s hard to understand how these files and APIs connect at first. I’ll walk you through it as slowly and simply as possible.

Step 1: Create the markdown files

Create a markdown file called about.md (that will go in the /pages folder) with the text/info that will go on the about page. Frontmatter (the information between the two triple dashes —) is where you can store fields like the title and the templateKey (which I’ll explain in a bit). Mine looks something like this:

about.md

templateKey: about-page

title: About Page (created with gatsby-node.js)

(This text is obnoxiously red on purpose)

appendTo is your leading source of technical news, tips, and tutorials.

[the rest of the post]

…..

Step 2: Create the pages using the Gatsby API methods in gatsby-node

We need to now create the pages using Gatsby’s createPages and onCreateNode API methods.

const path = require(`path`)
const { createFilePath } = require('gatsby-source-filesystem')

exports.createPages = ({ actions, graphql }) => {
 const { createPage } = actions

 return graphql(`
   {
     allMarkdownRemark(limit: 1000) {
       edges {
         node {
           id
           fields {
             slug
           }
           frontmatter {
             templateKey
           }
         }
       }
     }
   }
 `).then(result => {
   if (result.errors) {
     result.errors.forEach(e => console.error(e.toString()))
     return Promise.reject(result.errors)
   }


   const posts = result.data.allMarkdownRemark.edges

   posts.forEach(edge => {
     const id = edge.node.id
     createPage({
       path: edge.node.fields.slug,
       tags: edge.node.frontmatter.tags,
       component: path.resolve(
         `src/templates/${String(edge.node.frontmatter.templateKey)}.js`
       ),
       // additional data can be passed via context
       context: {
         id
       },
     })
   })
 })
}

 exports.onCreateNode = ({ node, actions, getNode }) => {
   const { createNodeField } = actions
  
    if (node.internal.type === `MarkdownRemark`) {
     const value = createFilePath({ node, getNode, basePath: `pages` })
     createNodeField({
       name: `slug`,
       node,
       value,
     })
   }
 }

Gatsby’s docs say “We do our best to make Gatsby APIs simple to implement.” I think they do an ok job here but it’s still a lot to grok. It doesn’t make sense for me to completely rehash part 7 of their tutorial where they explain this all in great depth. For this tutorial’s sake, I’ll give you the quick version of what’s going on in the code above.

The onCreateNode part below this is for creating the slugs field so that the page urls work properly. Similar to how that coolnewpage.js created a page at /coolnewpage above, this is doing so manually.

In essence, gatsby-node.js is in charge of querying the markdown files and then creating pages (and slugs) using the markdown data and the page templates. Now we need to create the appropriate page template so that this about page can be created.

Step 3: Create the page templates and their GraphQL queries

Next you need to create a /templates folder and a template for this page. My /templates/about.js page looks like this:

import React from 'react'
import PropTypes from 'prop-types'
import { graphql } from 'gatsby'
import Layout from '../components/layout'

const AboutPage = ({ data }) => {
 const { markdownRemark: post } = data

 return (
   <Layout>
     <h1>{post.frontmatter.title}</h1>   
      <p dangerouslySetInnerHTML={{ __html: post.html }} />      
   </Layout>
 )
}

AboutPage.propTypes = {
 data: PropTypes.object.isRequired,
}

export default AboutPage;

export const aboutPageQuery = graphql`
 query AboutPage($id: String!) {
   markdownRemark(id: { eq: $id }) {
     html
     frontmatter {
       title
     }
   }
 }
`

This looks similar to a normal React component, except for the graphql query at the bottom. This query is using the id passed to the context (on gatsby-node.js) to find the appropriate page (where that id matches). It then retrieves the html and the title from that page (aka, the markdown file). Remember this little bit at the bottom of the previous file?

That same ‘id’ is being used in the query right here to get the right page data.

Step 4: Install Plugins

The final thing we need to do is add the right plugins for us to be able to work with the file system and query markdown files.

Terminal

npm install --save gatsby-source-filesystem

npm install --save gatsby-transformer-remark

Within gatsby-config.js, you need to add these plugins into that [plugins array]:

   {
     resolve: `gatsby-source-filesystem`,
     options: {
       name: `src`,
       path: `${__dirname}/src/`,
     },
   },
  ….
   },
...
...
  `gatsby-transformer-remark`,

Then restart the server with gatsby develop and check out the new about page:

The first time doing this can feel a little challenging. There’s a lot of different files being used and it’s pretty easy for things to break. To prevent myself from getting lost in the cognitive overload of Markdown, React, config settings, GraphQL, React context, promises, and nested you-name-its, I came up with mental shorthand for what these files do:

Markdown files: The data that will go into the pages. Easy.

Templates in the /templates folder: React components with GraphQL queries at the bottom used to build the Gatsby static pages.

Gatsby-node.js: Finds my markdown files and creates pages/slugs for them using the right page template.

Gatsby-config.js: Array of plugins and settings I have to touch when installing plugins.

I hope these shorthands help you as you build your mental model of how Gatsby works.

If you need help understanding how to work with GraphQL in Gatsby, Gatsby comes with a built in GraphQL explorer/helper tool called GraphiQL. Gatsby’s tutorial also has a good write up on how to use it.

Creating the Blog Page and Blog Posts

Now that you’ve made an About page, creating other pages is fairly simple. Since appendTo (and the clone we’re building) is primarily a blog page, we’ll now create some blog posts. This involves creating three new files:

  1. /pages/blog.js (the parent /blog page with a list of the blog posts)
  2. /pages/blogs/blog1.md (an example of the markdown files for the blogs)
  3. /components/BlogItem.js (a small component to show the blog excerpt and title on the /blog page)
  4. /templates/single-blog.js (template for just one blog post)

Here’s how I created the blogs:

Step 1: Create blog.js

import React from "react"
import { StaticQuery, graphql } from "gatsby"

import Layout from "../components/layout"
import SEO from "../components/seo"
import BlogItem from "../components/BlogItem"

class BlogPage extends React.Component {
 render() {
   const { data } = this.props
   const { edges: posts } = data.allMarkdownRemark

   return (
     <Layout>
       <SEO title="Blog" />
       <h1>Blog Page</h1>
       <div>
         {posts &&
           posts.map(({ node: post }) => (
             <BlogItem
               key={post.id}
               post={post.frontmatter}
               slug={post.fields.slug}
               excerpt={post.excerpt}
             />
           ))}
       </div>
     </Layout>
   )
 }
}

export default () => (
 <StaticQuery
   query={graphql`
     query BlogPageQuery {
       allMarkdownRemark(
         sort: { order: DESC, fields: [frontmatter___date] }
         filter: { frontmatter: { templateKey: { eq: "single-blog" } } }
       ) {
         edges {
           node {
             excerpt(pruneLength: 40)
             id
             fields {
               slug
             }
             frontmatter {
               title
               templateKey
               date(formatString: "MMMM DD, YYYY")
             }
           }
         }
       }
     }
   `}
   render={data => <BlogPage data={data} />}
 />
)

This page looks similar to our about page. It is a React component with a GraphQL query at the bottom. This Static Query is a Gatsby component that can be used anywhere to query for data. In this case, it is querying for markdown files that have a templateKey of ‘single-blog’ in the frontmatter header. Let’s make these markdown files now.

Step 2: Create markdown blog files

Here’s an example blog I created and filled with hipster ispum. Notice the templateKey line at the top there. That’s what the query above (in my /blog.js file is looking for).

---

templateKey: single-blog

title: blog3

date: 2019-04-10T16:43:29.834Z

description: so stoked on blog3

---

astropub, small batch godard kickstarter sustainable shoreditch raw denim affogato twee. Disrupt normcore lumbersexual, craft beer aesthetic iPhone chambray irony glossier vinyl skateboard tbh fanny pack. Banh mi sartorial hot chicken semiotics roof party PBR&B whatever brunch, kombucha XOXO tumblr helvetica skateboard. Church-key chillwav

Now we need to make the BlogItem.js component that /blog.js can render this data into.

Step 3: Create BlogItem.js

BlogItem.js

import React from "react"
import { Link } from "gatsby"

function BlogItem(props) {
 const { post, slug, excerpt } = props

 return (
   <div>
     <div>
       <Link to={slug}>
         <h1>{post.title}</h1>
       </Link>
       <h3>{post.date}</h3>
       <p>{excerpt}</p>
     </div>
   </div>
 )
}

export default BlogItem

This is a fairly standard React component that uses the Gatsby Link component for linking to other pages.

Restart the development server and you should see this in your browser:

Step 4: Create the single-blog.js template

import React from 'react'
import PropTypes from 'prop-types'
import { graphql } from 'gatsby'
import Layout from '../components/layout'

const BlogPage = ({ data }) => {
  const { markdownRemark: post } = data

  return (
    <Layout>
      <p>{post.frontmatter.title}</p>
       <p> {post.html}  </p>
      />
    </Layout>
  )
}

BlogPage.propTypes = {
  data: PropTypes.object.isRequired,
}

export default BlogPage

export const BlogPageQuery = graphql`
  query BlogPage($id: String!) {
    markdownRemark(id: { eq: $id }) {
      html
      frontmatter {
        title
      }
    }
  }

Clicking on one of the blogs will lead you to this:

To review what’s happening here, the main /blog page is querying for blog markdown files (e.g. blog1.md), then passing this data to itself and rendering a list of blogs using the BlogItem components.

This post covered most of the basics of setting up Gatsby and creating pages manually and automatically using gatsby-node.js. The following post will explore the following:

  • Creating the Courses Page and Individual Courses
  • Installing NetlifyCMS
  • Styling it all with Material-UI

About the Author:

AirNYT: Firebase Favoriting & Final UI Tweaks

December 6th, 2018

This is the last tutorial in a series called AirNYT, which shows how to the clone the Airbnb search interface to explore NYT travel recommendations. This section of the series will cover:

  • Installing Firebase to allow for user authentication and saving favorites
  • Creating a sign up / login page using Firebase-ui
  • Adding favorite buttons and styling them appropriately according to sign in state
  • Adding a user photo dropdown menu
  • Adding a help-area right-side pop-out Drawer like Airbnb has
  • Setting up toggling of the list vs. map on mobile
  • Creating clickable popup cards on the Map Markers

Repo: https://github.com/kpennell/airnytwithfavorting

Demo: airnytwithfavorting.surge.sh

Installing Firebase to allow for user authentication and saving favorites

As users scroll through these NYT travel recs, I’m hoping some will want an easy to save where they want to go some will want a way to save their favorites. This requires a backend with user authentication. Firebase is a really easy way to do this in React applications. I’m going to show the basic setup for how to let users login and save/retrieve their favorites.

First, install firebase and react-firebaseui using Yarn. Next, setup a basic Firebase project within Firebase (here’s a great tutorial on how to do this).

Next, you’ll need to put the basic config items (given to you by firebase) into your index.js:

Something like this:

 const config = {
    apiKey: "AIzaSyAfdddIPGuz9TTbfVBccKFdwewB0YGNh_M",
    authDomain: "airnyt.firebaseapp.com",
    databaseURL: "https://airnyt.firebaseio.com",
    projectId: "airnyt-2018",
    storageBucket: "airnyt-2018.appspot.com",
    messagingSenderId: "1032396322"
}

// now initialize the firebase app
  const firebaseApp = firebase.initializeApp(config)

Firebase folds fairly easily into React components. The most common way to get and incorporate firebase data into React components is via the lifestyle hook componentDidMount. Here’s an example of this in action:

componentDidMount = () => {
// firebase.auth() is the user authing module. onAuthStateChanged checks if a user is logged in
    firebase.auth().onAuthStateChanged(firebaseUser => {      

/* Here’s what is happening below: If there is a user logged in, call the firebase ref (or part of the JSON tree) where the user’s favorites are. Then loop over the snapshot array of those favorites to make (push) a new array, which will ultimately be saved to state. */

      if (firebaseUser) {
        firebase
          .database()
          .ref("users/" + firebaseUser.uid + "/favorites")
          .on("value", snapshot => {
            let newFavorites = [];
            snapshot.forEach(function(favorite) {
              newFavorites.push({
                id: favorite.key,
                ...favorite.val()
              });
            });
            this.setState({
              user:firebaseUser, // also save the user in state
              favorites: newFavorites
            });
          });
      }
    });
  };

Above is a very common pattern for working with firebase data in React. The parent container gets the data from the firebase ref (a specific part of the JSON tree), saves it in state and passes it to the child components to use. And that’s what we’ll do here for building out this favoriting functionality. Below are the steps I took to build out the Airbnb-esque favorites for this app.

Building a favoriting component (with buttons that style according to whether they are in favorites).

The favoriting component needs empty heart icons and full heart icons, depending on whether the item has been favorited. This component also needs to either add or remove the favorite, depending on whether it is in there or not. The end goal is something like this:

Below is what my FavoritingButton component looks like. Note that this component receives the favorites array and user object (from firebase) as a prop. I’ll comment in the code below to explain what’s going on:

export default class FavoritingButton extends Component {
  addToFavorites = location => {
    if (!this.props.user) {
// If there is no user, open up the login modal (which we’ll build next)
      this.props.toggleLoginSignupModal();

// if there is a user, push the location to their favorites ref in firebase
    } else {
      let userId = this.props.user.uid;
      let favoritesPath = "users/" + userId + "/favorites";  // creating the ref path
      firebase
        .database()
        .ref(favoritesPath)
        .push(location);
      //}
    }
  };

// This one is more complicated at first glance!
// How to remove a favorite
  removeFromFavorites = location => {

    firebase.auth().onAuthStateChanged(firebaseUser => {
      if (firebaseUser) {
        firebase
          .database()
          .ref("users/" + firebaseUser.uid + "/favorites")
          .once("value", snapshot => {

/* This is firebase speak for get all of the users’ favorites */
            snapshot.forEach(function(favorite) {
// Loop over the favorites and if the location matches the favorite….
              if (location.pageid === favorite.val().pageid) {
                let favoriteId = favorite.key;
                let favoritesRef = firebase
                  .database()
                  .ref(
                    "users/" + firebaseUser.uid + `/favorites/${favoriteId}`
                  ); // first create a firebase ref to that specific favorite
                favoritesRef.remove(); // Now remove that favorite
              }
            });
          });
      }
    });
  };

/* To sum up the removeFromFavorites, we first have to surgically go into the existing favorites, find the one that needs removing, make a firebase ref for it, then pull it out with the .remove() method. This is the firebasey way of removing data. */

  render() {
    const { favorites, location, classes } = this.props;

/* We need to show an empty or full heart depending on whether that card location is currently in the favorites array. This tests if it is and shows the right icon */
    if (
      favorites.filter(favorite => favorite.pageid === location.pageid)
        .length > 0
    ) {
      return (
        <Favorite
          className={classes.favoriteButtonStyle}
          onClick={() => this.removeFromFavorites(location)}
        />
/* These are each in the material-ui-icons library */
      );
    } else {
      return (
        <FavoriteBorder
          className={classes.emptyFavoriteButtonStyle}
          onClick={() => this.addToFavorites(location)}
        />
      );
    }
  }

I absolute positioned the favoriting heart icons within the LocationCard(s) and styled them a bit.

Built a signup/login modal using firebase-ui

Next, we need a way to login and signup users so they can use these favorite buttons. In keeping with the imitating Airbnb streak, I created a signup/login modal. Like most popup modals, this one will pop open above all the other content and close by clicking outside of it.

The core code for this modal is simple enough:

 <div>
        <Modal
          aria-labelledby="simple-modal-title"
          aria-describedby="simple-modal-description"
          open={this.props.open}
          onClose={this.props.toggleLoginSignupModal}>
          <div style={getModalStyle()} className={classes.paper}>
            <Grid container className={classes.overallContainer}>
              <Grid item xs={5} className={classes.textStuff}>
                <Typography align="center" variant="headline">
                  Please Sign Up or In
                </Typography>
                <Typography align="center" variant="subheading">
                  (To save your favorites)
                </Typography>
              </Grid>
              <Grid item xs={7}>
                <StyledFirebaseAuth
                  uiConfig={this.props.uiConfig}
                  firebaseAuth={firebase.auth()}
                />
              </Grid>
            </Grid>
          </div>
        </Modal>
      </div>

This uses the material-ui Grid to create a simple form page. StyledFirebaseAuth /> is an amazingly simple component given to us by the react-firebaseui library. It creates a nice login component with as many ways to login as we setup (e.g. Github, Facebook). It’s config settings also need to be configured in the index.js like so:

const uiConfig = {
  signInFlow: 'popup',
  signInSuccessUrl: '/',
Google and Email as auth providers.
  signInOptions: [
    firebase.auth.GoogleAuthProvider.PROVIDER_ID,
       firebase.auth.EmailAuthProvider.PROVIDER_ID,
  ]
};

But with very little work, I can get some nice login buttons:

I put this modal component as a child in my App.js. Whether it is open or closed will be toggled with a simple function that changes a state value:

// component
      <LoginSignupModal
            open={this.state.LoginSignupModalOpen}
            toggleLoginSignupModal={this.toggleLoginSignupModal}
            uiConfig={this.props.uiConfig}
          />
// toggling function
  toggleLoginSignupModal = () => {
    this.setState({ LoginSignupModalOpen: !this.state.LoginSignupModalOpen });
  };
// default state value
    LoginSignupModalOpen: false,

Hide or Show Menu Items According to Login State

Within Airbnb (and many major sites), the menu will show the user’s face with a dropdown menu if the user is logged in. If the user is logged out, it will show Button/Menu Items like sign up and login.

Let’s set this up now within the AirNYT app.

First, we need to pass some new props to our Header component within App.js:

 <Header
           ...
              user={this.state.user}
              logOut={this.logOut}
              toggleLoginSignupModal={this.toggleLoginSignupModal}

            />

Now, within Header, we need to add a menu and hide/show this menu according to whether user exists. So around login and signup, I wrap the inline conditional test like so:

// If there is no user, show these buttons which will toggle the modal
           {user === null && 
                  <div style={{display:"inline"}}>
                  <Button color="inherit" onClick={this.props.toggleLoginSignupModal}>
                    Sign Up
                  </Button>

                  <Button color="inherit" onClick={this.props.toggleLoginSignupModal}>
                    Log in
                  </Button>
                  </div>
                    }

But what if there is a user? What should I show then? Maybe something like this:

   {user && (
              <div style={{display:"inline"}}>
                <IconButton
                  aria-owns={open ? 'menu-appbar' : undefined}
                  aria-haspopup="true"
                  onClick={this.handleMenu}
                  color="inherit"
                >
         
                  <Avatar alt="user pic" src={user.photoURL} className={classes.avatar} />
                </IconButton>
                <Menu
                  id="menu-appbar"
                  anchorEl={anchorEl}
                  anchorOrigin={{
                    vertical: 'top',
                    horizontal: 'right',
                  }}
                  transformOrigin={{
                    vertical: 'top',
                    horizontal: 'right',
                  }}
                  open={open}
                  onClose={this.handleClose}
                >
               
                  <MenuItem onClick={this.props.logOut}>Log Out</MenuItem>
                </Menu>
              </div>
            )}

We get a basic little dropdown menu with a logout button. Notice how easy it is to pull in user.photoURL? This is the beauty of firebase auth-ing. Average/intermediate developers can very quickly put a professional touch on their app (like importing a user’s facebook or google photo into a new app). I put a grey border around the Avatar to make it a look a bit more airbnb-ish.

Obviously I skipped a lot of things here like styling and how all the parents and children get wired up, but the gist is all here. Firebase makes it incredibly simple to add a backend and user authing to an app like this.

Adding a help right-side pop-out Drawer like Airbnb has

Airbnb has a popout right-side help drawer on their desktop web app. It features access to common questions:

My guess would be this allows easy access to common questions while also not breaking users out of the buying flow (like if they immediately redirected to the help center). But who knows 🤷‍♂️? Either way, I wanted to show how easy it was to recreate this within material-ui. Here is my simple HelpDrawer component:

class HelpDrawer extends React.Component {
  render() {
    const { classes, open } = this.props;

    return (
      <div>
        <Drawer
          anchor="right"
          open={this.props.open}
          onClose={this.props.toggleDrawer}
          classes={{
                paper: classes.drawer,
              }}

          >
          <div className={classes.toolbar}>
            <Typography variant="h6" component="h2">
              AirNYT Help
            </Typography>
              <IconButton onClick={this.props.toggleDrawer} className={classes.farRightToggleButton}>
                <Close />
               </IconButton>
             
          </div>

          <Divider />
          <div
            tabIndex={0}
            role="button"
            onClick={this.props.toggleDrawer}
            className={classes.content}
            onKeyDown={this.props.toggleDrawer}>
            
            Content goes here
          </div>
        </Drawer>
      </div>
    );
  }
}

Within App.js, my drawer works much like the LoginSignupModal. It sits there and can be toggled open/close with a simple function:

// App.js
 <HelpDrawer toggleDrawer={this.toggleDrawer} open={this.state.drawerOpen} />

// The function
 toggleDrawer = () => {
    this.setState({
      drawerOpen: !this.state.drawerOpen,
    });
  };

And with that, we end up with a nice start for right-side Drawer. I need to cut this tutorial short, but this would an excellent place to show a list of user favorites.

Mobile: toggle map vs. list

Our desktop map vs. list toggling experience matches that of Airbnb and works fine. On a desktop sized screen, the map and list of cards can share the screen. Obviously, this won’t cut it on mobile. And so Airbnb (and many others) allow users to toggle between the map and list like so:

With a bit zIndex trickery and some responsive breakpoints in our CSS, this is actually fairly easily to emulate.

The action will happen within my LocationsGrid.js (where the List and Map live). First, I’ll show you what I accomplished:

To accomplish this, I need a state value and a toggling function

// state
mobileMapShowing: false

// function
  toggleMapAndListOnMobile = () => {
    this.setState({
      mobileMapShowing: !this.state.mobileMapShowing
    })
  }

Now I need a floating action button:

            <Button variant="fab" aria-label="Add" className={classes.fab} onClick={this.toggleMapAndListOnMobile}>
              {this.state.mobileMapShowing ? <Collections /> : <Place /> }        
              </Button>
// That simple ternary will show a different icon depending on whether the mobile map is showing

Now I need to show and hide the map vs. the list. This is done easily with zIndex:

div className={classes.mapDiv} style={ this.state.mobileMapShowing ? {zIndex:4} : {zIndex:1} }>

So, in English, this toggles the zIndex from 1 to 4, depending on whether the value mobileMapShowing is true or false. Within my styles, the list has a higher initial zIndex vs. the map.

mapDiv: {
...
...
    }),
    [theme.breakpoints.down("sm")]: {
      zIndex: 2,
      position: "absolute",
      width: "100%",
    }
  },
  listGridDiv: {
….
    [theme.breakpoints.down("sm")]: {
      justifyContent: "center",
      position: "absolute",
      zIndex: 3,
      background:"white"
    },
  },

And just like that, we have a nice map vs. list toggling functionality on mobile. One button serves to toggle the zIndex of the map, thus hiding/showing it.

Map: Popup Cards on Markers

Airbnb’s map interface features clickable markers that show more info. This can be enormously helpful for exploring geographic data and keeping users from having to hop back and forth between the list and the map. The airbnb cards look something like this:

Here’s how I implement this card within my MapMarker. Within MapAndMarkers, I need to add some props onto the map marker:

         <MapMarker
... 
          clickedMarker={this.state.clickedMarker}
          handleMarkerClick={this.openMarkerPopup}
          closeAllMarkers={this.closeAllMarkers}
          favorites={this.props.favorites}
          toggleLoginSignupModal={this.props.toggleLoginSignupModal}
          user={this.props.user}

I’m adding the user and favorites props so that the FavortingButton will work on the card. handleMarkerClick={this.openMarkerPopup} and closeAllMarkers={this.closeAllMarkers} refer to the following functions:

// sets the state value for which marker is ‘popped up’
  openMarkerPopup = key => {
    this.setState({
      clickedMarker: key
    });
  };
// closes all markers
  closeAllMarkers = () => {
    this.setState({
      clickedMarker: null
    });
  };

Now, within MapMarker.js, there’s a bit of logic going on. It’s actually fairly straightforward: There’s 1 condition for the default marker, 1 condition for the hovered card (which changes the background color on the marker), and 1 condition for the clicked marker (which pop ups a card like interface).

// if hovered on the related card
    if (pageid == hoveredCardId) {
      return (
        <div className={classes.markerParent}>
          <span
            className={classNames(
              classes.tooltips_span,
              classes.niftyHoverBackground
            )}>
            {this.props.name}
          </span>
        </div>
      );
// if clicked
    } else if (pageid == clickedMarker) {
      return (
        <div className={classes.cardParent}>
          <Card className={classes.card}>
            <div className={classes.CarouselDiv}>
              <CardCarousel
                location={location}
                className={classes.CardCarouselImage}
              />
              <FavoritingButton {...this.props} location={location} />
              <Clear
                className={classes.closeButton}
                onClick={this.closeAllMarkers}
              />
            </div>

            <CardContent className={classes.cardContentArea}>
              <Typography noWrap className={classes.yearArea} component="p">
                Featured in:{" "}
                <span className={classes.year}>{location.year}</span> ·{" "}
                <a
                  href={location.article_link}
                  className={classes.articleLink}
                  target="_blank">
                  Original Article <Launch className={classes.launchicon} />
                </a>
              </Typography>

              <Typography variant="h6" component="h2">
                {location.location_name}
              </Typography>
              <div className={classes.snippet_area}>
                <Typography
                  className={classes.snippet_text}
                  noWrap
                  component="p">
                  {location.clean_snippet}
                </Typography>
                <Typography component="p">
                  <a
                    href={location.url}
                    style={{
                      textDecoration: "none",
                      color: "#008489",
                      fontWeight: 600,
                      fontSize: 12
                    }}
                    className={classes.articleLink}
                    target="_blank">
                    Learn More
                  </a>
                </Typography>
              </div>
            </CardContent>
          </Card>
        </div>
      );
// the default, neither hovered nor clicked
    } else {
      return (
        <div className={classes.markerParent} onClick={this.markerClickInChild}>
          <span className={classes.tooltips_span}>{this.props.name}</span>
        </div>
      );
    }

And just like that, we get a fairly intuitive popup card on the map.

Final Thoughts

Thanks for following along with this tutorial. Hopefully it showed you one way of piecing together some components to make an interactive Airbnb-esque map interface. Feel free to submit suggestions and pull requests to the Github repo for this project.

https://github.com/kpennell/airnytwithfavorting

This is the last tutorial in a series called AirNYT, which shows how to the clone the Airbnb search interface to explore NYT travel recommendations. This section of the series will cover:

  • Installing Firebase to allow for user authentication and saving favorites
  • Creating a sign up / login page using Firebase-ui
  • Adding favorite buttons and styling them appropriately according to sign in state
  • Adding a user photo dropdown menu
  • Adding a help-area right-side pop-out Drawer like Airbnb has
  • Setting up toggling of the list vs. map on mobile
  • Creating clickable popup cards on the Map Markers

Repo: https://github.com/kpennell/airnytwithfavorting

Demo: airnytwithfavorting.surge.sh

Installing Firebase to allow for user authentication and saving favorites

As users scroll through these NYT travel recs, I’m hoping some will want an easy to save where they want to go some will want a way to save their favorites. This requires a backend with user authentication. Firebase is a really easy way to do this in React applications. I’m going to show the basic setup for how to let users login and save/retrieve their favorites.

First, install firebase and react-firebaseui using Yarn. Next, setup a basic Firebase project within Firebase (here’s a great tutorial on how to do this).

Next, you’ll need to put the basic config items (given to you by firebase) into your index.js:

Something like this:

 const config = {
    apiKey: "AIzaSyAfdddIPGuz9TTbfVBccKFdwewB0YGNh_M",
    authDomain: "airnyt.firebaseapp.com",
    databaseURL: "https://airnyt.firebaseio.com",
    projectId: "airnyt-2018",
    storageBucket: "airnyt-2018.appspot.com",
    messagingSenderId: "1032396322"
}

// now initialize the firebase app
  const firebaseApp = firebase.initializeApp(config)

Firebase folds fairly easily into React components. The most common way to get and incorporate firebase data into React components is via the lifestyle hook componentDidMount. Here’s an example of this in action:

componentDidMount = () => {
// firebase.auth() is the user authing module. onAuthStateChanged checks if a user is logged in
    firebase.auth().onAuthStateChanged(firebaseUser => {      

/* Here’s what is happening below: If there is a user logged in, call the firebase ref (or part of the JSON tree) where the user’s favorites are. Then loop over the snapshot array of those favorites to make (push) a new array, which will ultimately be saved to state. */

      if (firebaseUser) {
        firebase
          .database()
          .ref("users/" + firebaseUser.uid + "/favorites")
          .on("value", snapshot => {
            let newFavorites = [];
            snapshot.forEach(function(favorite) {
              newFavorites.push({
                id: favorite.key,
                ...favorite.val()
              });
            });
            this.setState({
              user:firebaseUser, // also save the user in state
              favorites: newFavorites
            });
          });
      }
    });
  };

Above is a very common pattern for working with firebase data in React. The parent container gets the data from the firebase ref (a specific part of the JSON tree), saves it in state and passes it to the child components to use. And that’s what we’ll do here for building out this favoriting functionality. Below are the steps I took to build out the Airbnb-esque favorites for this app.

Building a favoriting component (with buttons that style according to whether they are in favorites).

The favoriting component needs empty heart icons and full heart icons, depending on whether the item has been favorited. This component also needs to either add or remove the favorite, depending on whether it is in there or not. The end goal is something like this:

Below is what my FavoritingButton component looks like. Note that this component receives the favorites array and user object (from firebase) as a prop. I’ll comment in the code below to explain what’s going on:

export default class FavoritingButton extends Component {
  addToFavorites = location => {
    if (!this.props.user) {
// If there is no user, open up the login modal (which we’ll build next)
      this.props.toggleLoginSignupModal();

// if there is a user, push the location to their favorites ref in firebase
    } else {
      let userId = this.props.user.uid;
      let favoritesPath = "users/" + userId + "/favorites";  // creating the ref path
      firebase
        .database()
        .ref(favoritesPath)
        .push(location);
      //}
    }
  };

// This one is more complicated at first glance!
// How to remove a favorite
  removeFromFavorites = location => {

    firebase.auth().onAuthStateChanged(firebaseUser => {
      if (firebaseUser) {
        firebase
          .database()
          .ref("users/" + firebaseUser.uid + "/favorites")
          .once("value", snapshot => {

/* This is firebase speak for get all of the users’ favorites */
            snapshot.forEach(function(favorite) {
// Loop over the favorites and if the location matches the favorite….
              if (location.pageid === favorite.val().pageid) {
                let favoriteId = favorite.key;
                let favoritesRef = firebase
                  .database()
                  .ref(
                    "users/" + firebaseUser.uid + `/favorites/${favoriteId}`
                  ); // first create a firebase ref to that specific favorite
                favoritesRef.remove(); // Now remove that favorite
              }
            });
          });
      }
    });
  };
/* To sum up the removeFromFavorites, we first have to surgically go into the existing favorites, find the one that needs removing, make a firebase ref for it, then pull it out with the .remove() method. This is the firebasey way of removing data. */

  render() {
    const { favorites, location, classes } = this.props;

/* We need to show an empty or full heart depending on whether that card location is currently in the favorites array. This tests if it is and shows the right icon */
    if (
      favorites.filter(favorite => favorite.pageid === location.pageid)
        .length > 0
    ) {
      return (
        <Favorite
          className={classes.favoriteButtonStyle}
          onClick={() => this.removeFromFavorites(location)}
        />
/* These are each in the material-ui-icons library */
      );
    } else {
      return (
        <FavoriteBorder
          className={classes.emptyFavoriteButtonStyle}
          onClick={() => this.addToFavorites(location)}
        />
      );
    }
  }

I absolute positioned the favoriting heart icons within the LocationCard(s) and styled them a bit.

Built a signup/login modal using firebase-ui

Next, we need a way to login and signup users so they can use these favorite buttons. In keeping with the imitating Airbnb streak, I created a signup/login modal. Like most popup modals, this one will pop open above all the other content and close by clicking outside of it.

The core code for this modal is simple enough:

 <div>
        <Modal
          aria-labelledby="simple-modal-title"
          aria-describedby="simple-modal-description"
          open={this.props.open}
          onClose={this.props.toggleLoginSignupModal}>
          <div style={getModalStyle()} className={classes.paper}>
            <Grid container className={classes.overallContainer}>
              <Grid item xs={5} className={classes.textStuff}>
                <Typography align="center" variant="headline">
                  Please Sign Up or In
                </Typography>
                <Typography align="center" variant="subheading">
                  (To save your favorites)
                </Typography>
              </Grid>
              <Grid item xs={7}>
                <StyledFirebaseAuth
                  uiConfig={this.props.uiConfig}
                  firebaseAuth={firebase.auth()}
                />
              </Grid>
            </Grid>
          </div>
        </Modal>
      </div>

This uses the material-ui Grid to create a simple form page. StyledFirebaseAuth /> is an amazingly simple component given to us by the react-firebaseui library. It creates a nice login component with as many ways to login as we setup (e.g. Github, Facebook). It’s config settings also need to be configured in the index.js like so:

const uiConfig = {
  signInFlow: 'popup',
  signInSuccessUrl: '/',
Google and Email as auth providers.
  signInOptions: [
    firebase.auth.GoogleAuthProvider.PROVIDER_ID,
       firebase.auth.EmailAuthProvider.PROVIDER_ID,
  ]
};

But with very little work, I can get some nice login buttons:

I put this modal component as a child in my App.js. Whether it is open or closed will be toggled with a simple function that changes a state value:

// component
      <LoginSignupModal
            open={this.state.LoginSignupModalOpen}
            toggleLoginSignupModal={this.toggleLoginSignupModal}
            uiConfig={this.props.uiConfig}
          />
// toggling function
  toggleLoginSignupModal = () => {
    this.setState({ LoginSignupModalOpen: !this.state.LoginSignupModalOpen });
  };
// default state value
    LoginSignupModalOpen: false,

Hide or Show Menu Items According to Login State

Within Airbnb (and many major sites), the menu will show the user’s face with a dropdown menu if the user is logged in. If the user is logged out, it will show Button/Menu Items like sign up and login.

Let’s set this up now within the AirNYT app.

First, we need to pass some new props to our Header component within App.js:

 <Header
           ...
              user={this.state.user}
              logOut={this.logOut}
              toggleLoginSignupModal={this.toggleLoginSignupModal}

            />

Now, within Header, we need to add a menu and hide/show this menu according to whether user exists. So around login and signup, I wrap the inline conditional test like so:

// If there is no user, show these buttons which will toggle the modal
           {user === null && 
                  <div style={{display:"inline"}}>
                  <Button color="inherit" onClick={this.props.toggleLoginSignupModal}>
                    Sign Up
                  </Button>

                  <Button color="inherit" onClick={this.props.toggleLoginSignupModal}>
                    Log in
                  </Button>
                  </div>
                    }

But what if there is a user? What should I show then? Maybe something like this:

{user && (
              <div style={{display:"inline"}}>
                <IconButton
                  aria-owns={open ? 'menu-appbar' : undefined}
                  aria-haspopup="true"
                  onClick={this.handleMenu}
                  color="inherit"
                >
         
                  <Avatar alt="user pic" src={user.photoURL} className={classes.avatar} />
                </IconButton>
                <Menu
                  id="menu-appbar"
                  anchorEl={anchorEl}
                  anchorOrigin={{
                    vertical: 'top',
                    horizontal: 'right',
                  }}
                  transformOrigin={{
                    vertical: 'top',
                    horizontal: 'right',
                  }}
                  open={open}
                  onClose={this.handleClose}
                >
               
                  <MenuItem onClick={this.props.logOut}>Log Out</MenuItem>
                </Menu>
              </div>
            )}

We get a basic little dropdown menu with a logout button. Notice how easy it is to pull in user.photoURL? This is the beauty of firebase auth-ing. Average/intermediate developers can very quickly put a professional touch on their app (like importing a user’s facebook or google photo into a new app). I put a grey border around the Avatar to make it a look a bit more airbnb-ish.

Obviously I skipped a lot of things here like styling and how all the parents and children get wired up, but the gist is all here. Firebase makes it incredibly simple to add a backend and user authing to an app like this.

Adding a help right-side pop-out Drawer like Airbnb has

Airbnb has a popout right-side help drawer on their desktop web app. It features access to common questions:

My guess would be this allows easy access to common questions while also not breaking users out of the buying flow (like if they immediately redirected to the help center). But who knows 🤷‍♂️? Either way, I wanted to show how easy it was to recreate this within material-ui. Here is my simple HelpDrawer component:

class HelpDrawer extends React.Component {
  render() {
    const { classes, open } = this.props;

    return (
      <div>
        <Drawer
          anchor="right"
          open={this.props.open}
          onClose={this.props.toggleDrawer}
          classes={{
                paper: classes.drawer,
              }}

          >
          <div className={classes.toolbar}>
            <Typography variant="h6" component="h2">
              AirNYT Help
            </Typography>
              <IconButton onClick={this.props.toggleDrawer} className={classes.farRightToggleButton}>
                <Close />
               </IconButton>
             
          </div>

          <Divider />
          <div
            tabIndex={0}
            role="button"
            onClick={this.props.toggleDrawer}
            className={classes.content}
            onKeyDown={this.props.toggleDrawer}>
            
            Content goes here
          </div>
        </Drawer>
      </div>
    );
  }
}

Within App.js, my drawer works much like the LoginSignupModal. It sits there and can be toggled open/close with a simple function:

// App.js
 <HelpDrawer toggleDrawer={this.toggleDrawer} open={this.state.drawerOpen} />

// The function
 toggleDrawer = () => {
    this.setState({
      drawerOpen: !this.state.drawerOpen,
    });
  };

And with that, we end up with a nice start for right-side Drawer. I need to cut this tutorial short, but this would an excellent place to show a list of user favorites.

Mobile: toggle map vs. list

Our desktop map vs. list toggling experience matches that of Airbnb and works fine. On a desktop sized screen, the map and list of cards can share the screen. Obviously, this won’t cut it on mobile. And so Airbnb (and many others) allow users to toggle between the map and list like so:

With a bit zIndex trickery and some responsive breakpoints in our CSS, this is actually fairly easily to emulate.

The action will happen within my LocationsGrid.js (where the List and Map live). First, I’ll show you what I accomplished:

To accomplish this, I need a state value and a toggling function

// state
mobileMapShowing: false

// function
  toggleMapAndListOnMobile = () => {
    this.setState({
      mobileMapShowing: !this.state.mobileMapShowing
    })
  }

Now I need a floating action button:

 <Button variant="fab" aria-label="Add" className={classes.fab} onClick={this.toggleMapAndListOnMobile}>
              {this.state.mobileMapShowing ? <Collections /> : <Place /> }        
              </Button>
// That simple ternary will show a different icon depending on whether the mobile map is showing

Now I need to show and hide the map vs. the list. This is done easily with zIndex:

<div className={classes.mapDiv} style={ this.state.mobileMapShowing ? {zIndex:4} : {zIndex:1}  }>

So, in English, this toggles the zIndex from 1 to 4, depending on whether the value mobileMapShowing is true or false. Within my styles, the list has a higher initial zIndex vs. the map.

  mapDiv: {
...
...
    }),
    [theme.breakpoints.down("sm")]: {
      zIndex: 2,
      position: "absolute",
      width: "100%",
    }
  },
  listGridDiv: {
….
    [theme.breakpoints.down("sm")]: {
      justifyContent: "center",
      position: "absolute",
      zIndex: 3,
      background:"white"
    },
  },

And just like that, we have a nice map vs. list toggling functionality on mobile. One button serves to toggle the zIndex of the map, thus hiding/showing it.

Map: Popup Cards on Markers

Airbnb’s map interface features clickable markers that show more info. This can be enormously helpful for exploring geographic data and keeping users from having to hop back and forth between the list and the map. The airbnb cards look something like this:

Here’s how I implement this card within my MapMarker. Within MapAndMarkers, I need to add some props onto the map marker:

      <MapMarker
... 
          clickedMarker={this.state.clickedMarker}
          handleMarkerClick={this.openMarkerPopup}
          closeAllMarkers={this.closeAllMarkers}
          favorites={this.props.favorites}
          toggleLoginSignupModal={this.props.toggleLoginSignupModal}
          user={this.props.user} />

I’m adding the user and favorites props so that the FavortingButton will work on the card. handleMarkerClick={this.openMarkerPopup} and closeAllMarkers={this.closeAllMarkers} refer to the following functions:

// sets the state value for which marker is ‘popped up’
  openMarkerPopup = key => {
    this.setState({
      clickedMarker: key
    });
  };
// closes all markers
  closeAllMarkers = () => {
    this.setState({
      clickedMarker: null
    });
  };

Now, within MapMarker.js, there’s a bit of logic going on. It’s actually fairly straightforward: There’s 1 condition for the default marker, 1 condition for the hovered card (which changes the background color on the marker), and 1 condition for the clicked marker (which pop ups a card like interface).

// if hovered on the related card
    if (pageid == hoveredCardId) {
      return (
        <div className={classes.markerParent}>
          <span
            className={classNames(
              classes.tooltips_span,
              classes.niftyHoverBackground
            )}>
            {this.props.name}
          </span>
        </div>
      );
// if clicked
    } else if (pageid == clickedMarker) {
      return (
        <div className={classes.cardParent}>
          <Card className={classes.card}>
            <div className={classes.CarouselDiv}>
              <CardCarousel
                location={location}
                className={classes.CardCarouselImage}
              />
              <FavoritingButton {...this.props} location={location} />
              <Clear
                className={classes.closeButton}
                onClick={this.closeAllMarkers}
              />
            </div>

            <CardContent className={classes.cardContentArea}>
              <Typography noWrap className={classes.yearArea} component="p">
                Featured in:{" "}
                <span className={classes.year}>{location.year}</span> ·{" "}
                <a
                  href={location.article_link}
                  className={classes.articleLink}
                  target="_blank">
                  Original Article <Launch className={classes.launchicon} />
                </a>
              </Typography>

              <Typography variant="h6" component="h2">
                {location.location_name}
              </Typography>
              <div className={classes.snippet_area}>
                <Typography
                  className={classes.snippet_text}
                  noWrap
                  component="p">
                  {location.clean_snippet}
                </Typography>
                <Typography component="p">
                  <a
                    href={location.url}
                    style={{
                      textDecoration: "none",
                      color: "#008489",
                      fontWeight: 600,
                      fontSize: 12
                    }}
                    className={classes.articleLink}
                    target="_blank">
                    Learn More
                  </a>
                </Typography>
              </div>
            </CardContent>
          </Card>
        </div>
      );
// the default, neither hovered nor clicked
    } else {
      return (
        <div className={classes.markerParent} onClick={this.markerClickInChild}>
          <span className={classes.tooltips_span}>{this.props.name}</span>
        </div>
      );
    }

And just like that, we get a fairly intuitive popup card on the map.

Final Thoughts

Thanks for following along with this tutorial. Hopefully it showed you one way of piecing together some components to make an interactive Airbnb-esque map interface. Feel free to submit suggestions and pull requests to the Github repo for this project.

https://github.com/kpennell/airnytwithfavorting

About the Author:

AirNYT: Pagination + Filtering + Responsiveness

November 27th, 2018

This tutorial picks up where we left off around building the map and markers. This is part of a larger tutorial series on cloning the Airbnb map and cards interface to be able to explore the last 7 years of New York Times travel recommendations. The short name for this app/series is AirNYT.
In this part of the tutorial, we will continue building out the following:

  • Working Pagination to make exploring the data easier/faster
  • A popup year filter that allows users to show NYT data by year
  • Text/Search Filtering to allow for string query filtering of the data
  • A responsive menu
  • General responsible responsiveness to make this app mobile-friendly

Our end goal is this: http://airnyt.surge.sh

Building working pagination to make exploring the data easier

If you play around with the Airbnb interface as much as I have, you’ll notice that they use infinite scroll when the map is hidden and pagination when the map is showing. This makes perfect sense, from the user’s perspective, as the map should only display a subset of the total properties. Well, you might be able to make some sort of cool infinite scroll updating map, but I don’t think most user would find this intuitive. I mention this background so as to say: we need pagination.
If Github stars are a proxy for popularity, then most React developers are using react-paginate for their pagination. Well, I tried and tried to like and use react-paginate but I found it lacking on the docs and props front. It felt sort of heavy and clunky and Angular 1-ey to me. After some Googling, I came across react-paginating. I found it’s API and examples much easier to understand so I went with this.
To achieve basic client-side pagination, we need two things:

  1. A pagination ui with next buttons and page numbers
  2. To appropriately slice our big array down to only display a subset of it on the appropriate page

This was achieved fairly easily with the following code:
I first needed to calculate a variety of values within my LocationsGrid component. I’ll comment what each of them do:

const { currentPage } = this.state; // a state variable that tracks which page the user is on.
const resultsPerPage = 30; // how many results I’ll display
const pageCount = Math.ceil(locations.length / resultsPerPage); // quantity of pages
const total = Math.ceil(locations.length); // total number of values
const offset = (currentPage - 1) * resultsPerPage; // the specific area of my area I need to slice out for that page
const locationsSlicedDownOnPage = locations.slice(
  offset,
  offset + resultsPerPage
); // a subset of my locations array to be displayed on the page

The following simple function changes the page:

handlePageChange = page => {
  this.setState({
    currentPage: page
  });
};

As you can see, with very little code, we can calculate the appropriate props to give to our PaginationComponent:

// LocationsGrid.js
<PaginationComponent
  total={total}
  resultsPerPage={resultsPerPage}
  pageCount={pageCount}
  currentPage={currentPage}
  handlePageChange={this.handlePageChange}
  offset={offset}
/>;

The pagination component has a bit more going on, but it’s easy to figure out with some explaining. First, take a look a what we’re making:

Here’s the code:

const PaginationComponent = ({
  total,
  resultsPerPage,
  pageCount,
  currentPage,
  handlePageChange,
  offset
}) => {
  return (
    <Pagination
      total={total}
      limit={resultsPerPage}
      pageCount={pageCount}
      currentPage={currentPage}
      offset={Math.ceil(offset)}
      resultsPerPage={Math.ceil(resultsPerPage)}
    >
      {({
        pages,
        currentPage,
        hasNextPage,
        hasPreviousPage,
        previousPage,
        nextPage,
        totalPages,
        getPageItemProps
      }) => (
        <div
          style={{
            display: "flex",
            flexDirection: "column",
            justifyContent: "space-between",
            alignItems: "center"
          }}
        >
          <div
            style={{
              display: "flex",
              justifyContent: "space-evenly",
              alignItems: "center",
              width: "300px"
            }}
          >
            {hasPreviousPage && (
              <NavigateBefore
                style={{
                  border: "1px solid #008489 ",
                  borderRadius: "50%",
                  color: "#008489"
                }}
                {...getPageItemProps({
                  pageValue: previousPage,
                  onPageChange: handlePageChange
                })}
              >
                {"<"}
              </NavigateBefore>
            )}
            {pages.map(page => {
              let activePage = { color: "#008489", cursor: "pointer" };
              if (currentPage === page) {
                activePage = {
                  border: "1px solid #008489 ",
                  borderRadius: "50%",
                  color: "#008489",
                  borderRadius: "50%",
                  width: "25px",
                  height: "25px",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  backgroundColor: "#008489",
                  color: "white"
                };
              }
              if (
                page < currentPage + 3 &&
                page > currentPage - 3 &&
                page != totalPages
              ) {
                return (
                  <Typography
                    variant="subtitle2"
                    gutterBottom
                    key={page}
                    style={activePage}
                    {...getPageItemProps({
                      pageValue: page,
                      onPageChange: handlePageChange
                    })}
                  >
                    {page}
                  </Typography>
                );
              }
            })}
            <span>...</span>
            {pages.map(page => {
              let activePage = null;
              if (currentPage === page) {
                activePage = {
                  border: "1px solid #008489 ",
                  borderRadius: "50%",
                  color: "#008489",
                  cursor: "pointer"
                };
              }
              if (page > totalPages - 1) {
                return (
                  <Typography
                    variant="subtitle2"
                    gutterBottom
                    key={page}
                    style={activePage}
                    style={{
                      color: "#008489",
                      cursor: "pointer"
                    }}
                    {...getPageItemProps({
                      pageValue: page,
                      onPageChange: handlePageChange
                    })}
                  >
                    {page}
                  </Typography>
                );
              }
            })}
            {hasNextPage && (
              <NavigateNext
                {...getPageItemProps({
                  pageValue: nextPage,
                  onPageChange: handlePageChange
                })}
                style={{
                  border: "1px solid #008489 ",
                  borderRadius: "50%",
                  color: "#008489",
                  cursor: "pointer"
                }}
              >
                {">"}
              </NavigateNext>
            )}
          </div>

          <div>
            <Typography variant="caption" gutterBottom>
              {offset} - {offset + resultsPerPage} of {total} Locations
            </Typography>
          </div>
        </div>
      )}
    </Pagination>
  );
};

export default PaginationComponent;

I’ll break down what’s happening. Our pagination needs a previous and next arrows. Those are created using the material-ui icons (NavigateBefore and NavigateNext). We need a subset of pages to display, then an ellipsis …, followed by the last page. That’s what all of those pages.map are accomplishing.
The page < currentPage + 3 && page > currentPage – 3 && page != totalPages part is getting a very specific subset of the pages to display to user. In English, this says “show them a few behind the current page, a few ahead of the current page, but not the last page!”.
The section at the bottom of the code is telling the user how many of the listings they are seeing:


It’s a fair amount of code, but it ultimately adds up to a component that is pretty close the Airbnb version.
Airbnb’s:

AirNYT’s

Boo-yah!

A client-side year filter that allows users to show NYT data by year

I really like the way Airbnb gives users a ton of different filters without cluttering up their ui. Their filter bar features buttons that queue popovers/modals to achieve this.

I mimicked this using Material-ui’s Popover component. Here’s how I created a filter that allows user to filter out different years from the total results.

// FilterPopup.js
<div className={classes.FilterPopup}>
  <Button
    className={this.state.buttonActive ? classes.active : classes.inactive}
    // toggle the active class to change the color of the button
    onClick={this.handleClick}
    variant="outlined" // conveniently built-in button variant
  >
    Year
  </Button>
  <Popover
    id="simple-popper"
    open={open}
    anchorEl={anchorEl}
    onClose={this.handleClose}
    className={classes.popover}
    PaperProps={{ classes: { root: classes.rightPaper } }}
    BackdropProps={{
      classes: {
        root: classes.backDropStyling // this styling creates the background change to make the popover more pronounced
      }
    }}
  >
    <Card className={classes.card}>
      <CardContent className={classes.cardcontent}>
        <ChipFilter // popover contains the ChipFilter which is where the actual filtering will take place
          years={this.props.years}
          toggleChipProperty={this.props.toggleChipProperty}
          clearAllChips={this.props.clearAllChips}
          selectAllChips={this.props.selectAllChips}
        />
      </CardContent>
    </Card>
  </Popover>
</div>;

ChipFilter is a component that allows users to click the years to toggle them on and off (and thus achieve this client-side filtering):

// ChipFilter.js
<div className={classes.root}>
  <div className={classes.chipSection}>
    // map over the years array and style them according to whether they are
    toggled on or off
    {this.props.years.map((year, index) => {
      return (
        <span
          key={index}
          onClick={this.props.toggleChipProperty(year.key)}
          className={year.showing ? classes.active : classes.inactive}
        >
          {year.label}{" "}
        </span>
      );
    })}
  </div>
  // A simple way to select or deselect all
  <div className={classes.buttonSection}>
    <Button
      variant="outlined"
      color="ternary"
      onClick={this.props.clearAllChips}
      className={classes.button}
    >
      Deselect All
    </Button>
    <Button
      variant="outlined"
      color="ternary"
      onClick={this.props.selectAllChips}
      className={classes.button}
    >
      Select All
    </Button>
  </div>
</div>;

But how does the actual filtering itself happen? That takes place within our App.js:
State.years is an array of objects with key, label, and showing properties:

years: [
  {
    key: 0,
    label: 2011,
    showing: true
  },
  {
    key: 1,
    label: 2012,
    showing: true
  },
  {
    key: 2,
    label: 2013,
    showing: true
  }...
];

matchChipsAndTags is the filtering function that determines whether the location’s year matches up with the state.year’s object showing property. It does this with some ES6 kungfu:

const matchChipsAndTags = function(chips, year) {
  return chips.some(function(chip) {
    return year === chip.label && chip.showing === true;
  });
};

🤔
This function takes the state.chips and the location.year as its parameters. Array.prototype().some is a newer array prototype function that stops executing once a truthy value has been found. So, in English, this function is returning true or false depending on whether that specific locations year property matches the currently showing chips.
So, for instance, a Card/Location like Istanbul will only get rendered (aka appear) if its year property (2013) is matches the chip.label of 2013 (it will) and is that particular chip’s showing property (within state) is true.

Ultimately, the array is filtered down like so:

let filteredlocations = textfilteredlocations.filter(location =>
  matchChipsAndTags(this.state.years, location.year)
);

Text/Search Filtering to allow for string queries of the data

Airbnb has a search box in their header that allows for easy searching of places/experiences:

We’re not going to recreate the whole server-side search experience/function here but I think some client-side filtering would work nicely. This is very easy to do in React.
First, within state in App.js, I’ll declare the search/string value:

filterValue: ""

I’ll then declare a function in App.js to change this value:

handleSearch = e => {
  this.setState({ filterValue: e.target.value });
};

I’ll pass this function as a prop to my Header component. Within component, I’ll call the function within an InputBase component:

const { classes, handleSearch } = this.props;
<InputBase
  placeholder="Filter Places..."
  type="search"
  onChange={handleSearch}
  classes={{
    root: classes.inputRoot,
    input: classes.inputInput
  }}
/>;

I use InputBase instead of TextField so as to avoid any of the default TextField styling (it underlines the search box). TextField is a component composed of several other underlying component, including InputBase.

This search filter box isn’t the most interesting component, but I did find it useful for doing country searches. It was fun to see when different places in the same country got that sweet NYT mention. Here’s what Mexico/Australia look like:

As you see, with New Mexico popping up in a Mexico, it would take some work and data formatting/reorganizing (e.g. a country property in the dataset) to make this truly a country only search. I would also love to have a continent search, but that’s for another tutorial.

A responsive menu

Airbnb’s menu crunches down into a full-screen mobile dropdown on mobile. This frees up a lot of space and only shows the user what they need to see. Here’s how we can achieve the same thing with material-ui.
Material-ui has a really helpful CSS in JSS API for handling breakpoints. You put the breakpoints right into your styles like so:

[theme.breakpoints.up('lg')]: {
   backgroundColor: green[500],
},

This translates to: large size screens and up will have this background style.

[theme.breakpoints.down('sm')]: {
backgroundColor: theme.palette.secondary.main,
},

This would translate to: all the way down to the small size of screen will have this style.
You can set your breakpoints accordingly when you define your theme (usually in index.js or app.js) using createMuiTheme.
To achieve something like the airbnb mobile menu, I’ll first hide the normal menu items on small screens with this styling:

menuItems:{
[theme.breakpoints.down('sm')]: {
display:"none"
},

I’m going to create a dropdown menu, but first I’ll put the styling in for it:

dropdownMenu:{
[theme.breakpoints.up('md')]: { // for mid-size screens and up, please hide this !
display:"none"
},

I then create a DropDownMenu component like so:

class DropDownMenu extends React.Component {
  state = {
    anchorEl: null
  };
  handleClick = event => {
    this.setState({ anchorEl: event.currentTarget });
  };
  handleClose = () => {
    this.setState({ anchorEl: null });
  };
  render() {
    const { anchorEl } = this.state;
    const { classes } = this.props;
    const open = Boolean(anchorEl);
    return (
      <div className={classes.DropDownMenuRoot}>
        <IconButton
          aria-label="More"
          aria-owns={open ? "long-menu" : undefined}
          aria-haspopup="true"
          onClick={this.handleClick}
          className={classes.downCarrotButton}
        >
          <KeyboardArrowDown />
        </IconButton>
        <Menu
          id="long-menu"
          anchorEl={anchorEl}
          open={open}
          onClose={this.handleClose}
          PaperProps={{
            style: {
              maxHeight: ITEM_HEIGHT * 4.5,
              width: 200
            }
          }}
        >
          <MenuItem className={classes.menubuttons} color="inherit">
            Help
          </MenuItem>
          <MenuItem className={classes.menubuttons} color="inherit">
            Sign Up
          </MenuItem>
          <MenuItem className={classes.menubuttons} color="inherit">
            Log in
          </MenuItem>
        </Menu>
      </div>
    );
  }
}

This creates a little popover menu that is toggled using the down arrow next to the logo. It’s not the elegant full-screen menu that airbnb offers, but it’s definitely a good start.
To make this app more generally functional and appealing on mobile, I went through and made small tweaks in the styling using these same theme breakpoint helpers.

To do

This a fun little map + cards + filtering app. But there’s still work to be done. These are the items I’d like to cover in future tutorials:

  • Mobile Map Toggling

Right now, I’m hiding the map div on mobile. Lame! Airbnb has a nice way of toggling the mobile map vs. the cards/list using a little hovering button in the lower right corner of the screen. With a material-ui floating action button and some clever breakpoints and styling, we can make the map available on mobile.

  • Accounts and Favoriting

I’ll show how to incorporate firebase into this app to allow for user authentication and letting people save their favorite places.

  • Infinite Scroll on Cards View and Pagination on Map View

We made a really nice react-virtualized fast loading list in a previous tut. There’s no reason we can’t show this infinite loading list when the map isn’t visible and show the paginated list when the map is showing. We can get the best of both worlds this way.

  • Small ui tweaks

The list should scroll to the top when the page is changed. It could be helpful to center the map on the appropriate map marker when a user hovers on a card.

  • Pop Up Card on the Marker

Airbnb’s map markers show a little pop up card when you click on them. This gives you more info about the place and is quite useful. I’ll add these in a future tutorial.

Stay tuned for more tutorials and the coming end of this tutorial!

About the Author:

AirNYT: Building the Map and Markers Interactivity

November 19th, 2018

This tutorial is a part of a larger tutorial series on cloning the Airbnb map/cards interface to be able to explore the last 7 years of New York Times travel recommendations. The short name for this app/series is AirNYT.

In this part of the tutorial, we will continue building out the various components that make up this map and cards interface.

This specific tutorial will cover how to turn this:

http://nytrecsalaairbnb.surge.sh

…into this:

http://airnyt.surge.sh

To do this, I’ll show you how I built out the following functionality:

  • A carousel slideshow on each card for each location
  • A interactive map with map markers that can be hidden/shown with a toggle switch
  • Hover events on cards that change styling on the map markers

There’s a lot to cover in this tutorial and I’ll try to skip as much boilerplate, yarn installations, and styling as possible when I’m showing how the different parts are coded. If you’d like to see the complete code, you can do so here:

https://github.com/kpennell/airnyt

A carousel slideshow on each card for each location

As you’ve likely seen, Airbnb property listings have these really elegant slideshows on each property:

These let you get the jist of the place without going to that page. To recreate that functionality in the AirNYT app, I’m going to use the React-slick library. This is a React version of the very popular Slick Carousel library.

The react-slick API was fairly straightforward to use. I created a separate CardCarousel component to house it. The component is a simple Slider component that rotates through different material-ui CardMedia images from my data.

class CardCarousel extends React.Component {
  render() {
    const { classes, location } = this.props;

    return (
      <div>
        <Slider {...settings}>
          <div>
            <CardMedia
              component="img"
              className={classes.media}
              image={location.image1}
            />
          </div>
          {location.image2 && (
            <div>
              <CardMedia
                component="img"
                className={classes.media}
                image={location.image2}
              />
            </div>
          )}
          {location.image3 && (
            <div>
              <CardMedia
                component="img"
                className={classes.media}
                image={location.image3}
              />
            </div>
          )}{" "}
        </Slider>
      </div>
    );
  }
}

To mimic the chevron right and left arrows from the Airbnb slider, I put these custom arrows in using material-ui icons:

function SampleNextArrow(props) {
  const {
    className,
    style,
    onClick
  } = props;
  return (
    <ChevronRight
      className={className}
      style={{
        ...style,
        display: "block",
        color: "white",
        fontSize: "3em",
        right: "9px",
        zIndex: 1
      }}
      onClick={onClick}
    />
  );
}
function SamplePrevArrow(props) {
  const {
    className,
    style,
    onClick
  } = props;
  return (
    <ChevronLeft
      className={className}
      style={{
        ...style,
        display: "block",
        color: "white",
        fontSize: "3em",
        left: "9px",
        zIndex: 1
      }}
      onClick={onClick}
    />
  );
}

Within the settings, I can tell the Slider component that I’d like to use these arrow components:

const settings = {
dots: true,
infinite: true,
speed: 300,
slidesToShow: 1,
slidesToScroll: 1,
nextArrow: <SampleNextArrow />,
prevArrow: <SamplePrevArrow />,

And voila, I have working sliders on all of my cards:

The one caveat I’d share about these sliders is that they definitely slow down the interface. We saw how fast React-virtualized made loading the 380+ cards in the last blog post. But now, each card is loading 3 images, and the lag is noticeable. To solve this, I’d recommend trying out some sort of virtualized or windowed slider that only loads the image when someone opens it or slides to it. But that being said, as long as we’re paginating these results (card listings), the lag shouldn’t be too terrible.

An interactive map with map markers

Maps are perhaps the original data visualization. For spatial or location data, a map is indispensable for visualizing the data. Airbnb has a really well-designed map that can be toggled with a simple switch and we can mimic this fairly easily.

But first, we need a mapping library! The gargantuan React ecosystem offers many options, both in terms of how they are loaded, how they are interacted with, and which tiles (e.g. Google, OSM) get loaded. I’ve tried several and have kept coming back to google-map-react. This library allows React developers to load any component as a marker on a Google map. I like how this library feels like a react-specific way of doing things (a la ‘everything is a component’) and it seems to get out of my way. If you wish to tweak things on the actual Google Maps JavaScript API, google-map-react gives you a simple API to do so.

My MapAndMarkers component is fairly straightforward. I’ll explain how it works after this code snippet:

class MapAndMarkers extends React.Component {
  static defaultProps = {
    center: {
      lat: 30,
      lng: -30
    },
    zoom: 0
  };
  render() {
    const {
      classes,
      locations,
      hoveredCardId,
      pageid
    } = this.props;
    let MapMarkers = locations.map(
      (location, index) => {
        return (
          <MapMarker
            key={location.id}
            lat={location.lat}
            lng={location.lng}
            name={
              location.location_name
            }
            pageid={location.pageid}
            hoveredCardId={
              hoveredCardId
            }
          />
        );
      }
    );
    return (
      <div
        style={{
          width: "100%",
          height: "100%"
        }}
      >
        <GoogleMapReact
          bootstrapURLKeys={{
            key:
              "AIzaSysBBvQLsewI7BPpXln_Jzl_tIUVsH1f775C7GXM",
            v: "3.31"
          }}
          defaultCenter={
            this.props.center
          }
          defaultZoom={this.props.zoom}
          hoverDistance={20 / 2}
          options={createMapOptions}
        >
          {MapMarkers}
        </GoogleMapReact>
      </div>
    );
  }
}

GoogleMapReact is the primary google-map-react component here, and it needs some props and children to work. The required props include the API keys and center and zoom. Center and zoom tell the map which tiles to load. The options prop allows for controlling the aforementioned underlying Google JavaScript API. Things like panning, zooming, controls, etc. get controlled with this object. hoverDistance allows for controlling how far/close to a marker counts as mouse hovering.

To create the {MapMarkers} children, I .map over the locations array prop (my cards/locations data) and pass things like key, lat, and lng as props to the MapMarkers(s). These props tell GoogleMapReact where to render these components/points/markers. I’ll explain the hoveredCardId prop within the context of my MapMarker component.

My MapMarker component is fairly straightforward. The goal was to mimic the plain white tooltips that Airbnb uses:

I used CSS arrow please to create the CSS for this component and used this CSS to React tool to ‘JSSify’ my CSS. There are a variety of nifty React tooltip libraries out there, but I went this route to keep the map fast and have one less dependency.

class MapMarker extends React.Component {
  render() {
    const {
      classes,
      pageid,
      hoveredCardId
    } = this.props;

    return (
      <div
        className={classes.markerParent}
      >
        <span
          className={classNames(
            classes.tooltips_span,
            pageid == hoveredCardId &&
              classes.niftyHoverBackground
          )}
        >
          {this.props.name}
        </span>
      </div>
    );
  }
}

When someone hovers on a Card in the list, I want the MapMarker background to turn purple, like Airbnb does:

To achieve this, I used the classNames library to toggle the class with the background color. So, if the id from the card (that is being hovered upon) is equal to id on the marker, the style changes. To set this id, I setup some functions to be called by mouse events:

// LocationCards.js

<Card className={classes.card} onMouseEnter={e => this.props.c(location)} onMouseLeave={e => this.props.resetCardMarkerHover()} >

setCardMarkerHover is a straightforward function that updates the state variable of hoveredCardId according to the card that is being hovered upon.

// LocationsGrid.js

setCardMarkerHover = location => {
  this.setState({
    hoveredCardId: location.pageid
  });
};

I know I’m skipping some of the steps here but please read through the code if you’d like to see how the state/props variables work here.

Toggle-able Map

Like I mentioned, I really like how Airbnb lets users toggle the map.

Sometimes you want a map and sometimes you just want a grid of cards (and more screen real estate). To recreate this functionality, I put a toggle switch in my FilterBar, like so:

import Switch from "@material-ui/core/Switch";
<FormGroup row>
  <FormControlLabel
    control={
      <Switch
        checked={this.props.mapShowing}
        onChange={
          this.props.toggleMapShowing
        }
        classes={{
          switchBase:
            classes.AirBnbSwitchBase,
          bar: classes.AirBnbBar,
          icon: classes.AirBnbIcon,
          iconChecked:
            classes.AirBnbIconChecked,
          checked: classes.AirBnbChecked
        }}
      />
    }
    label="Show Map"
    labelPlacement="start"
  />
</FormGroup>;

This switch is on or off depending on the prop mapShowing. When it gets toggled, it calls the function toggleMapShowing. Fairly straightforward! Now, within my LocationsGrid component, I toggle the map like so:

{
  this.props.mapShowing && (
    <div className={classes.mapDiv}>
      <MapAndMarkers
        locations={locationsSlicedDownOnPage}
        hoveredCardId={this.state.hoveredCardId}
      />
    </div>
  );
}

The state variable mapShowing and the toggleMapShowing function live within App.js.

Getting the map to push the list over was achieved using flex-box (how did we go without it?!) and inline-block:

mapDiv: {
height: "100%",
width: "65%",
display: "inline-block",
position: "sticky",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
[theme.breakpoints.down("sm")]: {
display: "none"
}
},
listGridDiv: {
overflow: "auto",
height: "85vh",
[theme.breakpoints.down("sm")]: {
justifyContent: "center",
}
},
parentDiv: {
display: "flex",
justifyContent: "space-between",
height: "100%",
overflow: "hidden"
},

This allows the map to kindly push its way onto the scene, and the flexbox grid of cards responds accordingly. The last thing I’ll point out here is that the cards are flex-start aligned when the map is in and centered when the map is hidden. This makes for a more attractive ui and is achieved with this simple ternary on the parent <Grid>:

justify={this.props.mapShowing ? "flex-start" : "space-evenly"}

I didn’t do it here, but the next step would be to mimic the ✓ vs. X in the AirBnb toggle switch:

I’m fairly confident a bit of digging into the various props of the material-ui Switch component would allow for this.

This tutorial will be continued in the next section, where we will cover building:

  • Working Pagination to make exploring the data easier/faster
  • A popup year filter that allows users to show NYT data by year
  • Text/Search Filtering to allow for string queries of the data
  • A responsive menu
  • General responsible responsiveness to make this app mobile-friendly

Note: This open source map got put into action for this Oaxaca Hiking Map Website. It has also been favorited over ten times on Github.

About the Author:

AirNYT: React-Virtualized + Material-UI Cards for Fast Lists

November 2nd, 2018

This tutorial will cover how to use React-virtualized with Material-ui Cards and Grid to make a list of image-heavy cards that loads extremely fast. Doing this not only allows for much faster loading and re-rendering (such as when using client-side filters) but also better user experience in general. This tutorial is part of a broader series on building an AirBnb-like interface for exploring New York Times travel recommendations.

We start with this app as our baseline:

Github: https://github.com/kpennell/nytairbnbbucketlist

Demo: http://nytrecsalaairbnb.surge.sh/

The Problem: Hundreds of Image-heavy Cards

This app is loading 400+ image-heavy cards. For fast wifi, this is generally fine. But for slower connections (and/or mobile), this app is going to feel sluggish. If I add client side filters, it will feel even more sluggish. And if someone were to use this for an app with 40000 instead of 400 cards, it would feel extremely sluggish. No one likes that.

Here are some snapshots of from the Chrome console before I ‘fix’ this problem:

The Solution: React-Virtualized

React-Virtualized is an awesome library written and maintained by Brian Vaughn (he works on the React team at Facebook). Brian describes React-Virtualized as a set of components for efficiently rendering large lists and tabular data. He gives a great explanation of the library and why he invented it here:

It is a great talk and the key element is this part about windowing: (https://youtu.be/t4tuhg7b50I?t=670):

Windowing is a technique of only rendering what a user actually sees in their browser. In other words, there’s no need to attach a bunch of list, table, grid items to the DOM that the user is not currently using or seeing. So the problem with my list of cards (in the example app, above) is exactly this: the user’s browser is forced to load a bunch of images that the user might not actually see or be using. Let’s fix this with React-Virtualized.

Implementing React-Virtualized

The current implementation of my Grid of Cards is fairly straightforward:

// LocationsGrid.js

class LocationsGrid extends React.Component {
  render() {
    const { locations, classes } = this.props;

    return (
      <div className={classes.root}>
        <Grid container justify="flex-start" spacing={16}>
          {locations.map((location, index) => (
            <Grid key={index} item>
              <LocationCard location={location} />
            </Grid>
          ))}
        </Grid>
      </div>
    );
  }
}

This maps over the props.locations and renders cards in a nice flex-box grid. Here’s the steps I’ll take to implement this same ui using react-virtualized.

First things first:

yarn add react-virtualized

Next, I’m going to use React-Virtualized AutoSizer and List components for this grid ui. Autosizer is a “High-order component that automatically adjusts the width and height of a single child”. Put a bit more simply, Autosizer is a component that goes around (as a parent or HOC) a List or Table component to allow it access to the width and height props. These width and height props are useful for making responsive or dynamic lists or tables.

The next component I will use is the List component. The List component is fairly self-explanatory in that it is what React-virtualized uses for ‘windowed’ or ‘virtualized’ lists.

Source: https://bvaughn.github.io/react-virtualized/#/components/List

Here is some code with inlined comments that show how I use AutoSizer and List together to implement my same Card grid.

<div style={{ marginTop: "10px", height: "80vh" }}>
  <AutoSizer>
    // The Autosizer component goes around the List component and you can see
    here the height // and width props that it will pass to List
    {({ height, width }) => {
      const itemsPerRow = Math.floor(width / CARD_WIDTH) || 1; // A calculation to establish how many cards will go on each row.

      // The || 1 part is a simple hack that makes it work in a really small viewport (if someone totally collapses the window)

      const rowCount = Math.ceil(locations.length / itemsPerRow); // List will need the number of rows in order to be able to properly know what to render and what not to

      return (
        <div>
          <List
            width={width}
            height={height}
            rowCount={rowCount}
            rowHeight={CARD_WIDTH}
            // CARD_WIDTH is a constant of 340

            rowRenderer={({ index, key, style }) => {
              // This is where stuff gets interesting/confusing

              // We are going to constantly update an array of items that our rowRenderer will render

              const items = [];

              // This array will have a start and an end.

              // The start is the top of the window

              // The end is the bottom of the window

              // the for loop below will constantly be updated as the the user scrolls down

              const fromIndex = index * itemsPerRow;

              const toIndex = Math.min(
                fromIndex + itemsPerRow,
                locations.length
              );

              for (let i = fromIndex; i < toIndex; i++) {
                let location = locations[i];

                items.push(
                  <div className={classes.Item} key={i}>
                    <LocationCard location={location} />
                  </div>

                  // Each of these items has the LocationCard in them
                );
              }

              return (
                // They get rendered into the Row

                <div className={classes.Row} key={key} style={style}>
                  {items}
                </div>
              );
            }}
          />
        </div>
      );
    }}
  </AutoSizer>
</div>;

If you happened to get lost in those comments and lines of code, let me try to simplify this:

We have an <AutoSizer> component. In our example, it calculates the (potentially) changing height and width of the user’s browser window.

Ok, then we have a <List> component. It will create our list. But we need to give it props first. If you check the docs for this component, you’ll see that it needs rowCount, height, rowHeight, rowRenderer, and width. rowRenderer (docs here) is the potentially confusing one. This is the function in charge of ‘creating’ or rendering our rows. But it needs to know which rows to render when. In this example, we give it a key and an index. The index tells the rowRenderer where exactly it is in the collection (be it row 2 or row 1,000,002).

From the docs:

index, // Index of row
key, // Unique key within array of rendered rows

Alrighty, so then we have that for loop in there:

for (let i = fromIndex; i < toIndex; i++) {
  let location = locations[i];

  items.push(
    <div className={classes.Item} key={i}>
      <LocationCard location={location} />
    </div>
  );
}

This for loop is creating a smaller array (from the whole big props.locations array) to be rendered within the window. If you’re still not quite getting it, I recommend logging index and items and then scrolling down, like so:

console.log("index " + index);

const toIndex = Math.min(
  fromIndex + itemsPerRow,

  locations.length
);

for (let i = fromIndex; i < toIndex; i++) {
  let location = locations[i];

  items.push(
    <div className={classes.Item} key={i}>
      <LocationCard location={location} />
    </div>
  );
}

console.log("items " + items);

And you should see something like this in the console:

What Did We Win?

What did we achieve with this slightly-confusing code? Let’s check the chrome console again:

The previous implementation had 329 requests and 29MB transferred, which took 8.46s to load:

The React-Virtualized example had 43 requests which transferred 3.6MB and loaded in 3.79s.

Using React-Virtualized allowed us to save a ton of bandwidth and user waiting time. Now if we could just get Soundcloud to do the same!

I hope this helped you understand the key points of using this incredible library. Upcoming tuts will get us back on track to finish up making this AirBnb clone.

Update: Brian Vaughn (the creator of React-Virtualized) submitted a pull request and showed how to do this tutorial using React-Window (a faster version of React-Virtualized): https://github.com/kpennell/nytairbnbbucketlist/commit/101a32bb0555f3a7cc29151de195882b249972e8

That said, here is the code for this tutorial using React- Virtualized: https://github.com/kpennell/nytairbnbwithvirtualized

 

About the Author:

AirNYT: Setup + Architecture + Basic Components

October 20th, 2018

This tutorial is part of a blog series on building an airbnb-esque interface for exploring New York Times travel recommendations. In this part of the series, I’ll show how to build the overall components and interface. Here’s what will get built:

Demo: http://nytrecsalaairbnb.surge.sh/ (not mobile friendly yet)

To build this, we will:

  • Export the data from Google Sheets and convert to JSON
  • Set up the project using create-react-app
  • Install UI libraries
  • Setup fonts and overall design theme
  • Create components for the headers, cards, filter bar, and a few others
    • Build the header
    • Build the filter bar
    • Build the parent collections of cards container
    • Build the cards to display the location data

Above all, this section will try to mimic the Airbnb exploring interface as much as reasonably possible…a la:

Export the data from Google Sheets into JSON

The first step toward building this interface is getting the data out of google sheets and into JSON that this app will be able to easily consume. This is simple as downloading the data as a csv and converting it to json using convertcsv.com. For future articles in this series, I will show how to get the data directly from Google Sheets (via the simple stack).

Set up the project using create-react-app and set up basic folder structure

I created the project using create-react-app. There are many tutorials about how to do this so I’ll skip covering those steps.

There are many different ways to structure React projects. I really like the simplicity of having one folder for containers (that will fetch data and pass it as props to components) and one for components (that will consume and display data given them). Dave Ceddia has a good article about this simple structure. I agree with him that “Simple is better. Start simple. Keep it simple, if you can”.

Source: https://daveceddia.com/react-project-structure/

Install UI libraries

I next need to install the UI libraries I’m going to use. I will recreate the Airbnb interface with material-ui and material-ui-icons. Material-ui is the most popular React UI library out there (and there many great ones). I find this library extremely intuitive and well-documented for making attractive and functional interfaces. I install both using Yarn and, so, my dependencies look like this:

"dependencies": {
"@material-ui/core": "^3.2.2",
"@material-ui/icons": "^3.0.1",
"react": "^16.5.2",
"react-dom": "^16.5.2",
"react-scripts": "2.0.5"
}

Setup theme + styling

Material-ui looks quite nice out of the box with its sleek Roboto font and contrasting pink and purple primary and secondary colors. One of the cool things about material-ui is how easy is it to tweak and customize the global style settings. Rather than changing the color on each every single button or h3, you can set their colors, sizing, etc. within your app.js and then have these changes propagate through your app using React’s context. This will, in theory, allow you to make UIs that look more consistent between pages and views.

I want the styling of this app to be as close as possible to AirBnb’s elegant design for their web app. Airbnb uses the very attractive, but unfortunately, proprietary sans-serif Circular and Cereal fonts.

I asked the internet (well, Reddit + StackExchange) which open source font is closest to Cereal and most people said Montserrat, Nunito Sans, or Poppins. I decided to go with Poppins for this app. Using Poppins as my font was as simple as putting this into my index.html:

<link href="https://fonts.googleapis.com/css?family=Poppins:400,600,700,800,900" rel="stylesheet">

And the following into my app.js:

const theme = createMuiTheme({
  typography: {
    fontFamily: "'Poppins', sans-serif",
    fontSize:14,
    textTransform: "none",
    color: "#484848",
  }
});

Next, I wanted to mimic AirBnb’s colors as much as possible. They use a mix of blacks, greys, pinks (Rausch), and greens (Kazan 🙄).

source: https://medium.freecodecamp.org/designing-in-color-abd358660a7b

To utilize these within my material-ui theme, I put these in my theme, within app.js:

const theme = createMuiTheme({
  palette: {
    primary: {
      light: "#ff8e8c",
      main: "#484848",
      dark: "#c62035",
      contrastText: "#fff"
    },
    secondary: {
      light: "#4da9b7",
      main: "#ff5a5f",
      dark: "#004e5a",
      contrastText: "#000"
    }
  },

Throughout this app, I’ll keep my styling DRY by using the global theme settings as much as possible. You can read more up on material-ui’s theming here.

Create components for the headers, cards, filter bar, and a few others

React is, of course, all about components. So it’s necessary to break down the app into components. In the spirit of ‘start simple, keep it simple’, I think the Airbnb interface can be broken down, initially, like so:

I’ll very likely break some of these components, like the filter bar, down into many other components/containers but this initial mockup gives a great starting point.

Building the Header

The header consists of a logo, a search bar, and some menu links. Here’s how I recreated it (I’ll comment in the code):

import React from "react";
import PropTypes from "prop-types";
// This is how you grab the components you need from Material-ui
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import InputBase from "@material-ui/core/InputBase";
import { withStyles } from "@material-ui/core/styles";
import SearchIcon from "@material-ui/icons/Search";
import Grid from "@material-ui/core/Grid";
import Explore from "@material-ui/icons/Explore";
import Button from "@material-ui/core/Button";
// Material-ui encourages the use of CSS in JS and makes it easy to inject with the withStyles HOC
const styles = theme => ({
  // This is the nice tall header style that AirBnb uses, with no boxShadow (which is material-ui’s default)
  header: {
    height: "80px",
    color: "#484848",
    backgroundColor: "white",
    boxShadow: "none", // nix the material design box shadow
    borderBottom: "1px solid #e2e2e2"
  },
  mainIcon: {
    fontSize: "40px",
    color: "#f44336"
  },
  toolbar: {
    height: "80px"
  },
  grid: {
    display: "flex",
    alignItems: "center"
  },
  root: {
    width: "100%"
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20
  },
  magnifyingGlass: {
    fontWeight: 800,
    color: "black"
  },
  title: {
    display: "none",
    [theme.breakpoints.up("sm")]: {
      display: "block"
    }
  },
  // Mimicking the search box style with a simple boxShadow + grey border
  search: {
    boxShadow: "rgba(0, 0, 0, 0.1) 0px 2px 4px",
    position: "relative",
    borderRadius: "4px",
    borderWidth: "1px",
    borderStyle: "solid",
    borderColor: "rgb(235, 235, 235)",
    borderRadius: "4px",
    marginRight: theme.spacing.unit * 2, // notice how spacing units + breakpoints can be defined at the theme level
    marginLeft: 0,
    width: "100%",
    [theme.breakpoints.up("sm")]: {
      marginLeft: 20,
      width: "auto"
    }
  },
  searchIcon: {
    width: "50px",
    height: "100%",
    position: "absolute",
    pointerEvents: "none",
    display: "flex",
    alignItems: "center",
    justifyContent: "center"
  },
  inputRoot: {
    color: "inherit",
    width: "100%"
  },
  inputInput: {
    paddingTop: "12px",
    paddingRight: "8px",
    paddingBottom: "12px",
    paddingLeft: "50px",
    transition: theme.transitions.create("width"),
    width: "100%",
    [theme.breakpoints.up("md")]: {
      width: 350
    },
    "&::placeholder": {
      color: "black",
      fontWeight: 600
    }
  },
  menubuttons: {
    fontWeight: 600
  }
});
class Header extends React.Component {
  render() {
    const { classes } = this.props;
    return (
      <div className={classes.root}>
        <AppBar position="fixed" className={classes.header}>
          <Toolbar className={classes.toolbar}>
            // Material-ui has some great grid components that I pair with
            flex-box’s space-between to create the same airbnb look and feel
            <Grid justify="space-between" container spacing={24}>
              <Grid item className={classes.grid}>
                // I went with a simple compass icon found in material-ui-icons
                (to fill in for the AirBnb icon)
                <Explore className={classes.mainIcon} />
                <div className={classes.search}>
                  <div className={classes.searchIcon}>
                    // Airbnb’s search bar is used to allow people to find
                    rentals in various locations. I’ll use the search bar to
                    allow users to filter the NYT locations
                    <SearchIcon className={classes.magnifyingGlass} />
                  </div>
                  <InputBase
                    placeholder="Filter Places..."
                    classes={{
                      root: classes.inputRoot,
                      input: classes.inputInput
                    }}
                  />
                </div>
              </Grid>
              <Grid item className={classes.grid}>
                <div>
                  // These are not identical to airbnb’s but they make sense
                  here and I’ll build out their functionality later
                  <Button className={classes.menubuttons} color="inherit">
                    Help
                  </Button>
                  <Button className={classes.menubuttons} color="inherit">
                    Sign Up
                  </Button>
                  <Button className={classes.menubuttons} color="inherit">
                    Log in
                  </Button>
                </div>
              </Grid>
            </Grid>
          </Toolbar>
        </AppBar>
      </div>
    );
  }
}
Header.propTypes = {
  classes: PropTypes.object.isRequired
};
// The aforementioned withStyles HOC to inject the styles
export default withStyles(styles)(Header);

Building the Filter Bar

The AirBnb filter bar consists of some really intuitive/attractive filters for sorting through thousands of properties. In my little app with ~423 places, there’s no need for this much robust filtering. But I will build out a filter that lets users sort by year the articles were mentioned.

// FilterBar.js
// imports excluded for brevity
const styles = theme => ({
  header: {
    height: "65px",
    color: "#f44336",
    backgroundColor: "white",
    boxShadow: "none",
    borderBottom: "1px solid #e2e2e2",
    marginTop: 80
  },
  toolbar: {
    height: "65px",
    display: "flex",
    justifyContent: "space-between",
    padding: "0 85px" // side-padding
  },
  grid: {
    display: "flex",
    alignItems: "center"
  },
  root: {
    width: "100%"
  },
  buttons: {
    margin: "0 0px",
    minHeight: 20,
    padding: "5px 10px"
  },
  title: {
    display: "none",
    [theme.breakpoints.up("sm")]: {
      display: "block"
    }
  }
});
class FilterBar extends React.Component {
  render() {
    const { classes } = this.props;
    return (
      <div className={classes.root}>
        <AppBar position="fixed" className={classes.header}>
          <Toolbar className={classes.toolbar}>
            <Grid container spacing={24}>
              <Grid item className={classes.grid}>
                <Button
                  className={classes.buttons}
                  variant="outlined"
                  color="primary"
                >
                  Year
                </Button>
              </Grid>
              <Grid item className={classes.grid}>
                <div />
              </Grid>
            </Grid>
          </Toolbar>
        </AppBar>
      </div>
    );
  }
}

The filter bar component is really similar to the primary header component and both use AppBar, ToolBar, and Outlined Buttons. I will build out the year filter and pop up modal in a future blog in this series.

Displaying the Cards

Now that I have a nice header and filter bar in place, it’s time to actually display the data using Material-ui cards. I will make two components for this: LocationCard.js and LocationGrid.js.

First, I need to get the data in App.js and save it in state.

// App.js
// import bucketlistjson from "../data/bucketlist.json";
// class App extends Component {
// state = { locations: bucketlistjson };
return (
  <MuiThemeProvider theme={theme}>
    <div>
      <Header />
      <FilterBar />
      <SubtitleSection />
      <LocationsGrid locations={this.state.locations} /> // passing the data
      down as props
    </div>
  </MuiThemeProvider>
);

Next, I’ll build the LocationsGrid component, which primary serves to map over the locations json and create the individual cards.

// LocationsGrid.js
import LocationCard from "./LocationCard.js"; // to be built next
import Grid from "@material-ui/core/Grid";
const styles = theme => ({
  root: {
    padding: "0 85px", // side-padding
    marginTop: 20,
    justifyContent: "flex-start"
  }
});
class LocationsGrid extends React.Component {
  render() {
    const { locations, classes } = this.props;
    return (
      <div className={classes.root}>
        <Grid
          Container
          justify="flex-start" // pulls the cards to the left
          spacing={16}
        >
          {locations.map((
            location,
            index // iterating over the json array using map
          ) => (
            <Grid key={index} item>
              {" "}
              // the Material-ui Grid components creates the nice grid of cards
              <LocationCard location={location} />
            </Grid>
          ))}
        </Grid>
      </div>
    );
  }
}

Creating the location cards (LocationCard.js)

The final step in this part of the tutorial is creating the individual cards for each New York Times place.

As this tutorial is already quite long, I’m going to skip the beautiful carousel slideshows gallery (and save it for another tut).

Here’s what I came up with:

Here’s how I implemented the card using Material-ui:

import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';

import Launch from "@material-ui/icons/Launch"; 

/*
I tried to match the look, feel, and dimensions of the Airbnb cards. But I chose to diverge on the content where it made sense and cover the NYT locations (vs. content that would be right for a rental).

*/
const styles = theme => ({
  card: {
    width: 340,
    boxShadow: "none"
  },
  media: {
    height:220,
    objectFit: 'cover',
        borderRadius: 5
  },
  cardContentArea:{
    padding:"4px 0px"
  },
  year:{
    backgroundColor:"#A61D55",
    borderRadius:"3.2px", // This is the border radius Airbnb uses for their little PLUS chips
    color:"white",
    padding:"0 4px" // Same padding that AirBnb uses
  },
  yearArea:{
    textTransform:"uppercase",
    color:"#A61D55", // matching the Airbnb purple
    fontWeight: 600,
    fontSize:12,
    lineHeight:"16px",
    paddingTop:4,
    
  },
  launchicon: {
    fontSize:12, // I put a little icon next to the original article link
  },
  articleLink:{
    textDecoration:"none",
    color:"#A61D55", 
  }
});

class LocationCard extends React.Component {
  render() {
    const { classes, location } = this.props; // destructuring props 


    return (
      <Card className={classes.card}>
      <CardActionArea>
        <CardMedia
          component="img"
          className={classes.media}
          image={location.image1}
        />
        <CardContent className={classes.cardContentArea}>
{/*
Instead of AirBnb PLUS I put a little chip with the year the place appeared and a link to the original article*/}

          <Typography noWrap className={classes.yearArea} component="p">
           Featured in: <span className={classes.year}>{location.year}</span>  · <a href={location.article_link} className={classes.articleLink} target="_blank">Original Article <Launch className={classes.launchicon} /></a>
          </Typography>

          <Typography variant="h6" component="h2">
            {location.location_name}
          </Typography>
          <div className={classes.snippet_area}>
          <Typography className={classes.snippet_text} noWrap component="p">
           {location.clean_snippet} 
          </Typography>
          <Typography component="p">
          <a href={location.url} style={{textDecoration:"none", color:"#008489", fontWeight:600, fontSize:12}} className={classes.articleLink} target="_blank">Learn More</a>
          </Typography>
          </div>
        </CardContent>
      </CardActionArea>
      
    </Card>
    )      
  }
}

And we’re done here. This gives a great starting point with which to continue building on. Future tutorials will cover:

  • Making this responsive
  • Adding the Map toggling + Map

  • Creating user favoriting + sign up + log in
  • Information Drawer similar to the AirBnb’s help drawer

  • Filtering functionality (by year which the places appear in the NYT list)
  • React-virtualized so that the map cards load quickly

Stay tuned for upcoming tuts!

About the Author:

AirNYT: Getting the Data

October 14th, 2018

This article is part of the series on building a Bucket List App using NYT’s “places to go” lists paired with open source data from Wikivoyage and Pixabay.

This section will describe how to get the data that will be used in the application. If you plan on making an app using open source data, you might find some of these techniques useful.

Copy NYT Lists into Google Sheets

The first step was to copy the lists into a Google Spreadsheets. I used the importxml functionality with Google Sheets to quickly extract the lists of places. Importxml is a must-have simple data scraping tool and Distilled.net has great tutorial on how to use it.

Here’s a quick example of how I did it for 2011:

=importxml(“https://www.nytimes.com/2011/01/09/travel/09where-to-go.html”,”//strong“)

Get Pixabay Images for Each Location

Next, I needed to get images of the places to use in the application. The New York Times lists have these beautiful accompanying images and embedded videos but… we can’t use these. Thankfully, Pixabay, an open source image repository, has a straightforward API. I queried the Pixabay API using importjson directly within Google Sheets.

=transpose(importjson(CONCATENATE(“https://pixabay.com/api/?key=3111109-acd0034c9055b6da0366ff7c1&q=”,A2,”&image_type=photo&pretty=true“),”/hits/webformatURL”,”noHeaders”))

This gave me a list of images that matched that query (e.g. “Iceland”, “London”). However, the Pixabay images expire a couple hours later. I can’t find out why they do this in their docs. But I imagine it is to prevent hotlinking. They don’t want people hammering their servers by hosting images all over the place. You are meant to download the images yourself and self host. Hence the expiring image links.

Host Pixabay images on Cloudinary

Pixabay’s expiring image links presented a challenge. I could have come up with a script to download each image using something like requests and then upload it to S3 or Google Cloud. But I wanted something simpler. With a bit of searching, I discovered a service called Cloudinary that allows automatic uploading and hosting of images that are prefixed with their service. I set the auto-upload feature up within Cloudinary (simple as mapping to a folder) and then I changed Pixabay urls to Cloudinary urls:

I changed this:

https://pixabay.com/get/e832b0062ff3033ed1584d05fb1d4597ea74e2dc04b014429cf4c67aa2ecb0_640.jpg

To this:

https://res.cloudinary.com/kpennell/image/upload/pixabay/e832b0062ff3033ed1584d05fb1d4597ea74e2dc04b014429cf4c67aa2ecb0_640.jpg

I had to open these Cloudinary urls within new tabs (I could have also used a http service to do this). And now I had 2000+ images of my travel destinations hosted on Cloudinary.

Get WikiVoyage Data

This bucket list travel application will feature a list of images with accompanying descriptions of the places and links to more info. Wikivoyage is an excellent open source travel guide that I can use for this. Wikivoyage is owned/maintained by Wikimedia and has a great API. As much as I also like Wikitravel, they didn’t have a straightforward way to get their data via API search. Searching the Wikivoyage API was fairly easy:

First, I needed to run a search to get what they call placeids for different locations/pages.

A search like this will get these placeids and snippets from the wikivoyage page (i.e. The first sentence in the guide):

Here’s an example:

https://en.wikivoyage.org/w/api.php?action=query&list=search&srwhat=text&srsearch=Andermatt,%20Switzerland&format=json&prop=info

This gives me the top pageid of “1127” for Andermat, Switzerland (featured in the 2014 list). It also gives me a snippet that reads “<span class=\”searchmatch\”>Andermatt</span> is a town and ski resort in the canton of Uri, <span class=\”searchmatch\”>Switzerland</span>. It is located at a crossroad in the Alps with great historical importance.”

Next, I needed to take the top placeid (which is right 95% of the time) and get the url for the page (so users can visit it to learn more). This was done with the call below for Andermat (aka pageid 1127):

https://en.wikivoyage.org/w/api.php?action=query&prop=info&pageids=1127&inprop=url&format=json

I imported and parsed all of the results straight into Google Sheets, again, using importjson.

Geocode the Data

Finally, I needed latitude and longitude for each location so that I can put them on an AirBnb-like map interface. I described how to geocode easily using Google’s geocoder here.

Final Data

My final data can be found (and forked!) here: https://docs.google.com/spreadsheets/d/1oOJQttPMHope_hkQ2RC7Epzr5ZmORtJEs2-Zo5mURYw/edit?usp=sharing

This is what it ended up looking like:

Now I’m ready to build an application that can display, filter, and otherwise consume this awesome travel data!

 

About the Author:

AirNYT: Clone Airbnb with New York Times Travel Data

October 7th, 2018

The New York Times has published a list of travel recommendations called ‘Places to go in [year]’ over the last 7+ years. The list length was standardized to 52 in 2014.

The top of the list often features ‘off-the-beaten-track’ places like Mexico City, Ghana, Houston, or the Albanian Coast. Each item in the lists features a photo or video of the place with a succinct summary of why people should go there. Check out 2018’s list here.

I’m a huge fan of these lists and I’ve long wanted a simple application to be able to explore all the places at once. What I’m imagining is something with a simple clean Airbnb-esque interface that would let people explore the travel recommendations on a list/map, filter by year, learn more, and bookmark their favorites (to hopefully go travel to them later).

And so that’s what I’ll build in this short tutorial series: A New York Times places-to-go meets Airbnb mashup built with React, Material-ui, and Firebase. I shall call it AirNYT.

I’ll break this tutorial series into a couple of parts:

Getting the Data

I’ll need to get the lists from New York Times pages over the last 7 years. I figure I can’t get in trouble by using a list of places in the world but would get a quick C&D for using their content. So I’m going to pull the actual place data from WikiVoyage and link to their pages (per Wikivoyage terms).

Setup + Architecture + Basic Components

I’ll use Create-react-app, React-router (4), and Material-ui to do this. This section will explore the power/ease of Material-ui, React, and React-router for building attractive interactive user interfaces quickly.

React-Virtualized + Material-UI Cards for Fast Lists

In this section, I’ll show how to use windowing (via React-Virtualized) to make 380+ images load quickly in a list.

Build the Map and Markers Interactivity

In this section, I’ll show how to build out a ‘toggle-able’ map with Airbnb-esque markers using a library called Google-map-react.

Build Pagination + Filtering + Responsiveness

In this section, I’ll show how to setup pagination, client-side filtering, and general responsiveness.

Setup the user favoriting functionality with firebase (coming soon)

The main purpose of this app is to inspire us to travel to new places. We need bookmarking to do this and Firebase is a simple way to do this within the React world. This section will cover creating user accounts and saving/retrieving user favorites.

Purpose of this Tutorial

I hope this tutorial shows you how to use React, Material-ui, and Firebase to build client-side mapping interfaces that could be recycled to build a number of applications. I’d love if this code/tutorial inspires people to build all sorts of map-based web apps.

The working version of this app can be found here: http://airnyt.surge.sh

About the Author:

25 Public React Repos/Apps to Inspire Your Next Project (part 2)

September 7th, 2018

We all love seeing what other developers have done with the tools we use. Like many front-end developers, we continue to be thoroughly in love with React and build most of our software with it. Part 1 of this blog series covered 12 solid React projects to inspire your next project. We’ll now finish the series with 13 more!

I combed Github for 13 more great React open-source projects for you to learn from and be inspired by. I hope these inspire you to try new design patterns, new libraries, or simply just build new things.

Note: Mind the licenses! Some, but not all, are MIT-licensed.

My criteria, during my Github combing, was that they be somewhat interesting, have a functioning demo (with the exception of mobile apps), and be updated after mid 2017 (and preferably much later).

/webamp (demo)

If you were online in the early 2000s, you probably used Winamp to listen to your Napster/Kazaa tracks. Here is a web app version of Winamp 2.9 that uses React, the web audio API, and many other libraries to recreate the player I used everyday when I was 11 (to listen to mostly crap).

/react-living-app

This simple web app will give you cost-of-living comparisons (via Numbeo and Teleport API’s data) between common world cities. The simple, clean design uses bootstrap and react-select (for smooth input controls).

/english-accents-map (demo)

This simple/fun map lets you explore (and hear) different English accents. This progressive web app (PWA) is built using React, Redux, Firebase, Material-design-lite, and the English Accents Database (it exists!😲).

/retro-board (demo)

This Trello board clone was built using React, Socket-IO, Redux, and MongoDB. It supports several languages and appears to use local storage to save user settings. This project has many contributors and translators and appears to be extremely well-documented.

/material-dashboard-react (demo)

Material-ui is, by far, the most popular ui-kit in use in the React world in 2018. This dashboard builds upon material-ui for a nice open source dashboard with many additional components. The author also offers ui-kits in other popular frameworks/libraries.

/react-reduction (demo)

No list like this would be complete without a Bootstrap based dashboard. This dashboard has a wide variety of pages and components, some of which use libraries like react d3 map and react-chartjs2. All in all, there’s a lot of great ui examples here that you can borrow or learn from.

/hackernews-react-graphql(demo)

This HackerNews demo looks exactly like the original HN and is powered by GraphQL and Universal React. This is a great demo to learn from if you’re considering diving in the ‘universal’ apps world.

/react-daily-ui (many demos in the repo)

Fullstack React offers many excellent tutorials and books on React. 30 Days of React is a free and incredibly comprehensive for learning React. This repo is a collection of many of the demos and code repos for their smaller tutorial series called react daily ui. As you might imagine, this tut series will show you the React way of building many common pieces of ui.

/food-help (demo)

This surprisingly full-featured Yelp clone deserves a lot more stars than it currently has (<10). This is a great example app for anyone looking to make a map based guide or review site using React.

/reactjs-tmdb-app (demo)

TMDb Movie Search is a responsive React app that utilizes Twitter’s typeahead.js and the Bloodhound suggestion engine, loading data via The Movie Database (TMDb) API. This would be great to checkout if you’re building a search interface.

/chatty (mobile/no demo)

Chatty is a WhatsApp clone built with React and Apollo. This great open source project also features a free long tutorial to show you how it was built.

/Yosoro (demo)

Yosoro is an elegant markdown-friendly note taking app build using  React, Redux, and Electron. Notes taken in Yosoro can be synced to Microsoft’s OneDrive storage product. This project would be a great one to check out if you’re interested in making cross-platform desktop apps using React and Electron.

/heard (mobile/no dem0)

Heard is an enterprise React Native Social Messaging App built using AWS AppSync & AWS Amplify. AppSync is a newer AWS product for building mobile and offline apps using GraphQL. AppSync seems to be a competitor to using Firebase or a platform like Graphcool.

The Heard repo gives you a nice tutorial on how to setup the GraphQL schema properly within AppSync so you should be able to clone this app, setup a database and get started. This looks to be a great project for someone wanting to see how GraphQL (in a AWS PaaS context) works with React.

/react-slack-clone (demo) [bonus #14)

This fully-featured React Slack Clone is built on the Pusher Chatkit (what seems to be a nicely designed PaaS). This demo is well componentized and simple (there’s only 7 dependencies in the package.json). This is a great project to check out if you need to include chat in your app but want some of the help that a PaaS product offers.

 

As with the first part of the series, I hope this projects inspire you, teach you, and get you building even more cool software with React.