harunpehlivan
2/1/2018 - 2:31 PM

React | Crypto UI

React | Crypto UI

<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://codepen.io/TheVVaFFle/pen/e1f2a0ef54733cf90c8aed4d7233b027" rel="stylesheet" />
$lightBlue: rgb(79,195,247);
$darkBlue: rgb(13,71,161);

$lightGreen: rgb(102,187,106);
$darkGreen: rgb(27,94,32);
$limeGreen: rgb(205,220,57);

$lightYellow: rgb(255,235,59);
$yellow: rgb(253,216,53);
$goldYellow: rgb(251,192,45);

$orange: rgb(255,152,0);
$darkOrange: rgb(230,81,0);

body{
  overflow: hidden;
  h1, h2, h3, div{
    color: $gray60;
    font-family: 'Roboto', sans-serif;
    margin: 0px;
  }
}

body, html, #root, #app{
  height: 100%;
  margin: 0px;
  padding: 0px;
  width: 100%;
}

.scroll-bar{
  &::-webkit-scrollbar {
    height: 0px;
    width: 0px;
  }

  &::-webkit-scrollbar-thumb {
    background-color: $gray240;
  }
}

#app{
  &.updating{
    #card-wrapper{
      #card{
        #card-left{
          #coin-symbol-vert{
            opacity: 0;
            transform: rotate(-90deg) translateY(20px);
          }
        }
        
        #card-right{
          #card-right-contents{
            #coin-header, #coin-price, #coin-info{
              opacity: 0;
              transform: translateY(20px);
            }
            
            #card-right-stripes{
              opacity: 0;
              transform: translateX(100%) translateY(100%);
            }
          }
        }
      }
    }
  }
  
  #particles{
    height: 100%;
    left: 0px;
    position: fixed;  
    top: 0px;
    width: 100%;
    z-index: 1;
  }
  
  #help-tooltip{
    bottom: 10px;
    left: 10px;
    position: fixed;
    transition: all 0.4s;
    z-index: 4;
    
    &.hide{
      opacity: 0;
    }
    
    i{
      font-size: 2em;
      height: 50px;
      line-height: 50px;
      text-align: center;
      vertical-align: top;
      width: 50px;
    }
    
    h1{
      animation: bounce-tooltip 3s ease-in-out infinite;
      display: inline-block;
      font-size: 1em;
      position: relative;
      .text{
        background-color: white;
        border-radius: 2px;
        box-shadow: $shadow1;
        display: inline-block;
        height: 40px;
        font-size: 1em;
        font-weight: 100;
        line-height: 40px;
        margin: 5px 0px;
        margin-left: 10px;
        padding: 0px 15px;
        position: relative;
        vertical-align: top;
        z-index: 2;
      }
      
      &:before, .triangle{
        background-color: white;
        box-shadow: $shadow1;
        display: inline-block;
        height: 20px;
        left: 0px;
        position: absolute;
        top: 50%;
        transform: translateY(-50%) rotate(45deg);
        width: 20px;
        z-index: 1;
      }
      
      &:before{
        box-shadow: none;
        content: '';
        z-index: 3;
      }
    }
  }
  
  #card-loading{
    @include center;
    height: 300px;
    width: 300px;
    z-index: 3;
    #card-loading-spinner{
      @include center;
      
      &:before, &:after{
        @include center;
        content: '';
      }
      
      &:before{
        animation: rotate 3s linear infinite;
        border-bottom: 1px solid transparent;
        border-left: 1px solid $lightBlue;
        border-right: 1px solid $lightBlue;
        border-top: 1px solid transparent;
        border-radius: 1000px;
        height: 150px;
        width: 150px;
      }
      
      &:after{
        animation: rotate-reverse 3s linear infinite;
        border-bottom: 1px solid $gray200;
        border-left: 1px solid transparent;
        border-right: 1px solid transparent;
        border-top: 1px solid $gray200;
        border-radius: 1000px;
        height: 120px;
        width: 120px;
      }
      
      height: 150px;
      width: 150px;
    }
  }
  
  #card-wrapper{
    @include center;
    height: calc(100% - 40px);
    pointer-events: none;
    width: calc(100% - 40px);
    z-index: 3;

    &.orange{
      #card{
        #card-left{
          background-image: linear-gradient(45deg, $lightYellow, $orange);
        }

        #card-right{
          #card-right-contents #coin-header{
            #coin-symbol h1{
              color: $orange;
            }

            #coin-rank{
              .value{
                h1{
                  color: $orange;
                }
              }
            }
          }
        }
      }
    }

    #card{
      @include center;
      animation: fade-in-up 1s ease-in-out;
      font-size: 0px;
      height: 400px;
      pointer-events: initial;
      width: 647px;

      .card-half{
        background-color: white;
        display: inline-block;
        height: 100%;
        position: relative;
        vertical-align: top;
        width: 50%;
      }

      #card-left{
        box-shadow: $shadow3;
        z-index: 1;

        &:hover{
          #coin-selection{
            opacity: 1;
            pointer-events: initial;
            
            #coin-options-wrapper{
              #coin-options{
                .coin-option{
                  .coin-option-icon{
                    transform: scale(0.7);
                  }
                }
              } 
            }
          }

          #coin-icon{
            opacity: 0;
          }
        }

        #coin-icon{
          background-color: white;
          background-position: center;
          background-repeat: no-repeat;
          background-size: cover;
          border-radius: 1000px;
          box-shadow: $shadow3;
          height: 260px;
          left: 0px;
          position: absolute;
          top: 50%;
          transform: translateX(-40px) translateY(-50%);
          transition: all 0.2s;
          width: 260px;
          z-index: 2;
        }

        #coin-symbol-vert{
          bottom: 100px;
          height: 120px;
          margin: 10px;
          position: absolute;
          right: -100px;
          transform: rotate(-90deg);
          transition: all 0.2s;
          width: 320px;

          h1{
            color: rgba(white, 0.2);
            font-size: 150px;
            font-weight: 700;
            height: 120px;
            line-height: 120px;
            margin: 0px;
          }
        }

        #coin-selection{
          height: 100vh;
          left: 0px;
          margin-left: -60px;
          opacity: 0;
          pointer-events: none;
          position: absolute;
          top: 50%;
          transform: translateY(-50%);
          transition: all 0.2s;
          width: calc(100% + 60px);
          z-index: 3;

          &:before, &:after{
            height: 15vh;
            content: '';
            left: 0px;
            position: absolute;
            width: 100%;
            z-index: 2;
          }

          &:before{
            background: linear-gradient(to bottom, white, transparent);
            top: 0px;
          }

          &:after{
            background: linear-gradient(to top, white, transparent);
            bottom: 0px;
          }

          #coin-options-wrapper{
            height: 100%;
            overflow: auto;
            width: 100%;
            #coin-options{
              margin: calc(50vh - 130px) 0px;
              padding-left: 20px;
              position: relative;
              z-index: 1;
              .coin-option{
                margin-bottom: 20px;
                position: relative;
                
                &.selected{
                  .coin-option-icon{
                    opacity: 1;
                    transform: scale(1);
                  }
                }
                
                .coin-option-icon{
                  background-color: white;
                  background-position: center;
                  background-repeat: no-repeat;
                  background-size: cover;
                  border-radius: 1000px;
                  box-shadow: $shadow3;
                  height: 260px;
                  opacity: 0.8;
                  transform: scale(0.2);
                  transition: all 0.2s;
                  width: 260px;
                }
              }
            }
          }
        }
      }

      #card-right{
        z-index: 2;
        #card-right-contents{
          background-color: white;
          box-shadow: $shadow3;
          height: 460px;
          margin-top: -30px;
          overflow: hidden;
          position: relative;

          #coin-header{
            border-bottom: 1px solid $gray240;
            margin: 20px;
            margin-bottom: 10px;
            padding-bottom: 10px;
            position: relative;
            transition: all 0.2s;

            #coin-name{
              h1{
                font-size: 30px;
                font-weight: 100;
                text-transform: uppercase;
              }
            }

            #coin-symbol{
              h1{
                color: $gray200;
                font-size: 15px;
                font-weight: 300;
              }
            }

            #coin-rank{
              backface-visibility: hidden;
              display: inline-block;
              position: absolute;
              right: 0px;
              top: 0px;
              .label, .value{
                display: inline-block;
                h1{
                  font-size: 20px;
                }
              }

              .label{
                h1{
                  color: $gray200;
                  font-weight: 400;
                  text-transform: uppercase;
                }
              }
              .value{
                margin-left: 6px;
                h1{
                  font-weight: 100;
                  font-size: 2
                }
              }
            }
          }

          #coin-price{
            backface-visibility: hidden;
            margin: 20px;
            margin-top: 0px;
            transition: all 0.2s;
            .value{
              display: inline-block;
              vertical-align: top;
              h1{
                font-size: 40px;
                font-weight: 100;
              }
            }
            
            #coin-change-24hr{
              display: inline-block;
              margin: 5px;
              vertical-align: top;
              
              &.positive{
                h1{
                  color: $lightGreen;
                }
              }
              
              &.negative{
                h1{
                  color: $red;
                }
              }
              
              h1{
                color: $gray60;
                font-size: 20px;
                font-weight: 100;
              }
            }
          }

          #coin-info{
            margin: 20px;
            transition: all 0.2s;
            .coin-info-field{
              margin-top: 20px;
              .value{
                h1{
                  font-size: 20px;
                  font-weight: 300;
                }
              }

              .label{
                margin-top: 4px;
                h1{
                  color: $gray200;
                  font-size: 12px;
                  font-weight: 400;
                  text-transform: uppercase;
                }
              }
            }
          }
          
          #card-right-stripes{
            bottom: 0px;
            height: 50px;
            right: 0px;
            position: absolute;
            transition: all 0.2s;
            width: 50px;
            
            &:before, &:after{
              background-color: red;
              content: '';
              height: 200px;
              position: absolute;
            }
            
            &:after{
              box-shadow: $shadow2;
              left: 0px;
              top: -70px;
              transform: rotate(45deg);
              width: 80px;
            }
          }
        }
      }
    }
  }

  // Color specific

  #card-wrapper{
    &.orange{
      #card{
        #card-left{
          background-image: linear-gradient(45deg, $lightYellow, $orange);
        }

        #card-right{
          #card-right-contents {
            #coin-header{
              #coin-symbol h1{
                color: $orange;
              }

              #coin-rank .value h1{
                color: $orange;
              }
            }
            
            #card-right-stripes{
              &:before, &:after{
                background-color: $orange;
              }
            }
          }
        }
      }
    }

    &.blue{
      #card{
        #card-left{
          background-image: linear-gradient(45deg, $lightBlue, $darkBlue);
        }

        #card-right{
          #card-right-contents{
            #coin-header{
              #coin-symbol h1{
                color: $lightBlue;
              }

              #coin-rank .value h1{
                color: $lightBlue;
              }
            }
            
            #card-right-stripes{
              &:before, &:after{
                background-color: $lightBlue;
              }
            }
          }
        }
      }
    }

    &.green{
      #card{
        #card-left{
          background-image: linear-gradient(45deg, $lightGreen, $limeGreen);
        }

        #card-right{
          #card-right-contents{
            #coin-header{
              #coin-symbol h1{
                color: $lightGreen;
              }

              #coin-rank .value h1{
                color: $lightGreen;
              }
            }
            
            #card-right-stripes{
              &:before, &:after{
                background-color: $lightGreen;
              }
            }
          }
        }
      }
    }

    &.gray{
      #card{
        #card-left{
          background-image: linear-gradient(45deg, $gray60, $gray150);
        }

        #card-right{
          #card-right-contents{
            #coin-header{
              #coin-symbol h1{
                color: $gray150;
              }

              #coin-rank .value h1{
                color: $gray60;
              }
            }
            
            #card-right-stripes{
              &:before, &:after{
                background-color: $gray60;
              }
            }
          }
        }
      }
    }
  }
}

@keyframes rotate {
  0%{
    transform: translateX(-50%) translateY(-50%) rotate(0deg);
  }
  100%{
    transform: translateX(-50%) translateY(-50%) rotate(360deg);
  }
}

@keyframes rotate-reverse {
  0%{
    transform: translateX(-50%) translateY(-50%) rotate(0deg);
  }
  100%{
    transform: translateX(-50%) translateY(-50%) rotate(-360deg);
  }
}

@keyframes bounce-tooltip{
  0%, 55%, 65%, 75%, 100%{
    margin-left: 5px;
  }
  
  60%, 70%{
    margin-left: 10px;
  }
}

@keyframes fade-in-up {
  from,
  60%,
  75%,
  90%,
  to {
    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
  }

  from {
    opacity: 0;
    transform: translate3d(-50%, -40%, 0);
  }

  to {
    transform: translate3d(-50%, -50%, 0);
  }
}
<script src="https://codepen.io/TheVVaFFle/pen/0299c8b36352c72cbfe8df48b47b54ca"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.17.1/axios.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
class App extends React.Component{
  constructor(props){
    super(props)
    this.getCoins = this.getCoins.bind(this)
    this.setIndex = this.setIndex.bind(this)
    this.state = {
      coins: [],
      index: 0,
      updating: false,
      isLoading: false,
      isShowingTooltip: true
    }
  }
  componentDidMount(){
    this.getCoins()
    particlesJS("particles", particlesConfig)
  }
  componentDidUpdate(prevProps, prevState){
    if(prevState.index !== this.state.index){
      if(this.state.isShowingTooltip){
        this.setState({isShowingTooltip: false})
      }       
      this.setState({updating: true})
      setTimeout(() => {
        this.setState({updating: false})
      }, 200)
    }
  }
  getCoins(){
    this.setState({isLoading: true})
    axios.get('https://api.coinmarketcap.com/v1/ticker/')
      .then(res => {
        const coins = this.mapCoins(res.data)
        this.setState({coins})
        this.setState({isLoading: false})
      })
      .catch(err => {
        console.error('Error loading data from Coin Market Cap')
        console.error(err)
      })
  }
  getCoinIcon(symbol){
    return `https://s3-us-west-2.amazonaws.com/s.cdpn.io/1468070/${symbol}.svg`
  }
  mapCoins(coins){
    return coins.map(coin => ({
      name: coin.name,
      symbol: coin.symbol,
      rank: coin.rank,
      price: formatNum(coin.price_usd),
      change24hr: coin.percent_change_24h,
      cap: formatNum(coin.market_cap_usd),
      volume: formatNum(coin['24h_volume_usd']),
      circulating: formatNum(coin.available_supply),
      img: this.getCoinIcon(coin.symbol.toLowerCase()),
      color: getCoinColor(coin.symbol)
    }))
  }
  setIndex(index){
    this.setState({index})
  }
  render(){
    const {
      coins, 
      index, 
      updating,
      isLoading, 
      isShowingTooltip
    } = this.state
    let card = null
    if(isLoading){
      card = (
        <div id="card-loading">
          <div id="card-loading-spinner"/>
        </div>
      )   
    }
    else if(coins.length > 0){
      card = (
        <Card 
          coins={coins} 
          index={index}
          setIndex={this.setIndex}
        />
      )  
    }
    
    return(
      <div id="app" className={updating ? 'updating' : ''}>
        <div id="particles"/>
        <div id="help-tooltip" className={isShowingTooltip ? 'showing' : 'hide'}>
          <i className="fa fa-question-circle-o"/>
          <h1><span className="text">Hover over the coin icon and scroll.</span><span className="triangle"/></h1>
        </div>
        {card}
      </div>
    )
  }
}

class Card extends React.Component{
  determineSign(num){
    return parseFloat(num) >= 0 ? 'positive' : 'negative'
  }
  render(){
    const {coins, index} = this.props,
          coin = coins[index],
          colorClass = getColorClass(coin.color)
    return(
      <div id="card-wrapper" className={colorClass}>
        <div id="card">
          <div id="card-left" className="card-half">
            <div id="coin-icon" style={{backgroundImage: `url(${coin.img})`}}/>
            <div id="coin-symbol-vert">
              <h1>{coin.symbol}</h1>
            </div>
            <CoinSelection 
              coins={coins} 
              index={index}
              setIndex={this.props.setIndex}
            />
          </div>
          <div id="card-right" className="card-half">
            <div id="card-right-contents">
              <div id="coin-header">
                <div id="coin-name">
                  <h1>{coin.name}</h1>
                </div>
                <div id="coin-symbol">
                  <h1>{coin.symbol}</h1>
                </div>
                <div id="coin-rank">
                  <div className="label">
                    <h1>Rank</h1>
                  </div>
                  <div className="value">
                    <h1>{coin.rank}</h1>
                  </div>
                </div>
              </div>
              <div id="coin-price">
                <div className="value">
                  <h1>${coin.price}</h1>
                </div>
                <div id="coin-change-24hr" className={this.determineSign(coin.change24hr)}>
                  <h1>{coin.change24hr}%</h1>
                </div>
              </div>
              <div id="coin-info">
                <CoinInfoField value={`$${coin.cap}`} label={"Market Cap"}/>
                <CoinInfoField value={`$${coin.volume}`} label={"Volume"}/>
                <CoinInfoField value={`${coin.circulating} ${coin.symbol}`} label={"Circulating Supply"}/>
              </div>
              <div id="card-right-stripes"/>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

const CoinInfoField = ({
  value,
  label
}) => {
  return(
    <div className="coin-info-field">
      <div className="value">
        <h1>{value}</h1>
      </div>
      <div className="label">
        <h1>{label}</h1>
      </div>
    </div>
  )
}

class CoinSelection extends React.Component {
  constructor(props){
    super(props)
    this.setCurrentScrollTop = this.setCurrentScrollTop.bind(this)
    this.moveScrollTop = this.moveScrollTop.bind(this)
    this.onOptionsScroll = this.onOptionsScroll.bind(this)
    this.state = {
      currentScrollTop: 0
    }
  }
  setCurrentScrollTop(val){
    this.setState({currentScrollTop: val})
  }
  moveScrollTop(){
    this.refs.coinOptions.scrollTop = this.state.currentScrollTop
  }
  onOptionsScroll(){
    const option = document.getElementsByClassName('coin-option')[0],
          topOffset = window.innerHeight / 2,
          optionHeight = option.clientHeight,
          scrollTop = this.refs.coinOptions.scrollTop,
          newScrollTop = this.props.index * (optionHeight + 20),
          index = Math.max(1, Math.ceil(scrollTop / optionHeight))
    this.setCurrentScrollTop(newScrollTop)
    this.props.setIndex(index - 1)
  }
  render(){
    const coinOptions = this.props.coins.slice(0,10).map(coin => {
      const selected = this.props.index == coin.rank - 1
      return(
        <div key={coin.symbol} className={`coin-option ${selected ? 'selected' : ''}`}>
          <div className={'coin-option-icon'} style={{backgroundImage: `url(${coin.img})`}}/>
        </div>
      )
    })
    return(
      <div id="coin-selection" onMouseLeave={this.moveScrollTop}>
        <div id="coin-options-wrapper" 
          ref="coinOptions"
          className="scroll-bar"
          onScroll={_.throttle(this.onOptionsScroll, 200)}
        >
          <div id="coin-options">
            {coinOptions}
          </div>
        </div>
      </div>
    )
  }
}

const formatNum = num => {
  const splitNum = num.split('.'),
        firstHalf = splitNum[0].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","),
        secondHalf = splitNum[1]
  return secondHalf ? `${firstHalf}.${secondHalf}` : firstHalf
}

const getCoinColor = symbol => {
  switch(symbol){
    case 'BTC':
      return ORANGE
    case 'ETH':
      return BLUE
    case 'XRP':
      return BLUE
    case 'BCH':
      return GREEN
    case 'ADA':
      return BLUE
    case 'XLM':
      return BLUE
    case 'LTC':
      return GRAY
    case 'NEO':
      return GREEN
    case 'EOS':
      return GRAY
    case 'XEM':
      return BLUE
    default:
      return GRAY
  }
}

const getColorClass = color => {
  switch(color){
    case ORANGE:
      return 'orange'
    case BLUE:
      return 'blue'
    case GREEN:
      return 'green'
    case GRAY:
      return 'gray'
  }
}

const ORANGE = 'ORANGE',
      BLUE = 'BLUE',
      GREEN = 'GREEN',
      GRAY = 'GRAY'

const particlesConfig = {
  "particles": {
    "number": {
      "value": 30
    },
    "color": {
      "value": "#607d8b"
    },
    "size": {
      "value": 2
    },
    "line_linked": {
      "enable": true,
      "distance": 350,
      "color": "#607d8b"
    }
  },
  "interactivity": {
    "events": {
      "onhover": {
        "enable": true,
        "mode": "grab"
      },
      "onclick": {
        "enable": false
      }
    },
    "modes": {
      "grab": {
        "distance": 500,
        "line_linked": {
          "opacity": 1
        }
      }
    }
  }
}

ReactDOM.render(<App/>, document.getElementById('root'))