« Previous Tutorial Next Tutorial »

In our last tutorial, we set up a basic list view that allowed us to see what artists and albums a user had added to their list. In this tutorial, we’re going to pretty that up and add some “remove from list” buttons that will only be available if a user is viewing their own profile.

Let’s start with some CSS that’ll come in handy. Open /src/css/musiclist.scss and, at the end of the file, add this code:

/ Flexbox tables /
table.flex {
  tr {
    display: flex;

    th, td {
      flex-basis: 25%;

      &.thumb {
        flex-basis: auto;
        width: 100px;
        text-align: center;
        padding: 10px;

        img {
          width: 80px;
        }
      }
    }
  }
}

This is going to help us keep our unconnected tables looking the same, in terms of formatting. It should work in any modern browser, and older browsers will degrade nicely – they’ll still have bootstrap-styled tables, but things won’t line up quite as perfectly.

Now let’s make those tables. Save this file and open /src/components/list/ListPageContainer.jsx. We need to pass some authentication information down to our component. Below line 68, grab authentication from the Store like this:

  authentication: state.authentication,

Change line 45 to this:

    const { authentication, list } = this.props;

Then alphabetize lines 52 to 54, and, below line 53, add this:

        authentication={authentication}

Save this file and open /src/components/list/ListPage.jsx. We’re going to add two methods here that are similar to ones we wrote for the artists and albums pages, and generate a table. However, these methods will be smart enough to take a list of either of those two things and generate a table from them. This is, obviously, a better approach that writing four separate methods, two for artists and two for albums. We need to import some stuff from ReactStrap, so, below line 1, add this code:

import { Button, Table } from 'reactstrap';

And then, below line 15, add a padding line and the following method:

  createTable(items) {
    return (
      <Table striped responsive className="flex">
        <thead>
          <tr>
            <th className="thumb" />
            <th>{ items.type === 'album' ? 'Title' : 'Name' }</th>
            <th>{ items.type === 'album' ? 'Artist' : 'Active Members' }</th>
            <th>{ items.type === 'album' ? 'Genre(s)' : ' ' }</th>
            <th />
          </tr>
        </thead>
        <tbody>
          { this.listItems(items) }
        </tbody>
      </Table>
    );
  }

Now, below that method, which ends on line 34, add a padding line, and then our listItems method, like this:

  listItems(items) {
    const { authentication, username } = this.props;
    return items.list.map(item =>
      (
        <tr key={item.discogsId}>
          <td className="thumb"><img src={item.images[0] ? item.images[0].uri : ''} alt="item thumbnail" /></td>
          <td>{ items.type === 'album' ? item.title : item.name }</td>
          <td>{ items.type === 'album' ? item.artists[0].name : formatMembers(item.members) }</td>
          <td>{ items.type === 'album' ? formatGenre(item.genres) : ' ' }</td>
          <td>
            { username === authentication.username ?
              <Button
                color="secondary"
                onClick={() => this.deleteItem(item.discogsId, items.type)}
                outline
              >
                Remove From List
              </Button> :
              null
            }
          </td>
        </tr>
      ));
  }

We’re using a lot of ternary operators here to provide two different possible outputs. You should be pretty comfortable with them by now, but in case they’re still confusing, we covered them back in Tutorial 26.

You may also note that this uses several functions that don’t actually exist yet, such as formatMembers. Let’s fix that. Up at the top of the file, from lines 4 to 10, we’ve got some basic formatting functions. We’re going to nuke those in favor of the more specific ones called by our new methods. So highlight this entire block:

const formatAlbums = albums => albums.map(album => (
  <p key={album.discogsId}>
    <em>{album.title}</em> by
    {album.artists[0].name}
  </p>
));
const formatArtists = artists => artists.map(artist => <p key={artist.discogsId}>{artist.name}</p>);

delete it, and replace it with the following code:

const formatGenre = discogsGenre => discogsGenre.join(', ');
const formatMembers = (discogsMembers) => {
  const activeMembers = discogsMembers.filter(member => member.active);
  const memberNames = activeMembers.map(member => member.name);
  return memberNames.join(', ');
};

Now we’re going to create a not-yet-implemented deleteItem method. Below our constructor, which now ends on line 14, add a padding line, and this code:

  deleteItem(id, type) {
    console.log(id, type);
  }

Last but not least we need to change our render block to use our new methods. First, below line 65, add these lines:

    const albumsFormatted = { type: 'album', list: albums };
    const artistsFormatted = { type: 'artist', list: artists };

And then replace line 74 with this code:

            { artists && artists.length > 0 ? this.createTable(artistsFormatted) : null }

And line 78 with this code:

            { albums && albums.length > 0 ? this.createTable(albumsFormatted) : null }

You should be good to go, here, so let’s make sure things are working. Save the file, head for a browser, and visit localhost:3000/list/yourusername. You should see the artists and albums you’ve added, now in a prettier format. If you log in, you should also see delete buttons, which will console log some information when you click them.

Getting all our API endpoints, actions, and reducers set up for those delete buttons would push this tutorial way over five minutes, so let’s take a brief diversion instead. I’m tired of having to manually navigate to all these pages. Let’s add a menu to our application.

Open /src/components/shared/Header.jsx and, first, change line 45 to these three lines:

          <span className="nav-link">Welcome, {name}
           | <a href="/logout" onClick={this.logOutClick}>Log Out</a>
          </span>

Yes, it’s a little weird to wrap non-link text in a nav-link class, but it’ll help keep things looking right.

Next, change line 54 to include a username prop, like this:

    const { isLoggedIn, firstName, username } = this.props.authentication;

Now, directly below line 60, add the following:

            <Nav className="ml-auto" navbar>
              <NavItem>
                <NavLink tag={Link} to="/albums">Albums</NavLink>
              </NavItem>
              <NavItem>
                <NavLink tag={Link} to="/artists">Artists</NavLink>
              </NavItem>
              { username && username !== '' ?
                <NavItem>
                  <NavLink tag={Link} to={/list/${username}}>My List</NavLink>
                </NavItem>
              : null }
            </Nav>

This will add handy artists and albums links in the menu, along with a link to your list that only shows up if you’re logged in. This will make bouncing back and forth between searches and your list a lot easier, which is handy for testing things like the delete button.

We’ve got a little more time so let’s make one more improvement. Save this file and then open /src/components/artists/ArtistsPageContainer.jsx. Right now the artist search page shows “add to my list” buttons even when a user’s not logged in. That sucks! To fix it, we need to pass down authentication as a prop, so change line 33 to these five lines:

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

And then change line 16 to this:

    const { addArtistFunction, authentication, artists, searchArtistsFunction, user } = this.props;

And below line 20 add this code:

        authentication={authentication}

Save that file and open /src/components/artists/ArtistsPage.jsx. Add a padding line below line 57 and then add this method:

  generateButton(user, artist) {
    return (
      user.artists.indexOf(artist.id) < 0 ?
        <Button color="primary" outline id={artist.id} onClick={this.addArtist}>
          Add To My List
        </Button> :
        <span>Already Listed</span>
    );
  }

Then change line 70 to this code:

    const { user, authentication } = this.props;

And change lines 76 to 83 from to this single line of code:

          <td>{authentication.username.length > 0 ? this.generateButton(user, artist) : null}</td>

Save the file, and open /src/components/albums/AlbumsPageContainer.jsx. We’re going to do exactly the same thing here. Change line 33 to this:

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

And line 16 to this:

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

And add this code below line 20:

        authentication={authentication}

Save the file, and hop over to /src/components/albums/AlbumsPage.jsx. Same deal here: identical changes. First, add a padding line below line 63 and then our generateButton function like this:

  generateButton(user, album) {
    return (
      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>
    );
  }

Then change line 76 to this code:

    const { authentication, user } = this.props;

And then change lines 84 to 91 to this single line:

          <td>{authentication.username.length > 0 ? this.generateButton(user, album) : null}</td>

You’re all set. Save that file, head for a browser, and do some artist / album searching while not logged in. No buttons! Now log in, and repeat the searches, and watch your buttons appear. Terrific!

« Previous Tutorial Next Tutorial »