« Previous Tutorial Next Tutorial »

When last we left off, we could search albums and get a list of them back from Discogs to display to the user. Today we’re going to cover a bunch of front-end stuff we’ll need to let users save those albums to their profile. The first thing we’re going to do is pretty up the album listing. Then we’ll add some actions. Let’s get started!

Open up /src/components/albums/AlbumsPage.jsx. We’re going to add some functions, update a few others, and improve the cosmetics of our output significantly. First let’s import a Reactstrap table by changing line 3 to this:

import { Button, Label, Table } from 'reactstrap';

Next we need to create two new helper functions. For some reason, Discogs master listings come back with Titles that are formatted as “Artist - Title” and that’s not super useful for us. Also, their genres are an array, allowing albums to have multiple entries. That makes sense, but we’ll want to list them as comma-separated values. So, here’s a function that splits the title into the part before the dash and the part after it (and then returns whichever one we request), and a function that creates a comma-separated string from an Array. Add them just below line 5.

const formatTitle = (discogsTitle, value) => discogsTitle.split(' - ')[value];
const formatGenre = discogsGenre => discogsGenre.join(', ');

Next we’re going to move listAlbums into our class, because it turns out we’re going to need to access some props (they’ll show up in the next tutorial). So cut what are now lines 8 to 15, these guys:

const listAlbums = albums => albums.map(album =>
  (
    <p key={album.id}>
      <img src={album.thumb} alt="album thumbnail" />
      <strong>Title: {album.title}</strong><br />
    </p>
  ),
);

And paste them in below what is now line 41, the end of our handleValidSubmit function. Then modify them to look like this:

  listAlbums(albums) {
    return albums.map(album =>
      (
        <tr key={album.id}>
          <td><img src={album.thumb} alt="album thumbnail" width="80" height="80" /></td>
          <td>{formatTitle(album.title, 1)}</td>
          <td>{formatTitle(album.title, 0)}</td>
          <td>{formatGenre(album.genre)}</td>
          <td>
            <Button color="primary" outline id={album.id} onClick={this.addAlbum}>
              Add To My List
            </Button>
          </td>
        </tr>
      ));
  }

You may notice we’re generating table rows here. That means we also need to generate the table itself, but we can’t do that in the listAlbums function because it’d create a separate table for every individual album. So instead, add a new function in between handleValidSubmit and listAlbums like this:

  createTable(albums) {
    return (
      <Table striped responsive>
        <thead>
          <tr>
            <th />
            <th>Title</th>
            <th>Artist</th>
            <th>Genre(s)</th>
            <th />
          </tr>
        </thead>
        <tbody>
          { this.listAlbums(albums) }
        </tbody>
      </Table>
    );
  }

Then, below listAlbums (which should end on line 77 right now), let’s add a simple function for adding an album, which will use a prop that we’re not passing down yet … but that’s OK. Here’s the code:

  // Add an album to the user's list
  addAlbum(e) {
    const { addAlbumFunction } = this.props;
    // get id from button and send to the API
    addAlbumFunction(e.target.id);
  }

We need to bind all these functions, so head up to line 13, and make our bindings look like this:

    this.addAlbum = this.addAlbum.bind(this);
    this.createTable = this.createTable.bind(this);
    this.handleSearchChange = this.handleSearchChange.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
    this.handleValidSubmit = this.handleValidSubmit.bind(this);
    this.listAlbums = this.listAlbums.bind(this);

Last thing: change lines 120 and 121, which look like this:

            { albums && albums.length > 0 ? <div><hr /><h2>Albums</h2></div> : null }
            { albums && albums.length > 0 ? listAlbums(albums) : null }

To this:

            { albums && albums.length > 0 ? <h2>Albums</h2> : null }
            <div className="row">
              <div className="col-sm-12 col-lg-12">
                { albums && albums.length > 0 ? this.createTable(albums) : null }
              </div>
            </div>

We’re good here. Save the file and open /src/components/AlbumsPageContainer.jsx. Below line 1, add this:

import { bindActionCreators } from 'redux';

And change line 4 to this:

import { addAlbum, albumSearchClear, searchAlbums } from '../../actions/albums';

We’ll create that additional action in a minute. In the meantime, let’s use Redux’s built-in mapDispatchToProps functionality to reduce the need for helper functions that just dispatch action creators. Delete lines 21 to 24, our entire searchAlbumsFunction block, entirely. We don’t actually need it. This also means we don’t need our constructor anymore, so delete lines 9 to 14 while you’re at it. Then just above line 25, our mapStateToProps function, add this code:

const mapDispatchToProps = dispatch => bindActionCreators({
  addAlbumFunction: addAlbum,
  searchAlbumsFunction: searchAlbums,
  dispatch,
}, dispatch);

Note: we have to explicitly map dispatch in here because of how Redux works. If you don’t define mapDispatchToProps it automatically sends dispatch along as a prop, but if you define it manually, it leaves it up to you to send it. Note that we’re also gaining access to searchAlbums here instead of through the helper function we just deleted, as well as giving ourselves access to the addAlbum function we’re about to create. Cool. Change line 33 to this:

export default connect(mapStateToProps, mapDispatchToProps)(AlbumsPageContainer);

And then change line 16 to this code:

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

And change lines 19 and 20 to this:

        addAlbumFunction={addAlbumFunction}
        albums={albums}
        searchAlbumsFunction={searchAlbumsFunction}

Save this file and let’s create that addAlbum function. Open /src/actions/albums.js. Below line 5, add two new action creators:

export const albumAddFailure = error => ({ type: 'MUSIC_ALBUM_ADD_FAILURE', error });
export const albumAddSuccess = json => ({ type: 'MUSIC_ALBUM_ADD_SUCCESS', json });

Then, below line 10, add a padding line and the following function. It’s reaching out to an API endpoint that we’ll create in the next tutorial. Here’s the code:

// Add an Album
export function addAlbum(id) {
  return async (dispatch) => {
    // clear the error box if it's displayed
    dispatch(clearError());

    // turn on spinner
    dispatch(incrementProgress());

    // Send packet to our API, which will communicate with Discogs
    await fetch(
      // where to contact
      '/api/albums/add',
      // what to send
      {
        method: 'POST',
        body: JSON.stringify({ id }),
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'same-origin',
      },
    )
    .then((response) => {
      if (response.status === 200) {
        return response.json();
      }
      return null;
    })
    .then((json) => {
      if (json.email) {
        return dispatch(albumAddSuccess(json));
      }
      return dispatch(albumAddFailure(new Error(json)));
    })
    .catch(error => dispatch(albumAddFailure(new Error(error))));

    // turn off spinner
    return dispatch(decrementProgress());
  };
}

That’s it. Obviously this function won’t work yet, since we don’t have the API endpoint, but it also won’t break anything unless we click a button, which will at least allow us to test our visual changes! We’re going to hold off on creating reducers until the next tutorial, because we don’t have any data to send to them right now anyway, so for now, we’re done with code. Save the file.

Head for a browser, navigate to localhost:3000/albums and perform a new search. You should see your table full of albums come up, with an “add album” button for each. Obviously there’s a great deal of UX improvement you could do here, such as adding checkboxes to add multiple albums at once, but this tutorial is about creating a barebones app. So, for the sake of brevity we’re going to stick to what we’ve got. It still looks a whole lot better than it did!

In the next tutorial, we’ll make those buttons work, and make them disappear when a user’s already saved any given album to their profile. See you then!

« Previous Tutorial Next Tutorial »