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'))
Utilizes the CoinMarketCap api: https://coinmarketcap.com/api/ Card style inspiration: https://www.uplabs.com/posts/levitating-product-card Particles effect: https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js
A Pen by HARUN PEHLİVAN on CodePen.