AirNYT: Setup + Architecture + Basic Components

Follow us on LinkedIn for our latest data and tips!

,

AirNYT: Setup + Architecture + Basic Components

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!