Build a Music Player with React & Electron II: Making the UI

Chris Nwamba
👁️ 19,152 views
💬 comments

In the previous article we setup an Electron project, discussed the basic concepts of React and created a useless example to get us started. Now we will start making something - a music app.

We also discussed about the types of components which are presentation and container components. In this tutorial we will focus on our presentation component and talk about container in the next.

Revisit app Directory Structure

|---app #All React projects goes here
|----components # Presentation Component Directory
|------details.component.js
|------footer.component.js
|------player.component.js
|------progress.component.js
|------search.component.js
|----containers # Container Component Directory
|------app.container.js
|----app.js

The app folder has two sub folders and an app.js at its root. Our only concern in this section of the series is app/components where our presentation component will live and app.js which will put the components together and tie them to the DOM.

Table of Contents

    Components

    We will build the presentation component one after the other. The idea of components makes it easier for designers to work with wireframes and transform the wire-frames into components. Below is a wireframe for Scotch Player:

    React.js and Electron Music Player Wireframe

    With the above wireframe, it becomes really simple to fish out the components and reason around them. We have 5 UI (presentation) components:

    1. Search
    2. Details
    3. Player
    4. Progress
    5. Footer

    A presentation component can also be referred to as UI component

    Search (UI) Component

    This component lives in app/components/search.component.js and has the same basic React component skeleton:

    // Import React
    import React from 'react';
    
    // Create Search component class
    class Search extends React.Component{
    
      render() {
        // Return JSX via render()
        return (
          <div className="search">
    
          </div>
        );
      }
    
    }
    
    // Export Search
    export default Search

    What will be cool is that instead of using the usual search form with a submit button, we could make use of an auto-complete component. The React team has an awesome auto-complete component which we aready included in the package.json; no need to re-invent the wheel:

    // Import React
    import React from 'react';
    
    // Import React's Autocomplete component
    import Autocomplete from 'react-autocomplete';
    
    // Create Search component class
    class Search extends React.Component{
    
      render() {
        // Return JSX via render()
        return (
          <div className="search">
            {/*Autocomplete usage with value and behavior handled via this.props*/}
            <Autocomplete
             ref="autocomplete"
             inputProps={{title: "Title"}}
             value={this.props.autoCompleteValue}
             {/*Array of tracks is passed in to items*/}
             items={this.props.tracks}
             {/*Single value selected*/}
             getItemValue={(item) => item.title}
             {/*What happens when an item is selected*/}
             onSelect={this.props.handleSelect}
             {/*What happens when keystrokes are received*/}
             onChange={this.props.handleChange}
             {/*How items are redered.*/}
             renderItem={this.handleRenderItem.bind(this)}
           />
          </div>
        );
      }
    
    }
    
    // Export Search
    export default Search

    Although {/* */} is the proper way to comment in JSX, React will not allow those comments between properties, therefore, remove in your usage.

    renderitem is the only property that does not receive props but value which is handleRenderitem and is created as a method in the Search class:

    // ...
    //class Search extends React.Component{
      handleRenderItem(item, isHighlighted){
        // Some basic style
        const listStyles = {
          item: {
            padding: '2px 6px',
            cursor: 'default'
          },
    
          highlightedItem: {
            color: 'white',
            background: '#F38B72',
            padding: '2px 6px',
            cursor: 'default'
          }
        };
    
        // Render list items
        return (
          <div
            style={isHighlighted ? listStyles.highlightedItem : listStyles.item}
            key={item.id}
            id={item.id}
          >{item.title}</div>
        )
      }
    //  render() {
    // ...

    Notice how styles are pased in too. In React there is room for global styles and inline dynamic styles as seen above.

    Note that the Search component has no constructor method. If all a component does is receive props (which is exactly what UI components do), then the constructor is not needed.

    To see what we have done so far, update the app.js with the following:

    // ES6 Component
    // Import React and ReactDOM
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    // Import Search Component
    import Search from './components/search.component';
    
    // Component Class
    class App extends React.Component {
        // render method is most important
        // render method returns JSX template
        render() {
            return (
              <Search />
            );
        }
    }
    
    // Render to ID content in the DOM
    ReactDOM.render(
        <App/ >,
        document.getElementById('content')
    );

    See how Search is imported and used inside App. No functionality attached yet, remember this section is just about UI

    React Electron Music Player Search

    Yeah! So darn ugly! We will fix that later.

    Details (UI) Component

    This is the simplest component we will create as it just has a h3 tag with the track title:

    // Import React
    import React from 'react';
    
    class Details extends React.Component {
      // Render
      render(){
        return(
          <div className="details">
            <h3>{this.props.title}</h3>
          </div>
        )
      }
    
    }
    // Export
    export default Details

    We update app.js with Details:

    // ES6 Component
    // Import React and ReactDOM
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    // Import Search Component
    import Search from './components/search.component';
    
    // Import Details Component
    import Details from './components/details.component';
    
    // Component Class
    class App extends React.Component {
        // render method is most important
        // render method returns JSX template
        render() {
            return (
              <div>
                <Search />
                {/* Added Details Component */}
                <Details title={'Track title'} />
              </div>
            );
        }
    }
    
    // Render to ID content in the DOM
    ReactDOM.render(
        <App/ > ,
        document.getElementById('content')
    );
    

    We import the Details component and add it to the App.

    JSX will fail to compile if we do not wrap and expose the content using only one tag. That's why we wrapped Search and Details with div

    React Electron Music Player Details

    Player (UI) Component

    The player component is made of the controls for our music player:

    // Import React
    import React from 'react';
    
    // Import ClassNames
    import ClassNames from 'classnames';
    
    // Player component class
    class Player extends React.Component {
    
      render(){
        // Dynamic class names with ClassNames
        const playPauseClass = ClassNames({
          'fa fa-play': this.props.playStatus == 'PLAYING' ? false : true,
          'fa fa-pause': this.props.playStatus == 'PLAYING' ? true : false
        });
    
        // Return JSX
        return(
          <div className="player">
            {/*Rewind Button*/}
            <div className="player__backward">
              <button onClick={this.props.backward}><i className="fa fa-backward"></i></button>
            </div>
            <div className="player__main">
              {/*Play/Pause Button*/}
              <button onClick={this.props.togglePlay}><i className={playPauseClass}></i></button>
              {/*Stop Button*/}
              <button onClick={this.props.stop}><i className="fa fa-stop"></i></button>
              {/*Random Track Button*/}
              <button onClick={this.props.random}><i className="fa fa-random"></i></button>
            </div>
            {/*Forward Button*/}
            <div className="player__forward">
              <button onClick={this.props.forward}><i className="fa fa-forward"></i></button>
            </div>
          </div>
        )
      }
    
    }
    
    // Export Player
    export default Player

    The new stuff to observe in this component is that we imported a classnames utility library which helps us dynamically set classes. It is useful here to swith the play/pause button to pause or play depending on the current state of the track.

    The events callbacks will be handled in the container component which we will discuss in the next section. Next, include Player component in app.js:

    // ES6 Component
    // Import React and ReactDOM
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    // Import Search Component
    import Search from './components/search.component';
    
    // Import Details Component
    import Details from './components/details.component';
    
    // Import Player Component
    import Player from './components/player.component';
    
    // Import Progress Component
    // Component Class
    class App extends React.Component {
    
        // render method is most important
        // render method returns JSX template
        render() {
            return (
              <div>
                <Search />
                <Details title={'Track title'} />
                {/* Added Player component*/}
                <Player  />
              </div>
            );
        }
    
    }
    
    // Render to ID content in the DOM
    ReactDOM.render(
        <App/ > ,
        document.getElementById('content')
    );

    We need to add font-awesome to index.html for the control icons to look as expected:

    <!-- ... -->
      <title>Scotch Player</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
    <!-- ... -->

    React Electron Music Player Play Buttons

    You will end up dissapointed if you try clicking them - no behavior or state yet. Relax!

    Progress (UI) Component

    This guy is a simple one also - just a:

    • progress bar
    • total play time
    • elapsed time:
    // Import React
    import React from 'react';
    
    // Create Progress component class
    class Progress extends React.Component {
    
      // Render method
      render() {
    
        return(
          <div className="progress">
            {/* Elapsed time */}
            <span className="player__time-elapsed">{this.props.elapsed}</span>
            {/* Progress Bar */}
            <progress
               value={this.props.position}
               max="1"></progress>
             {/* Total time */}
             <span className="player__time-total">{this.props.total}</span>
          </div>
        )
      }
    
    }
    
    //Export Progress
    export default Progress

    And now to include this component in app.js like we did for the others:

    // ES6 Component
    // Import React and ReactDOM
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    // Import Search Component
    import Search from './components/search.component';
    
    // Import Details Component
    import Details from './components/details.component';
    
    // Import Player Component
    import Player from './components/player.component';
    
    // Import Progress Component
    import Progress from './components/progress.component';
    
    // Component Class
    class App extends React.Component {
    
        // render method is most important
        // render method returns JSX template
        render() {
            return (
              <div>
                <Search />
                <Details title={'Track title'} />
                <Player  />
                {/* Added Progress component*/}
                <Progress
                  position={'0.3'}
                  elapsed={'00:00'}
                  total={'0:40'}/>
              </div>
            );
        }
    
    }
    
    // Render to ID content in the DOM
    ReactDOM.render(
        <App/ > ,
        document.getElementById('content')
    );
    

    After updating the app.js as we have been doing, you should have something like the image below:

    React Electron Music Player Progress Bar

    Footer (UI) Component

    The last is the footer component. This is a completely dumb static component:

    import React from 'react';
    
    class Footer extends React.Component {
      render(){
        return(
          <div className="footer">
            <p>Love from <img src="public/img/logo.png" className="logo"/>
                & <img src="public/img/soundcloud.png" className="soundcloud"/></p>
          </div>
        )
      }
    
    }
    
    export default Footer

    Include in app.js, add the images in the public/img directory and the app should be looking like the image below:

    React Electron Music Player Logo

    Awful look, right? Let's fix that.

    Global Styles

    React supports inline styles which we have seen. Inline styles are handy for dynamic functionality and to compose reusable components. This does not mean that we cannot use our global style and at the time of this writing, React has no rigid recommendation for how styles are added.

    We won't spend much time talking about the styles as it is out of scope. Just dump the following in public/css/globals.css and wait for the magic:

    /* Box sizing resets*/
    *, *:before, *:after {
      box-sizing: border-box;
    }
    
    /* Body resets and fancy font*/
    body{
      margin: 0;
      font-family: 'Exo 2', sans-serif;
    }
    
    .scotch_music{
      position: relative;
    }
    
    .search div, .search input {
      width: 100%;
    }
    
    .search input {
      border: none;
      border-bottom: 2px solid rgb(243, 139, 114);
      outline: none;
      background: rgba(255, 255, 255, 0.8);
      padding: 5px;
    }
    
    .details h3{
      text-align: center;
      padding: 50px 10px;
      margin: 0;
      color: white;
    }
    
    .player{
      text-align: center;
      margin-top: 60px;
    }
    
    .player div{
      display: inline-block;
      margin-left: 10px;
      margin-right: 10px;
    }
    
    .player .player__backward button, 
    .player .player__forward button{
      background: transparent;
      border: 1px solid rgb(243, 139, 114);
      color: rgb(243, 139, 114);
      width: 75px;
      height: 75px;
      border-radius: 100%;
      font-size: 35px;
      outline: none;
    }
    
    .player .player__backward button{
      border-left: none;
    }
    
    .player .player__forward button{
      border-right: none;
    }
    
    .player .player__main button:hover, 
    .player .player__backward button:hover, 
    .player .player__forward button:hover{
      color: rgba(243, 139, 114, 0.7);
      border: 1px solid rgba(243, 139, 114, 0.7);
    }
    
    .player .player__main {
      border: 1px solid rgb(243, 139, 114);
    }
    
    .player .player__main button {
      color: rgb(243, 139, 114);
      background: transparent;
      width: 75px;
      height: 75px;
      border: none;
      font-size: 35px;
      outline: none;
    }
    
    .progress{
      text-align: center;
      margin-top: 100px;
        color: white;
    }
    
    .progress progress[value] {
      /* Reset the default appearance */
      -webkit-appearance: none;
       appearance: none;
    
      width: 390px;
      height: 20px;
      margin-left: 4px;
      margin-right: 4px;
    }
    
    .progress progress[value]::-webkit-progress-bar {
      background-color: #eee;
      border-radius: 2px;
      box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
    }
    
    .progress progress[value]::-webkit-progress-value {
        background-color: rgb(243, 139, 114);
        border-radius: 2px;
        background-size: 35px 20px, 100% 100%, 100% 100%;
    }
    
    .footer{
      color: white;
      position: absolute;
      bottom: 0px;
      width: 100%;
      background: #524C4C;
    }
    
    .footer p{
      text-align: center;
    }
    
    .footer .logo{
      height: 25px;
      width: auto;
    }
    .footer .soundcloud{
      height: 25px;
      width: auto;
    }

    We need to import the global.css and Exo 2 font in the index.html:

    <link rel="stylesheet" href="public/css/global.css">
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
        <link href='https://fonts.googleapis.com/css?family=Exo+2:500' rel='stylesheet' type='text/css'>

    React Electron Music Player Styled

    Looks better, right? We're another big step closer to achieving our fully functional React and Electron music player!

    Up Next...

    Our presentation components are ready and styled (though needs some more touches). In the next section we will create our container component. See you soon...

    Chris Nwamba

    104 posts

    JavaScript Preacher. Building the web with the JS community.