« Previous Tutorial Next Tutorial »

Now that we have a front-end for adding albums, we need a back-end to power it. We’re going to build that out, along with our reducers, and a couple of minor features for the front-end just to make things operate a little more smoothly. In the immortal words of President Ike Eisenhower, as voiced by Krusty the Klown … LET’S GET BIZ-AY!

First, let’s adjust our User model so that we can save arrays of IDs for albums and artists. Open up /models/User.js and, first off, delete line 8. Passport is handling our passwords now, and we don’t want any risk of saving users’ passwords in plaintext in our database. Then, under line 6, add these two lines:

  albums: [Schema.Types.Mixed],
  artists: [Schema.Types.Mixed],

Let’s also take a second to alphabetize the rest of the lines. Then we’re done, so save the file.

Open up /routes/api/albums.js. Below line 4, import our User model like this:

const User = require('../../models/user.js');

Then head down to line 17, add a padding line, and let’s get started on our /add route. There’s some stuff happening in this route that we haven’t done before, so I’m going to do this one piece-by-piece. Here’s the route container:

// POST to /add
router.post('/add', async (req, res) => {
});

Now, let’s start filling stuff inside that. First off, we only want users who are logged in to be able to save albums to their profiles, so add this code:

  // Make sure a user's actually logged in
  if (!req.user) {
    return res.json({ error: 'User not logged in' });
  }

Then add a padding line below that, and let’s get weird! Here’s the code:

  // Wrap discogs API call in a promise so we can use async / await
  const discogsGetMaster = albumId => new Promise((resolve) => {
    discogsDB.getMaster(albumId, (err, data) => {
      resolve(data);
    });
  });

The Discogs API is callback-based, which means nesting functions. That’s fine, mildly annoying compared to async / await, so we’re wrapping the API call in our own function which returns an ES2015 promise, then resolves that promise once we have data to send. Now we can use that with async / await, which we’ll do below in a try / catch block.

In fact, here’s all the rest of the code:

  const albumId = parseInt(req.body.id, 10);
  let result;

  try {
    // Get album info from discogs AI
    const albumInfo = await discogsGetMaster(albumId);

    // Find the user we want to save to
    const query = User.findOne({ email: req.user.email });
    const foundUser = await query.exec();

    // Sanity Check! Is the album already added?
    const albumIndex = foundUser.albums.indexOf(albumInfo.id);
    if (albumIndex < 0) {
      foundUser.albums.push(albumInfo.id);
    }

    // Sanity Check 2! Is the artist already added?
    for (let i = 0; i < albumInfo.artists.length; i += 1) {
      const artistIndex = foundUser.artists.indexOf(albumInfo.artists[i].id);
      if (artistIndex < 0) {
        foundUser.artists.push(...albumInfo.artists.map(artist => artist.id));
      }
    }

    foundUser.save((error) => {
      if (error) {
        result = res.json({ error: 'Album could not be saved. Please try again.' });
      } else {
        result = res.json(foundUser);
      }
    });
  } catch (err) {
    result = res.json({ error: 'There was an error saving the album to the database. Please try again.' });
  }

  return result;

What we’re doing here should be pretty clear. We hit the Discogs API, await its result, get the user record associated with the logged-in user, and then we make sure the album and artist aren’t already saved to the user’s profile. If they’re not, we go ahead and save the album and artist IDs to the user’s DB reacord via the User object, throwing the requisite errors if things go wrong. Straightforward!

You may note something here: we’re not currently saving the actual album data or artist data to our database, just their IDs to our User object. We actually want to do both in this route, and we’ll be coming back here shortly to make that happen.

For now, we’re good, so save the file and move on to /src/reducers. We’re going to create a new file here called user.js which we’ll use to contain state information about a user that doesn’t include any authentication details. Here’s the code:

const initialState = {
  albums: [],
  artists: [],
};

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case 'AUTHENTICATION_LOGIN_SUCCESS':
    case 'AUTHENTICATION_SESSION_CHECK_SUCCESS': {
      const newState = Object.assign({}, state);
      newState.albums = action.json.albums;
      newState.artists = action.json.artists;
      return newState;
    }
    case 'MUSIC_ALBUM_ADD_SUCCESS': {
      const newState = Object.assign({ }, state);
      newState.albums = action.json.albums;
      return newState;
    }
    default: {
      return state;
    }
  }
}

This should be pretty straightforward. We’re updating the state when a user adds an album, but also when we do our initial session check or when a user logs in, which means we populate the user’s album IDs into the state right away, which means when we display search results we can identify which albums the user’s already added and mark them as such. Save this file. We need to include it in the Store, which means opening /src/reducers/index.js and adding this code under line 5:

import UserReducer from '../reducers/user';

And this code under line 12:

  user: UserReducer,

That’s it for that one, so save it, and open up /src/components/albums/AlbumsPageContainer.jsx. Let’s grab our user information from the Store and send it down to the Albums page as a prop. To do that, change line 32 to this code:

const mapStateToProps = state => ({ albums: state.albums, user: state.user });

And line 16 to this code:

    const { addAlbumFunction, albums, searchAlbumsFunction, user } = this.props;

Then add a line below line 21, with this code:

        user={user}

Save this file and then open up /src/components/albums/AlbumsPage.jsx. Find line 65 and, below it, add the following:

    const { user } = this.props;

Then change lines 75 to 77 from this:

            <Button color="primary" outline id={album.id} onClick={this.addAlbum}>
              Add To My List
            </Button>

To this:

            { user.albums.indexOf(album.id) < 0 ?
              <Button color="primary" outline id={album.id} onClick={this.addAlbum}>
                Add To My List
              </Button> :
              <span>Already Listed</span>
            }

That’s it! Now we can add an album and, when listing albums, show which ones have already been added. That’s great! Save the file, and let’s go check it out. Fire up a browser, log in, do a search for an album or artist you like, and add it. You should see the album switch to “already listed”. That’s good. Now do a hard-refresh and re-search to make sure it stays that way. Yup! Excellent. In the next tutorial, we’ll make some modifications to save albums to our DB, for faster lookups when we start displaying user profiles. Catch you there!

« Previous Tutorial Next Tutorial »