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:
1 2 3 4 5 6 7 8 9 |
"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:
1 2 3 |
<link href="https://fonts.googleapis.com/css?family=Poppins:400,600,700,800,900" rel="stylesheet"> |
And the following into my app.js:
1 2 3 4 5 6 7 8 9 10 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
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!