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: 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