@ -1,38 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { FaCircle } from 'react-icons/lib/fa' |
|||
import { btc, blockExplorer } from 'utils' |
|||
import styles from './Contact.scss' |
|||
|
|||
const ClosingContact = ({ channel }) => ( |
|||
<li className={styles.friend}> |
|||
<section className={styles.info}> |
|||
<p className={styles.closing}> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> |
|||
<span> |
|||
Removing |
|||
<i onClick={() => blockExplorer.showChannelClosing(channel)}> |
|||
(Details) |
|||
</i> |
|||
</span> |
|||
</p> |
|||
<h2>{channel.channel.remote_node_pub}</h2> |
|||
</section> |
|||
<section className={styles.limits}> |
|||
<div> |
|||
<h4>Can Pay</h4> |
|||
<p>{btc.satoshisToBtc(channel.channel.local_balance)}BTC</p> |
|||
</div> |
|||
<div> |
|||
<h4>Can Receive</h4> |
|||
<p>{btc.satoshisToBtc(channel.channel.remote_balance)}BTC</p> |
|||
</div> |
|||
</section> |
|||
</li> |
|||
) |
|||
|
|||
ClosingContact.propTypes = { |
|||
channel: PropTypes.object.isRequired |
|||
} |
|||
|
|||
export default ClosingContact |
@ -1,156 +0,0 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.friend { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
padding: 30px 60px 60px 60px; |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
|
|||
&.loading { |
|||
.info { |
|||
opacity: 0.2; |
|||
} |
|||
} |
|||
|
|||
&:hover { |
|||
background: $lightgrey; |
|||
} |
|||
|
|||
.limits { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
|
|||
div { |
|||
margin: 0 10px; |
|||
|
|||
h4 { |
|||
font-size: 12px; |
|||
margin-bottom: 20px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.info { |
|||
p { |
|||
margin-bottom: 20px; |
|||
|
|||
&.online { |
|||
color: $green; |
|||
|
|||
svg { |
|||
color: $green; |
|||
} |
|||
} |
|||
|
|||
&.pending { |
|||
color: $orange; |
|||
|
|||
svg { |
|||
color: $orange; |
|||
} |
|||
|
|||
i { |
|||
margin-left: 5px; |
|||
color: $darkestgrey; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&.closing { |
|||
color: $red; |
|||
|
|||
svg { |
|||
color: $red; |
|||
} |
|||
|
|||
i { |
|||
margin-left: 5px; |
|||
color: $darkestgrey; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
} |
|||
|
|||
svg, span { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
svg { |
|||
margin-right: 5px; |
|||
width: 12px; |
|||
height: 12px; |
|||
color: $darkestgrey; |
|||
} |
|||
|
|||
span { |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
|
|||
h2 { |
|||
color: $black; |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
letter-spacing: 1.3px; |
|||
|
|||
span { |
|||
color: $darkestgrey; |
|||
margin-left: 5px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@-webkit-keyframes animation-rotate { |
|||
100% { |
|||
-webkit-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@-moz-keyframes animation-rotate { |
|||
100% { |
|||
-moz-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@-o-keyframes animation-rotate { |
|||
100% { |
|||
-o-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@keyframes animation-rotate { |
|||
100% { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
.spinner { |
|||
border: 1px solid rgba(0, 0, 0, 0.1); |
|||
border-left-color: rgba(0, 0, 0, 0.4); |
|||
-webkit-border-radius: 999px; |
|||
-moz-border-radius: 999px; |
|||
border-radius: 999px; |
|||
} |
|||
|
|||
.spinner { |
|||
margin: 0 auto; |
|||
height: 50px; |
|||
width: 50px; |
|||
-webkit-animation: animation-rotate 1000ms linear infinite; |
|||
-moz-animation: animation-rotate 1000ms linear infinite; |
|||
-o-animation: animation-rotate 1000ms linear infinite; |
|||
animation: animation-rotate 1000ms linear infinite; |
|||
} |
|||
|
@ -1,35 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { FaCircle } from 'react-icons/lib/fa' |
|||
import styles from './Contact.scss' |
|||
|
|||
const LoadingContact = ({ pubkey, isClosing }) => ( |
|||
<li className={`${styles.friend} ${styles.loading}`}> |
|||
<section className={styles.info}> |
|||
<p> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> |
|||
<span> |
|||
{ |
|||
isClosing ? |
|||
'Closing' |
|||
: |
|||
'Loading' |
|||
} |
|||
</span> |
|||
</p> |
|||
<h2>{pubkey}</h2> |
|||
</section> |
|||
<section className={styles.limits}> |
|||
<div className={styles.loading}> |
|||
<div className={styles.spinner} /> |
|||
</div> |
|||
</section> |
|||
</li> |
|||
) |
|||
|
|||
LoadingContact.propTypes = { |
|||
pubkey: PropTypes.string.isRequired, |
|||
isClosing: PropTypes.bool.isRequired |
|||
} |
|||
|
|||
export default LoadingContact |
@ -0,0 +1,188 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import find from 'lodash/find' |
|||
import Isvg from 'react-inlinesvg' |
|||
import { FaAngleDown, FaCircle, FaRepeat } from 'react-icons/lib/fa' |
|||
import { btc } from 'utils' |
|||
import plus from 'icons/plus.svg' |
|||
import search from 'icons/search.svg' |
|||
import styles from './Network.scss' |
|||
|
|||
class Network extends Component { |
|||
constructor(props) { |
|||
super(props) |
|||
|
|||
this.state = { |
|||
refreshing: false |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
channels: { |
|||
searchQuery, |
|||
filterPulldown, |
|||
filter |
|||
// loadingChannelPubkeys,
|
|||
// closingChannelIds
|
|||
}, |
|||
currentChannels, |
|||
balance, |
|||
currentTicker, |
|||
|
|||
nodes, |
|||
|
|||
fetchChannels, |
|||
openContactsForm, |
|||
|
|||
nonActiveFilters, |
|||
toggleFilterPulldown, |
|||
changeFilter, |
|||
|
|||
updateChannelSearchQuery, |
|||
|
|||
openContactModal |
|||
} = this.props |
|||
|
|||
|
|||
const refreshClicked = () => { |
|||
// turn the spinner on
|
|||
this.setState({ refreshing: true }) |
|||
|
|||
// store event in icon so we dont get an error when react clears it
|
|||
const icon = this.repeat.childNodes |
|||
|
|||
// fetch channels
|
|||
fetchChannels() |
|||
|
|||
// wait for the svg to appear as child
|
|||
const svgTimeout = setTimeout(() => { |
|||
if (icon[0].tagName === 'svg') { |
|||
// spin icon for 1 sec
|
|||
icon[0].style.animation = 'spin 1000ms linear 1' |
|||
clearTimeout(svgTimeout) |
|||
} |
|||
}, 1) |
|||
|
|||
// clear animation after the second so we can reuse it
|
|||
const refreshTimeout = setTimeout(() => { |
|||
icon[0].style.animation = '' |
|||
this.setState({ refreshing: false }) |
|||
clearTimeout(refreshTimeout) |
|||
}, 1000) |
|||
} |
|||
|
|||
const displayNodeName = (channel) => { |
|||
const node = find(nodes, n => channel.remote_pubkey === n.pub_key) |
|||
|
|||
if (node && node.alias.length) { return node.alias } |
|||
|
|||
return channel.remote_pubkey ? channel.remote_pubkey.substring(0, 10) : channel.remote_node_pub.substring(0, 10) |
|||
} |
|||
|
|||
const channelStatus = (channel) => { |
|||
if (Object.prototype.hasOwnProperty.call(channel, 'confirmation_height')) { return 'pending' } |
|||
if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { return 'closing' } |
|||
if (!channel.active) { return 'offline' } |
|||
|
|||
return 'online' |
|||
} |
|||
|
|||
const usdAmount = btc.satoshisToUsd(balance.channelBalance, currentTicker.price_usd) |
|||
|
|||
return ( |
|||
<div className={styles.network}> |
|||
<header className={styles.header}> |
|||
<section> |
|||
<h2>My Network</h2> |
|||
<span className={styles.channelAmount}> |
|||
{btc.satoshisToBtc(balance.channelBalance)}BTC ≈ ${usdAmount ? usdAmount.toLocaleString() : ''} |
|||
</span> |
|||
</section> |
|||
<section className={`${styles.addChannel} hint--bottom-left`} onClick={openContactsForm} data-hint='Open a channel'> |
|||
<Isvg src={plus} /> |
|||
</section> |
|||
</header> |
|||
|
|||
<div className={styles.channels}> |
|||
<header className={styles.listHeader}> |
|||
<section> |
|||
<h2 onClick={toggleFilterPulldown} className={styles.filterTitle}> |
|||
{filter.name} <span className={filterPulldown && styles.pulldown}><FaAngleDown /></span> |
|||
</h2> |
|||
<ul className={`${styles.filters} ${filterPulldown && styles.active}`}> |
|||
{ |
|||
nonActiveFilters.map(f => ( |
|||
<li key={f.key} onClick={() => changeFilter(f)}> |
|||
{f.name} |
|||
</li> |
|||
)) |
|||
} |
|||
</ul> |
|||
</section> |
|||
|
|||
<section className={styles.refreshContainer}> |
|||
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}> |
|||
{ |
|||
this.state.refreshing ? |
|||
<FaRepeat /> |
|||
: |
|||
'Refresh' |
|||
} |
|||
</span> |
|||
</section> |
|||
</header> |
|||
|
|||
<ul className={filterPulldown && styles.fade}> |
|||
{ |
|||
currentChannels.length > 0 && currentChannels.map((channelObj, index) => { |
|||
const channel = Object.prototype.hasOwnProperty.call(channelObj, 'channel') ? channelObj.channel : channelObj |
|||
return ( |
|||
<li key={index} className={styles.channel} onClick={() => openContactModal(channelObj)}> |
|||
<span>{displayNodeName(channel)}</span> |
|||
<span className={`${styles[channelStatus(channelObj)]} hint--left`} data-hint={channelStatus(channelObj)}> |
|||
<FaCircle /> |
|||
</span> |
|||
</li> |
|||
) |
|||
}) |
|||
} |
|||
</ul> |
|||
</div> |
|||
|
|||
<footer className={styles.search}> |
|||
<label htmlFor='search' className={`${styles.label} ${styles.input}`}> |
|||
<Isvg src={search} /> |
|||
</label> |
|||
<input |
|||
id='search' |
|||
type='text' |
|||
className={`${styles.text} ${styles.input}`} |
|||
placeholder='search by alias or pubkey' |
|||
value={searchQuery} |
|||
onChange={event => updateChannelSearchQuery(event.target.value)} |
|||
/> |
|||
</footer> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
Network.propTypes = { |
|||
currentChannels: PropTypes.array.isRequired, |
|||
nodes: PropTypes.array.isRequired, |
|||
nonActiveFilters: PropTypes.array.isRequired, |
|||
|
|||
channels: PropTypes.object.isRequired, |
|||
balance: PropTypes.object.isRequired, |
|||
currentTicker: PropTypes.object.isRequired, |
|||
|
|||
fetchChannels: PropTypes.func.isRequired, |
|||
openContactsForm: PropTypes.func.isRequired, |
|||
toggleFilterPulldown: PropTypes.func.isRequired, |
|||
changeFilter: PropTypes.func.isRequired, |
|||
updateChannelSearchQuery: PropTypes.func.isRequired, |
|||
openContactModal: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default Network |
@ -0,0 +1,188 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.network { |
|||
position: relative; |
|||
width: 20%; |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
height: 100vh; |
|||
background: #31343f; |
|||
} |
|||
|
|||
.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
background: #2D303B; |
|||
padding: 10px 20px; |
|||
color: $white; |
|||
|
|||
h2 { |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
letter-spacing: 1.2px; |
|||
margin-bottom: 5px; |
|||
} |
|||
|
|||
.channelAmount { |
|||
font-size: 10px; |
|||
opacity: 0.5; |
|||
} |
|||
|
|||
.addChannel { |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
color: $darkestgrey; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.channels { |
|||
padding: 20px; |
|||
height: 100%; |
|||
overflow-y: scroll; |
|||
|
|||
.listHeader { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: baseline; |
|||
|
|||
h2, h2 span { |
|||
color: $white; |
|||
cursor: pointer; |
|||
transition: color 0.25s; |
|||
|
|||
&:hover { |
|||
color: $darkestgrey; |
|||
} |
|||
} |
|||
|
|||
h2, .filters li { |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.filters { |
|||
display: none; |
|||
|
|||
&.active { |
|||
display: block; |
|||
position: absolute; |
|||
bottom: -100px; |
|||
z-index: 10; |
|||
|
|||
li { |
|||
margin: 10px 0; |
|||
cursor: pointer; |
|||
color: $white; |
|||
|
|||
&:hover { |
|||
color: $darkestgrey; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
span { |
|||
color: $white; |
|||
opacity: 1; |
|||
font-size: 10px; |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
} |
|||
|
|||
ul { |
|||
margin-top: 20px; |
|||
height: 100%; |
|||
overflow-y: scroll; |
|||
} |
|||
|
|||
.fade { |
|||
opacity: 0.1; |
|||
} |
|||
|
|||
.channel { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
color: $white; |
|||
padding: 10px 0; |
|||
margin: 10px 0; |
|||
cursor: pointer; |
|||
|
|||
span:nth-child(1) { |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.online { |
|||
color: $green; |
|||
} |
|||
|
|||
.pending { |
|||
color: $orange; |
|||
} |
|||
|
|||
.offline { |
|||
color: $darkestgrey; |
|||
} |
|||
|
|||
.closing { |
|||
color: $red; |
|||
} |
|||
|
|||
svg { |
|||
width: 5px; |
|||
height: 5px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.search { |
|||
position: absolute; |
|||
bottom: 20px; |
|||
width: calc(100% - 40px); |
|||
padding: 10px 20px; |
|||
border-top: 1px solid $darkestgrey; |
|||
background: #2D303B; |
|||
|
|||
.input { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
height: 100%; |
|||
} |
|||
|
|||
.label { |
|||
width: 5%; |
|||
line-height: 50px; |
|||
font-size: 25px; |
|||
text-align: center; |
|||
cursor: pointer; |
|||
color: $white; |
|||
opacity: 0.5; |
|||
|
|||
svg { |
|||
width: 14px; |
|||
height:14px; |
|||
} |
|||
} |
|||
|
|||
.text { |
|||
width: calc(95% - 20px); |
|||
background: transparent; |
|||
outline: 0; |
|||
padding: 0 10px; |
|||
border: 0; |
|||
border-radius: 0; |
|||
height: 50px; |
|||
font-size: 12px; |
|||
color: $white; |
|||
} |
|||
} |
@ -1,34 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { FaCircle } from 'react-icons/lib/fa' |
|||
import { btc } from 'utils' |
|||
import styles from './Contact.scss' |
|||
|
|||
const OfflineContact = ({ channel, openContactModal }) => ( |
|||
<li className={styles.friend} key={channel.chan_id} onClick={() => openContactModal(channel)}> |
|||
<section className={styles.info}> |
|||
<p> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> |
|||
<span>Offline</span> |
|||
</p> |
|||
<h2>{channel.remote_pubkey}</h2> |
|||
</section> |
|||
<section className={styles.limits}> |
|||
<div> |
|||
<h4>Can Pay</h4> |
|||
<p>{btc.satoshisToBtc(channel.local_balance)}BTC</p> |
|||
</div> |
|||
<div> |
|||
<h4>Can Receive</h4> |
|||
<p>{btc.satoshisToBtc(channel.remote_balance)}BTC</p> |
|||
</div> |
|||
</section> |
|||
</li> |
|||
) |
|||
|
|||
OfflineContact.propTypes = { |
|||
channel: PropTypes.object.isRequired, |
|||
openContactModal: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default OfflineContact |
@ -1,34 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { FaCircle } from 'react-icons/lib/fa' |
|||
import { btc } from 'utils' |
|||
import styles from './Contact.scss' |
|||
|
|||
const OnlineContact = ({ channel, openContactModal }) => ( |
|||
<li className={styles.friend} key={channel.chan_id} onClick={() => openContactModal(channel)}> |
|||
<section className={styles.info}> |
|||
<p className={styles.online}> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> |
|||
<span>Online</span> |
|||
</p> |
|||
<h2>{channel.remote_pubkey}</h2> |
|||
</section> |
|||
<section className={styles.limits}> |
|||
<div> |
|||
<h4>Can Pay</h4> |
|||
<p>{btc.satoshisToBtc(channel.local_balance)}BTC</p> |
|||
</div> |
|||
<div> |
|||
<h4>Can Receive</h4> |
|||
<p>{btc.satoshisToBtc(channel.remote_balance)}BTC</p> |
|||
</div> |
|||
</section> |
|||
</li> |
|||
) |
|||
|
|||
OnlineContact.propTypes = { |
|||
channel: PropTypes.object.isRequired, |
|||
openContactModal: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default OnlineContact |
@ -1,38 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { FaCircle } from 'react-icons/lib/fa' |
|||
import { btc, blockExplorer } from 'utils' |
|||
import styles from './Contact.scss' |
|||
|
|||
const PendingContact = ({ channel }) => ( |
|||
<li className={styles.friend} key={channel.chan_id}> |
|||
<section className={styles.info}> |
|||
<p className={styles.pending}> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> |
|||
<span> |
|||
Pending |
|||
<i onClick={() => blockExplorer.showChannelPoint(channel)}> |
|||
(Details) |
|||
</i> |
|||
</span> |
|||
</p> |
|||
<h2>{channel.channel.remote_node_pub}</h2> |
|||
</section> |
|||
<section className={styles.limits}> |
|||
<div> |
|||
<h4>Can Pay</h4> |
|||
<p>{btc.satoshisToBtc(channel.channel.local_balance)}BTC</p> |
|||
</div> |
|||
<div> |
|||
<h4>Can Receive</h4> |
|||
<p>{btc.satoshisToBtc(channel.channel.remote_balance)}BTC</p> |
|||
</div> |
|||
</section> |
|||
</li> |
|||
) |
|||
|
|||
PendingContact.propTypes = { |
|||
channel: PropTypes.object.isRequired |
|||
} |
|||
|
|||
export default PendingContact |
@ -1,101 +0,0 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { FaCopy } from 'react-icons/lib/fa' |
|||
import styles from './LndSyncing.scss' |
|||
|
|||
|
|||
class LndSyncing extends Component { |
|||
constructor(props) { |
|||
super(props) |
|||
this.state = { |
|||
facts: [ |
|||
{ |
|||
title: 'The Lightning Network', |
|||
description: 'The Lightning Network is a second layer solution built on top of the Bitcoin block chain that attempts to increase Bitcoin\'s scalability and privacy' // eslint-disable-line max-len
|
|||
}, |
|||
{ |
|||
title: 'Payment Channel', |
|||
description: 'A payment channel is a class of techniques designed to allow users to make multiple Bitcoin transactions without commiting all of the transactions to the Bitcoin block chain. You can think of payment channels like tubes of money' // eslint-disable-line max-len
|
|||
}, |
|||
{ |
|||
title: 'HTLC', |
|||
description: 'Hashed TimeLock Contracts is a class of payments that use hashlocks and timelocks to require the receiver of a payment either acknowledge receiving the payment before a deadline or forfeit the ability to claim the payment. HTLCs are useful within the Lightning Network for routing payments across two or more payment channels' // eslint-disable-line max-len
|
|||
}, |
|||
{ |
|||
title: 'Onion Routing', |
|||
description: 'Onion routing is a technique for anonymous communication over a computer network. In an onion network, messages are encapsulated in layers of encryption, analogous to layers of an onion.' // eslint-disable-line max-len
|
|||
} |
|||
], |
|||
currentFact: 0 |
|||
} |
|||
} |
|||
|
|||
componentWillMount() { |
|||
const { fetchBlockHeight, newAddress } = this.props |
|||
|
|||
fetchBlockHeight() |
|||
newAddress('np2wkh') |
|||
} |
|||
|
|||
render() { |
|||
const { syncPercentage, address: { address } } = this.props |
|||
const { facts, currentFact } = this.state |
|||
const renderCurrentFact = facts[currentFact] |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
<header> |
|||
<section> |
|||
<h3>zap</h3> |
|||
</section> |
|||
<section className={styles.loading}> |
|||
<h4>{syncPercentage}%</h4> |
|||
<div className={styles.spinner} /> |
|||
</section> |
|||
</header> |
|||
|
|||
<div className={styles.facts}> |
|||
<div className={styles.fact}> |
|||
<h2>{renderCurrentFact.title}</h2> |
|||
<p>{renderCurrentFact.description}</p> |
|||
</div> |
|||
<ul className={styles.factButtons}> |
|||
{ |
|||
facts.map((fact, index) => ( |
|||
<li |
|||
className={`${styles.factButton} ${currentFact === index && styles.active}`} |
|||
key={index} |
|||
onClick={() => this.setState({ currentFact: index })} |
|||
/> |
|||
)) |
|||
} |
|||
</ul> |
|||
</div> |
|||
|
|||
<div className={styles.footer}> |
|||
<section> |
|||
<h2>Fund your Zap wallet</h2> |
|||
<p>Deposit to your wallet while your node is syncing so autopilot can start working magic for you</p> |
|||
</section> |
|||
<section> |
|||
<div className={styles.address}> |
|||
<span>{address}</span> |
|||
<span className='hint--left' data-hint='Copy Address'> |
|||
<FaCopy /> |
|||
</span> |
|||
</div> |
|||
</section> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
LndSyncing.propTypes = { |
|||
newAddress: PropTypes.func.isRequired, |
|||
fetchBlockHeight: PropTypes.func.isRequired, |
|||
syncPercentage: PropTypes.number.isRequired, |
|||
address: PropTypes.object.isRequired |
|||
} |
|||
|
|||
export default LndSyncing |
@ -1,242 +0,0 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.container { |
|||
height: 100vh; |
|||
background: $secondary; |
|||
|
|||
header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: top; |
|||
padding: 100px; |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 50px; |
|||
color: $white; |
|||
} |
|||
|
|||
.loading { |
|||
text-align: center; |
|||
position: relative; |
|||
|
|||
.spinner, h1 { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
h4 { |
|||
position: absolute; |
|||
min-width: 100px; |
|||
top: calc(50% - 5px); |
|||
left: calc(50% - 48px); |
|||
color: $main; |
|||
font-size: 10px; |
|||
} |
|||
|
|||
h1 { |
|||
color: $main; |
|||
line-height: 50px; |
|||
margin-left: 20px; |
|||
} |
|||
} |
|||
|
|||
.facts { |
|||
color: $white; |
|||
|
|||
.fact { |
|||
transition: all 0.25s; |
|||
width: 50%; |
|||
margin: 0 auto; |
|||
text-align: center; |
|||
line-height: 1.5; |
|||
letter-spacing: 1.1px; |
|||
height: 250px; |
|||
|
|||
h2 { |
|||
font-size: 50px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
p { |
|||
margin-bottom: 20px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.factButtons { |
|||
text-align: center; |
|||
} |
|||
|
|||
.factButton { |
|||
cursor: pointer; |
|||
display: inline-block; |
|||
width: 15px; |
|||
height: 15px; |
|||
background: $white; |
|||
opacity: 0.5; |
|||
border-radius: 50%; |
|||
margin: 0 5px; |
|||
|
|||
&:hover { |
|||
opacity: 0.75; |
|||
} |
|||
|
|||
&.active { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.aliasForm { |
|||
width: 50%; |
|||
margin: 0 auto; |
|||
|
|||
h1 { |
|||
text-align: center; |
|||
font-size: 32px; |
|||
color: $white; |
|||
} |
|||
|
|||
p { |
|||
color: $darkgrey; |
|||
text-align: center; |
|||
margin-top: 20px; |
|||
font-weight: 100; |
|||
} |
|||
|
|||
.inputContainer { |
|||
text-align: center; |
|||
margin-top: 50px; |
|||
} |
|||
|
|||
.input { |
|||
padding: 20px; |
|||
font-size: 18px; |
|||
color: $darkestgrey; |
|||
background: lighten($black, 15%); |
|||
border: none; |
|||
outline: 0; |
|||
-webkit-appearance: none; |
|||
font-weight: 200; |
|||
width: calc(100% - 20px); |
|||
} |
|||
|
|||
.submit { |
|||
background: $main; |
|||
color: $white; |
|||
font-size: 18px; |
|||
cursor: pointer; |
|||
width: 10%; |
|||
margin: 50px auto 0 auto; |
|||
padding: 20px 60px; |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
|
|||
.footer { |
|||
position: absolute; |
|||
bottom: 0; |
|||
width: 100%; |
|||
background: darken($secondary, 5%); |
|||
white-space: nowrap; |
|||
|
|||
section { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
width: 50%; |
|||
white-space: normal; |
|||
padding: 30px 0; |
|||
|
|||
h2 { |
|||
color: $white; |
|||
font-size: 22px; |
|||
letter-spacing: 1.2px; |
|||
font-weight: bold; |
|||
margin-bottom: 20px; |
|||
padding: 0 30px; |
|||
} |
|||
|
|||
p { |
|||
color: $white; |
|||
padding: 0 30px; |
|||
line-height: 1.5; |
|||
} |
|||
} |
|||
|
|||
.address { |
|||
display: flex; |
|||
flex-direction: row; |
|||
font-family: 'Roboto'; |
|||
font-size: 14px; |
|||
font-weight: 200; |
|||
background: lighten($black, 15%); |
|||
color: $darkestgrey; |
|||
width: 75%; |
|||
margin: 0 auto; |
|||
|
|||
span { |
|||
padding: 20px; |
|||
} |
|||
|
|||
span:nth-child(1) { |
|||
flex: 9; |
|||
overflow-x: scroll; |
|||
font-size: 18px; |
|||
} |
|||
|
|||
span:nth-child(2) { |
|||
background: $darkestgrey; |
|||
color: $white; |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
background: $darkestgrey; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.spinner { |
|||
border: 1px solid rgba(235, 184, 100, 0.1); |
|||
border-left-color: rgba(235, 184, 100, 0.4); |
|||
-webkit-border-radius: 999px; |
|||
-moz-border-radius: 999px; |
|||
border-radius: 999px; |
|||
} |
|||
|
|||
.spinner { |
|||
margin: 0 auto; |
|||
height: 50px; |
|||
width: 50px; |
|||
-webkit-animation: animation-rotate 1000ms linear infinite; |
|||
-moz-animation: animation-rotate 1000ms linear infinite; |
|||
-o-animation: animation-rotate 1000ms linear infinite; |
|||
animation: animation-rotate 1000ms linear infinite; |
|||
} |
|||
|
|||
@-webkit-keyframes animation-rotate { |
|||
100% { |
|||
-webkit-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@-moz-keyframes animation-rotate { |
|||
100% { |
|||
-moz-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@-o-keyframes animation-rotate { |
|||
100% { |
|||
-o-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@keyframes animation-rotate { |
|||
100% { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
@ -1,3 +0,0 @@ |
|||
import LndSyncing from './LndSyncing' |
|||
|
|||
export default LndSyncing |
@ -1,54 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { NavLink } from 'react-router-dom' |
|||
import Isvg from 'react-inlinesvg' |
|||
|
|||
import walletIcon from 'icons/wallet.svg' |
|||
import peersIcon from 'icons/peers.svg' |
|||
import networkIcon from 'icons/globe.svg' |
|||
|
|||
import styles from './Nav.scss' |
|||
|
|||
const Nav = ({ openPayForm, openRequestForm }) => ( |
|||
<nav className={styles.nav}> |
|||
<header className={styles.header}> |
|||
<h1>zap</h1> |
|||
<span>beta</span> |
|||
</header> |
|||
<ul className={styles.links}> |
|||
<NavLink exact to='/' activeClassName={styles.active} className={styles.link}> |
|||
<li> |
|||
<Isvg styles={{ verticalAlign: 'middle' }} src={walletIcon} /> |
|||
<span>Wallet</span> |
|||
</li> |
|||
</NavLink> |
|||
<NavLink exact to='/contacts' activeClassName={styles.active} className={styles.link}> |
|||
<li> |
|||
<Isvg styles={{ verticalAlign: 'middle' }} src={peersIcon} /> |
|||
<span>Contacts</span> |
|||
</li> |
|||
</NavLink> |
|||
<NavLink exact to='/network' activeClassName={styles.active} className={styles.link}> |
|||
<li> |
|||
<Isvg styles={{ verticalAlign: 'middle' }} src={networkIcon} /> |
|||
<span>Network</span> |
|||
</li> |
|||
</NavLink> |
|||
</ul> |
|||
<div className={styles.buttons}> |
|||
<div className={`buttonPrimary ${styles.button}`} onClick={openPayForm}> |
|||
<span>Pay</span> |
|||
</div> |
|||
<div className={`buttonPrimary ${styles.button}`} onClick={openRequestForm}> |
|||
<span>Request</span> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
) |
|||
|
|||
Nav.propTypes = { |
|||
openPayForm: PropTypes.func.isRequired, |
|||
openRequestForm: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default Nav |
@ -1,187 +0,0 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.nav { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
width: 20%; |
|||
font-size: 24px; |
|||
background: $secondary; |
|||
height: 100vh; |
|||
position: relative; |
|||
min-width: 15%; |
|||
color: $white; |
|||
} |
|||
|
|||
.header { |
|||
padding: 20px; |
|||
|
|||
h1 { |
|||
color: $main; |
|||
font-size: 30px; |
|||
font-weight: 300; |
|||
text-align: center; |
|||
float: left; |
|||
-webkit-font-smoothing: antialiased; |
|||
margin-right: 5px; |
|||
} |
|||
|
|||
span { |
|||
font-family: "Open Sans", "Helvetica Neue", Helvetica; |
|||
color: #bbb; |
|||
font-size: 11px; |
|||
letter-spacing: 2px; |
|||
text-transform: uppercase; |
|||
text-align: right; |
|||
line-height: 100%; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
} |
|||
|
|||
.info { |
|||
padding: 25px 10px 10px 10px; |
|||
|
|||
.link { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
list-style-type: none; |
|||
width: 50%; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
|
|||
.currency { |
|||
margin: 0 1px; |
|||
|
|||
&.active { |
|||
color: $main; |
|||
} |
|||
|
|||
span { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
|
|||
svg[data-icon='ltc'] { |
|||
width: 24px; |
|||
height: 28px; |
|||
|
|||
g { |
|||
transform: scale(1.75) translate(-5px, -5px); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.logo { |
|||
text-align: center; |
|||
margin-top: 20px; |
|||
margin-bottom: 35%; |
|||
|
|||
svg { |
|||
width: 100px; |
|||
height: 100px; |
|||
} |
|||
} |
|||
|
|||
.balance { |
|||
text-align: right; |
|||
color: $main; |
|||
|
|||
p { |
|||
margin: 2px 0; |
|||
|
|||
&:first-child { |
|||
font-size: 14px; |
|||
} |
|||
|
|||
&:nth-child(2) { |
|||
font-size: 12px; |
|||
} |
|||
|
|||
span { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
|
|||
svg[data-icon='ltc'] { |
|||
width: 10px; |
|||
height: 10px; |
|||
|
|||
g { |
|||
transform: scale(1.75) translate(-5px, -5px); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.links { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
margin-top: 50%; |
|||
|
|||
.link { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: 100%; |
|||
color: $darkestgrey; |
|||
opacity: 0.5; |
|||
cursor: pointer; |
|||
text-decoration: none; |
|||
border-left: 20px solid transparent; |
|||
transition: all 0.25s; |
|||
|
|||
li { |
|||
margin: 12.5px 0; |
|||
min-width: 200px; |
|||
} |
|||
|
|||
&.active { |
|||
color: $main; |
|||
opacity: 1.0; |
|||
|
|||
svg g { |
|||
stroke: $main; |
|||
} |
|||
} |
|||
|
|||
svg { |
|||
width: 32px; |
|||
height: 32px; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
span { |
|||
margin-left: 15px; |
|||
line-height: 22px; |
|||
font-size: 20px; |
|||
font-weight: 500; |
|||
letter-spacing: .2px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.buttons { |
|||
width: 75%; |
|||
font-size: 18px; |
|||
position: absolute; |
|||
bottom: 10px; |
|||
right: 12.5%; |
|||
} |
|||
|
|||
.button { |
|||
margin-bottom: 20px; |
|||
font-weight: bold; |
|||
cursor: pointer; |
|||
text-transform: uppercase; |
|||
letter-spacing: .2px; |
|||
} |
|||
|
|||
.content { |
|||
width: 80%; |
|||
} |
@ -1,3 +0,0 @@ |
|||
import Nav from './Nav' |
|||
|
|||
export default Nav |
@ -1,274 +0,0 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import * as d3Force from 'd3-force' |
|||
import * as d3Selection from 'd3-selection' |
|||
import * as d3Zoom from 'd3-zoom' |
|||
|
|||
import styles from './CanvasNetworkGraph.scss' |
|||
|
|||
const d3 = Object.assign({}, d3Force, d3Selection, d3Zoom) |
|||
|
|||
function generateSimulationData(nodes, edges) { |
|||
const resNodes = nodes.map(node => Object.assign(node, { id: node.pub_key })) |
|||
const resEdges = edges.map(node => Object.assign(node, { source: node.node1_pub, target: node.node2_pub })) |
|||
|
|||
return { |
|||
nodes: resNodes, |
|||
links: resEdges |
|||
} |
|||
} |
|||
|
|||
class CanvasNetworkGraph extends Component { |
|||
constructor(props) { |
|||
super(props) |
|||
|
|||
this.state = { |
|||
simulationData: { |
|||
nodes: [], |
|||
links: [] |
|||
}, |
|||
|
|||
svgLoaded: false |
|||
} |
|||
|
|||
this.startSimulation = this.startSimulation.bind(this) |
|||
this.zoomActions = this.zoomActions.bind(this) |
|||
this.ticked = this.ticked.bind(this) |
|||
this.restart = this.restart.bind(this) |
|||
} |
|||
|
|||
componentDidMount() { |
|||
// wait for the svg to be in the DOM before we start the simulation
|
|||
const svgInterval = setInterval(() => { |
|||
if (document.getElementById('mapContainer')) { |
|||
d3.select('#mapContainer') |
|||
.append('svg') |
|||
.attr('id', 'map') |
|||
.attr('width', '100%') |
|||
.attr('height', '100%') |
|||
|
|||
this.startSimulation() |
|||
|
|||
clearInterval(svgInterval) |
|||
} |
|||
}, 1000) |
|||
} |
|||
|
|||
componentWillReceiveProps(nextProps) { |
|||
const { network } = nextProps |
|||
const { simulationData: { nodes, links } } = this.state |
|||
|
|||
const simulationDataEmpty = !nodes.length && !links.length |
|||
const networkDataLoaded = network.nodes.length || network.edges.length |
|||
const prevNetwork = this.props.network |
|||
|
|||
if ( |
|||
// update the simulationData only if
|
|||
// the simulationData is empty and we have network data
|
|||
(simulationDataEmpty && networkDataLoaded) || |
|||
// the nodes or edges have changed
|
|||
(prevNetwork.nodes.length !== network.nodes.length || prevNetwork.edges.length !== network.edges.length)) { |
|||
this.setState({ |
|||
simulationData: generateSimulationData(network.nodes, network.edges) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
componentDidUpdate(prevProps) { |
|||
const { |
|||
selectedPeerPubkeys, |
|||
selectedChannelIds, |
|||
currentRouteChanIds |
|||
} = this.props |
|||
|
|||
if (prevProps.selectedPeerPubkeys.length !== selectedPeerPubkeys.length) { |
|||
this.updateSelectedPeers() |
|||
} |
|||
|
|||
if (prevProps.selectedChannelIds.length !== selectedChannelIds.length) { |
|||
this.updateSelectedChannels() |
|||
} |
|||
|
|||
if (prevProps.currentRouteChanIds.length !== currentRouteChanIds.length) { |
|||
this.renderSelectedRoute() |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
d3.select('#map') |
|||
.remove() |
|||
} |
|||
|
|||
updateSelectedPeers() { |
|||
const { selectedPeerPubkeys } = this.props |
|||
|
|||
// remove active class
|
|||
d3.selectAll('.active-peer').classed('active-peer', false) |
|||
|
|||
// add active class to all selected peers
|
|||
selectedPeerPubkeys.forEach((pubkey) => { |
|||
d3.select(`#node-${pubkey}`).classed('active-peer', true) |
|||
}) |
|||
} |
|||
|
|||
updateSelectedChannels() { |
|||
const { selectedChannelIds } = this.props |
|||
|
|||
// remove active class
|
|||
d3.selectAll('.active-channel').classed('active-channel', false) |
|||
|
|||
// add active class to all selected peers
|
|||
selectedChannelIds.forEach((chanid) => { |
|||
d3.select(`#link-${chanid}`).classed('active-channel', true) |
|||
}) |
|||
} |
|||
|
|||
startSimulation() { |
|||
const { simulationData: { nodes, links } } = this.state |
|||
|
|||
// grab the svg el along with the attributes
|
|||
const svg = d3.select('#map') |
|||
const svgBox = svg.node().getBBox() |
|||
|
|||
this.g = svg.append('g').attr('transform', `translate(${svgBox.width / 2},${svgBox.height / 2})`) |
|||
this.link = this.g.append('g').attr('stroke', 'white').attr('stroke-width', 1.5).selectAll('.link') |
|||
this.node = this.g.append('g').attr('stroke', 'silver').attr('stroke-width', 1.5).selectAll('.node') |
|||
|
|||
this.simulation = d3.forceSimulation(nodes) |
|||
.force('charge', d3.forceManyBody().strength(-750)) |
|||
.force('link', d3.forceLink(links).id(d => d.pub_key).distance(500)) |
|||
.force('collide', d3.forceCollide(300)) |
|||
.on('tick', this.ticked) |
|||
.on('end', () => { |
|||
this.setState({ svgLoaded: true }) |
|||
}) |
|||
// zoom
|
|||
const zoom_handler = d3.zoom().on('zoom', this.zoomActions) |
|||
zoom_handler(svg) |
|||
|
|||
this.restart() |
|||
} |
|||
|
|||
zoomActions() { |
|||
this.g.attr('transform', d3Selection.event.transform) |
|||
} |
|||
|
|||
ticked() { |
|||
this.node.attr('cx', d => d.x) |
|||
.attr('cy', d => d.y) |
|||
|
|||
this.link.attr('x1', d => d.source.x) |
|||
.attr('y1', d => d.source.y) |
|||
.attr('x2', d => d.target.x) |
|||
.attr('y2', d => d.target.y) |
|||
} |
|||
|
|||
restart() { |
|||
const { identity_pubkey } = this.props |
|||
const { simulationData: { nodes, links } } = this.state |
|||
|
|||
// Apply the general update pattern to the nodes.
|
|||
this.node = this.node.data(nodes, d => d.pub_key) |
|||
this.node.exit().remove() |
|||
this.node = this.node.enter() |
|||
.append('circle') |
|||
.attr('stroke', () => 'silver') |
|||
.attr('fill', d => (d.pub_key === identity_pubkey ? '#FFF' : '#353535')) |
|||
.attr('r', () => 100) |
|||
.attr('id', d => `node-${d.pub_key}`) |
|||
.attr('class', 'network-node') |
|||
.merge(this.node) |
|||
|
|||
// Apply the general update pattern to the links.
|
|||
this.link = this.link.data(links, d => `${d.source.id}-${d.target.id}`) |
|||
this.link.exit().remove() |
|||
this.link = |
|||
this.link.enter() |
|||
.append('line') |
|||
.attr('id', d => `link-${d.channel_id}`) |
|||
.attr('class', 'network-link') |
|||
.merge(this.link) |
|||
|
|||
// Update and restart the simulation.
|
|||
this.simulation.nodes(nodes) |
|||
this.simulation.force('link').links(links) |
|||
this.simulation.restart() |
|||
} |
|||
|
|||
renderSelectedRoute() { |
|||
const { currentRouteChanIds } = this.props |
|||
|
|||
// remove all route animations before rendering new ones
|
|||
d3.selectAll('.animated-route-circle').remove() |
|||
|
|||
currentRouteChanIds.forEach((chanId) => { |
|||
const link = document.getElementById(`link-${chanId}`) |
|||
|
|||
if (!link) { return } |
|||
const x1 = link.x1.baseVal.value |
|||
const x2 = link.x2.baseVal.value |
|||
const y1 = link.y1.baseVal.value |
|||
const y2 = link.y2.baseVal.value |
|||
|
|||
// create the circle that represent btc traveling through a channel
|
|||
this.g |
|||
.append('circle') |
|||
.attr('id', `circle-${chanId}`) |
|||
.attr('class', 'animated-route-circle') |
|||
.attr('r', 50) |
|||
.attr('cx', x1) |
|||
.attr('cy', y1) |
|||
.attr('fill', '#FFDC53') |
|||
|
|||
// we want the animation to repeat back and forth, this function executes that visually
|
|||
const repeat = () => { |
|||
d3.select(`#circle-${chanId}`) |
|||
.transition() |
|||
.attr('cx', x2) |
|||
.attr('cy', y2) |
|||
.duration(1000) |
|||
.transition() |
|||
.duration(1000) |
|||
.attr('cx', x1) |
|||
.attr('cy', y1) |
|||
.on('end', repeat) |
|||
} |
|||
|
|||
// call repeat to animate the circle
|
|||
repeat() |
|||
}) |
|||
} |
|||
|
|||
render() { |
|||
const { svgLoaded } = this.state |
|||
|
|||
return ( |
|||
<div className={styles.mapContainer} id='mapContainer'> |
|||
{ |
|||
!svgLoaded && |
|||
<div className={styles.loadingContainer}> |
|||
<div className={styles.loadingWrap}> |
|||
<div className={styles.loader} /> |
|||
<div className={styles.loaderbefore} /> |
|||
<div className={styles.circular} /> |
|||
<div className={`${styles.circular} ${styles.another}`} /> |
|||
<div className={styles.text}>loading</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
CanvasNetworkGraph.propTypes = { |
|||
identity_pubkey: PropTypes.string.isRequired, |
|||
|
|||
network: PropTypes.object.isRequired, |
|||
|
|||
selectedPeerPubkeys: PropTypes.array.isRequired, |
|||
selectedChannelIds: PropTypes.array.isRequired, |
|||
currentRouteChanIds: PropTypes.array.isRequired |
|||
} |
|||
|
|||
export default CanvasNetworkGraph |
@ -1,156 +0,0 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
@keyframes fadein { |
|||
0% { background: $white; } |
|||
50% { background: lighten($secondary, 50%); } |
|||
100% { background: $secondary; animation-fill-mode:forwards; } |
|||
} |
|||
|
|||
.mapContainer { |
|||
position: relative; |
|||
display: inline-block; |
|||
width: 70%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.loadingContainer { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
background: $secondary; |
|||
} |
|||
|
|||
.loadingWrap { |
|||
position: relative; |
|||
top: calc(50% - 150px); |
|||
width: 150px; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.loader { |
|||
position: absolute; |
|||
top: 0; |
|||
z-index: 10; |
|||
width: 50px; |
|||
height: 50px; |
|||
border: 15px solid; |
|||
border-radius: 50%; |
|||
border-top-color: rgba(44,44,44,0); |
|||
border-right-color: rgba(55,55,55,0); |
|||
border-bottom-color: rgba(66,66,66,0); |
|||
border-left-color: rgba(33,33,33,0); |
|||
animation: loadEr 3s infinite; |
|||
} |
|||
|
|||
@keyframes loadEr { |
|||
0% { |
|||
border-top-color: rgba(44,44,44,0); |
|||
border-right-color: rgba(55,55,55,0); |
|||
border-bottom-color: rgba(66,66,66,0); |
|||
border-left-color: rgba(33,33,33,0); |
|||
|
|||
} |
|||
10.4% { |
|||
border-top-color: rgba(44,44,44,0.5); |
|||
border-right-color: rgba(55,55,55,0); |
|||
border-bottom-color: rgba(66,66,66,0); |
|||
border-left-color: rgba(33,33,33,0); |
|||
} |
|||
20.8% { |
|||
border-top-color: rgba(44,44,44,0); |
|||
border-right-color: rgba(55,55,55,0); |
|||
border-bottom-color: rgba(66,66,66,0); |
|||
border-left-color: rgba(33,33,33,0); |
|||
} |
|||
31.2% { |
|||
border-top-color: rgba(44,44,44,0); |
|||
border-right-color: rgba(55,55,55,0.5); |
|||
border-bottom-color: rgba(66,66,66,0); |
|||
border-left-color: rgba(33,33,33,0); |
|||
} |
|||
41.6% { |
|||
border-top-color: rgba(44,44,44,0); |
|||
border-right-color: rgba(55,55,55,0); |
|||
border-bottom-color: rgba(66,66,66,0); |
|||
border-left-color: rgba(33,33,33,0); |
|||
transform: rotate(40deg); |
|||
} |
|||
52% { |
|||
border-top-color: rgba(44,44,44,0); |
|||
border-right-color: rgba(55,55,55,0); |
|||
border-bottom-color: rgba(66,66,66,0.5); |
|||
border-left-color: rgba(33,33,33,0); |
|||
} |
|||
62.4% { |
|||
border-top-color: rgba(44,44,44,0); |
|||
border-right-color: rgba(55,55,55,0); |
|||
border-bottom-color: rgba(66,66,66,0); |
|||
border-left-color: rgba(33,33,33,0); |
|||
} |
|||
72.8% { |
|||
border-top-color: rgba(44,44,44,0); |
|||
border-right-color: rgba(55,55,55,0); |
|||
border-bottom-color: rgba(66,66,66,0); |
|||
border-left-color: rgba(33,33,33,0.5); |
|||
} |
|||
} |
|||
|
|||
.loaderbefore { |
|||
width: 50px; |
|||
height:50px; |
|||
border: 15px solid #ddd; |
|||
border-radius: 50%; |
|||
position: absolute; |
|||
top: 0; |
|||
z-index: 9; |
|||
} |
|||
|
|||
.circular { |
|||
position: absolute; |
|||
top: -15px; |
|||
left: -15px; |
|||
width: 70px; |
|||
height: 70px; |
|||
border: 20px solid; |
|||
border-radius: 50%; |
|||
border-top-color: #333; |
|||
border-left-color: #fff; |
|||
border-bottom-color: #333; |
|||
border-right-color: #fff; |
|||
opacity: 0.2; |
|||
animation: poof 5s infinite; |
|||
} |
|||
@keyframes poof { |
|||
0% {transform: scale(1,1) rotate(0deg); opacity: 0.2;} |
|||
50% {transform: scale(4,4) rotate(360deg); opacity: 0;} |
|||
} |
|||
.another { |
|||
opacity: 0.1; |
|||
transform: rotate(90deg); |
|||
animation: poofity 5s infinite; |
|||
animation-delay: 1s; |
|||
} |
|||
@keyframes poofity { |
|||
0% {transform: scale(1,1) rotate(90deg); opacity: 0.1;} |
|||
50% {transform: scale(4,4) rotate(-360deg); opacity: 0;} |
|||
} |
|||
|
|||
.text { |
|||
position: absolute; |
|||
top: 95px; |
|||
left: 8px; |
|||
font-family: Arial; |
|||
text-transform: uppercase; |
|||
color: #888; |
|||
animation: opaa 10s infinite; |
|||
} |
|||
@keyframes opaa { |
|||
0% {opacity: 1;} |
|||
10% {opacity: 0.5} |
|||
15% {opacity: 1;} |
|||
30% {opacity: 1;} |
|||
65% {opacity: 0.3;} |
|||
90% {opacity: 0.8;} |
|||
} |
@ -1,45 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { btc, blockExplorer } from 'utils' |
|||
import styles from './ChannelsList.scss' |
|||
|
|||
const ChannelsList = ({ channels, updateSelectedChannels, selectedChannelIds }) => ( |
|||
<ul className={styles.channels}> |
|||
{ |
|||
channels.map(channel => ( |
|||
<li key={channel.chan_id} className={styles.channel} onClick={() => updateSelectedChannels(channel)}> |
|||
<span className={`${styles.dot} ${selectedChannelIds.includes(channel.chan_id) && styles.active}`} /> |
|||
|
|||
<header> |
|||
<h1>Capacity: {btc.satoshisToBtc(channel.capacity)}</h1> |
|||
<span onClick={() => blockExplorer.showChannelPoint({ channel })}>Channel Point</span> |
|||
</header> |
|||
|
|||
<section> |
|||
<h4>Remote Pubkey:</h4> |
|||
<p>{channel.remote_pubkey.substring(0, Math.min(30, channel.remote_pubkey.length))}...</p> |
|||
</section> |
|||
|
|||
<section className={styles.funds}> |
|||
<div> |
|||
<h4>Sent:</h4> |
|||
<p>{btc.satoshisToBtc(channel.total_satoshis_sent)} BTC</p> |
|||
</div> |
|||
<div> |
|||
<h4>Received:</h4> |
|||
<p>{btc.satoshisToBtc(channel.total_satoshis_received)} BTC</p> |
|||
</div> |
|||
</section> |
|||
</li> |
|||
)) |
|||
} |
|||
</ul> |
|||
) |
|||
|
|||
ChannelsList.propTypes = { |
|||
channels: PropTypes.array.isRequired, |
|||
updateSelectedChannels: PropTypes.func.isRequired, |
|||
selectedChannelIds: PropTypes.array.isRequired |
|||
} |
|||
|
|||
export default ChannelsList |
@ -1,78 +0,0 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.channels { |
|||
color: $white; |
|||
margin-top: 50px; |
|||
} |
|||
|
|||
.channel { |
|||
position: relative; |
|||
margin: 20px 0; |
|||
padding: 10px 40px; |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
background: darken(#353535, 10%); |
|||
|
|||
.dot { |
|||
background: #88D4A2; |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
|
|||
.dot { |
|||
position: absolute; |
|||
top: calc(15% - 10px); |
|||
left: 5%; |
|||
width: 10px; |
|||
height: 10px; |
|||
border: 1px solid #979797; |
|||
border-radius: 50%; |
|||
|
|||
&.active { |
|||
background: #88D4A2; |
|||
} |
|||
} |
|||
|
|||
header { |
|||
margin-bottom: 10px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
|
|||
h1 { |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
span { |
|||
font-size: 10px; |
|||
text-decoration: underline; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
color: #88D4A2; |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
section { |
|||
margin: 10px 0; |
|||
|
|||
h4 { |
|||
font-weight: bold; |
|||
text-transform: uppercase; |
|||
font-size: 10px; |
|||
margin-bottom: 5px; |
|||
} |
|||
} |
|||
|
|||
.funds { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-top: 20px; |
|||
} |
|||
} |
@ -1,25 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import styles from './PeersList.scss' |
|||
|
|||
const PeersList = ({ peers, updateSelectedPeers, selectedPeerPubkeys }) => ( |
|||
<ul className={styles.peers}> |
|||
{ |
|||
peers.map(peer => ( |
|||
<li key={peer.peer_id} className={styles.peer} onClick={() => updateSelectedPeers(peer)}> |
|||
<span className={`${styles.dot} ${selectedPeerPubkeys.includes(peer.pub_key) && styles.active}`} /> |
|||
<h1>{peer.address}</h1> |
|||
<h4>{peer.pub_key}</h4> |
|||
</li> |
|||
)) |
|||
} |
|||
</ul> |
|||
) |
|||
|
|||
PeersList.propTypes = { |
|||
peers: PropTypes.array.isRequired, |
|||
updateSelectedPeers: PropTypes.func.isRequired, |
|||
selectedPeerPubkeys: PropTypes.array.isRequired |
|||
} |
|||
|
|||
export default PeersList |
@ -1,46 +0,0 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.peers { |
|||
color: $white; |
|||
margin-top: 50px; |
|||
} |
|||
|
|||
.peer { |
|||
position: relative; |
|||
margin: 20px 0; |
|||
padding: 10px 40px; |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
background: darken(#353535, 10%); |
|||
|
|||
.dot { |
|||
background: #5589F3; |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
|
|||
.dot { |
|||
position: absolute; |
|||
top: calc(50% - 10px); |
|||
left: 5%; |
|||
width: 10px; |
|||
height: 10px; |
|||
border: 1px solid #979797; |
|||
border-radius: 50%; |
|||
|
|||
&.active { |
|||
background: #5589F3; |
|||
} |
|||
} |
|||
|
|||
h1 { |
|||
font-size: 16px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
h4 { |
|||
font-size: 8px; |
|||
} |
|||
} |
@ -1,63 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { btc } from 'utils' |
|||
import styles from './TransactionForm.scss' |
|||
|
|||
const TransactionForm = ({ |
|||
updatePayReq, pay_req, loadingRoutes, payReqRoutes, setCurrentRoute, currentRoute |
|||
}) => ( |
|||
<div className={styles.transactionForm}> |
|||
<div className={styles.form}> |
|||
<input |
|||
className={styles.transactionInput} |
|||
placeholder='Payment request...' |
|||
value={pay_req} |
|||
onChange={event => updatePayReq(event.target.value)} |
|||
/> |
|||
</div> |
|||
|
|||
{ |
|||
loadingRoutes && |
|||
<div className={styles.loading}> |
|||
<div className={styles.spinner} /> |
|||
<h1>calculating all routes...</h1> |
|||
</div> |
|||
} |
|||
|
|||
<ul className={styles.routes}> |
|||
{ |
|||
payReqRoutes.map((route, index) => ( |
|||
<li className={`${styles.route} ${currentRoute === route && styles.active}`} key={index} onClick={() => setCurrentRoute(route)}> |
|||
<header> |
|||
<h1>Route #{index + 1}</h1> |
|||
<span>Hops: {route.hops.length}</span> |
|||
</header> |
|||
|
|||
<div className={styles.data}> |
|||
<section> |
|||
<h4>Amount</h4> |
|||
<span>{btc.satoshisToBtc(route.total_amt)} BTC</span> |
|||
</section> |
|||
|
|||
<section> |
|||
<h4>Fees</h4> |
|||
<span>{btc.satoshisToBtc(route.total_fees)} BTC</span> |
|||
</section> |
|||
</div> |
|||
</li> |
|||
)) |
|||
} |
|||
</ul> |
|||
</div> |
|||
) |
|||
|
|||
TransactionForm.propTypes = { |
|||
updatePayReq: PropTypes.func.isRequired, |
|||
pay_req: PropTypes.string.isRequired, |
|||
loadingRoutes: PropTypes.bool.isRequired, |
|||
payReqRoutes: PropTypes.array.isRequired, |
|||
setCurrentRoute: PropTypes.func.isRequired, |
|||
currentRoute: PropTypes.object.isRequired |
|||
} |
|||
|
|||
export default TransactionForm |
@ -1,125 +0,0 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
@-webkit-keyframes animation-rotate { |
|||
100% { |
|||
-webkit-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@-moz-keyframes animation-rotate { |
|||
100% { |
|||
-moz-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@-o-keyframes animation-rotate { |
|||
100% { |
|||
-o-transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
@keyframes animation-rotate { |
|||
100% { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
.spinner { |
|||
border: 1px solid rgba(255, 220, 83, 0.1); |
|||
border-left-color: rgba(255, 220, 83, 0.4); |
|||
-webkit-border-radius: 999px; |
|||
-moz-border-radius: 999px; |
|||
border-radius: 999px; |
|||
} |
|||
|
|||
.spinner { |
|||
margin: 0 auto; |
|||
height: 100px; |
|||
width: 100px; |
|||
-webkit-animation: animation-rotate 1000ms linear infinite; |
|||
-moz-animation: animation-rotate 1000ms linear infinite; |
|||
-o-animation: animation-rotate 1000ms linear infinite; |
|||
animation: animation-rotate 1000ms linear infinite; |
|||
} |
|||
|
|||
.loading { |
|||
margin-top: 40px; |
|||
|
|||
h1 { |
|||
text-align: center; |
|||
margin-top: 25px; |
|||
} |
|||
} |
|||
|
|||
.transactionForm { |
|||
color: $white; |
|||
margin-top: 50px; |
|||
|
|||
.form { |
|||
padding: 0 20px; |
|||
} |
|||
|
|||
.transactionInput { |
|||
outline: 0; |
|||
border: 0; |
|||
border-bottom: 1px solid $secondary; |
|||
color: $secondary; |
|||
background: transparent; |
|||
padding: 5px; |
|||
width: 100%; |
|||
font-size: 14px; |
|||
color: $white; |
|||
} |
|||
} |
|||
|
|||
.routes { |
|||
margin-top: 40px; |
|||
} |
|||
|
|||
.route { |
|||
margin: 20px 0; |
|||
padding: 20px; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
background: darken(#353535, 10%); |
|||
} |
|||
|
|||
&.active { |
|||
background: darken(#353535, 10%); |
|||
} |
|||
|
|||
header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
margin-bottom: 20px; |
|||
|
|||
h1 { |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
span { |
|||
font-weight: bold; |
|||
text-transform: uppercase; |
|||
font-size: 12px; |
|||
letter-spacing: 1.2px; |
|||
} |
|||
} |
|||
|
|||
.data { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
|
|||
section { |
|||
h4 { |
|||
font-size: 12px; |
|||
font-weight: bold; |
|||
margin-bottom: 5px; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,23 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import styles from './Alias.scss' |
|||
|
|||
const Alias = ({ alias, updateAlias }) => ( |
|||
<div className={styles.container}> |
|||
<input |
|||
type='text' |
|||
placeholder='Satoshi' |
|||
className={styles.alias} |
|||
ref={input => input && input.focus()} |
|||
value={alias} |
|||
onChange={event => updateAlias(event.target.value)} |
|||
/> |
|||
</div> |
|||
) |
|||
|
|||
Alias.propTypes = { |
|||
alias: PropTypes.string.isRequired, |
|||
updateAlias: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default Alias |
@ -0,0 +1,15 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.alias { |
|||
background: transparent; |
|||
outline: none; |
|||
border: 0; |
|||
color: $gold; |
|||
-webkit-text-fill-color: $white; |
|||
font-size: 22px; |
|||
} |
|||
|
|||
.alias::-webkit-input-placeholder { |
|||
text-shadow: none; |
|||
-webkit-text-fill-color: initial; |
|||
} |
@ -0,0 +1,55 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import Isvg from 'react-inlinesvg' |
|||
import zapLogo from 'icons/zap_logo.svg' |
|||
import styles from './FormContainer.scss' |
|||
|
|||
const FormContainer = ({ title, description, back, next, children }) => ( |
|||
<div className={styles.container}> |
|||
<div className={styles.titleBar} /> |
|||
|
|||
<header className={styles.header}> |
|||
<section> |
|||
<Isvg src={zapLogo} /> |
|||
</section> |
|||
<section /> |
|||
</header> |
|||
|
|||
<div className={styles.info}> |
|||
<h1>{title}</h1> |
|||
<p>{description}</p> |
|||
</div> |
|||
|
|||
<div className={styles.content}> |
|||
{children} |
|||
</div> |
|||
|
|||
<footer className={styles.footer}> |
|||
<div className={styles.buttonsContainer}> |
|||
<section> |
|||
{ |
|||
back && <div onClick={back}>Back</div> |
|||
} |
|||
</section> |
|||
<section> |
|||
{ |
|||
next && <div onClick={next}>Next</div> |
|||
} |
|||
</section> |
|||
</div> |
|||
</footer> |
|||
</div> |
|||
) |
|||
|
|||
|
|||
FormContainer.propTypes = { |
|||
title: PropTypes.string.isRequired, |
|||
description: PropTypes.string.isRequired, |
|||
|
|||
back: PropTypes.func, |
|||
next: PropTypes.func, |
|||
|
|||
children: PropTypes.object.isRequired |
|||
} |
|||
|
|||
export default FormContainer |
@ -0,0 +1,67 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.container { |
|||
position: relative; |
|||
height: 100vh; |
|||
background: $darkspaceblue; |
|||
} |
|||
|
|||
.titleBar { |
|||
background: $spacegrey; |
|||
height: 20px; |
|||
-webkit-user-select: none; |
|||
-webkit-app-region: drag; |
|||
} |
|||
|
|||
.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
padding: 20px 40px; |
|||
} |
|||
|
|||
.info { |
|||
color: $white; |
|||
margin: 20px 0 20px 0; |
|||
padding: 20px 40px; |
|||
|
|||
h1 { |
|||
font-size: 22px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
p { |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
|
|||
.content { |
|||
position: relative; |
|||
background: $spaceblue; |
|||
height: 100vh; |
|||
padding: 60px 40px; |
|||
} |
|||
|
|||
.footer { |
|||
position: absolute; |
|||
bottom: 0; |
|||
padding: 20px 40px; |
|||
color: $white; |
|||
width: calc(100% - 80px); |
|||
|
|||
.buttonsContainer { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
|
|||
div { |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
@ -0,0 +1,49 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
|
|||
import LoadingBolt from 'components/LoadingBolt' |
|||
|
|||
import FormContainer from './FormContainer' |
|||
import Alias from './Alias' |
|||
import styles from './Onboarding.scss' |
|||
|
|||
const Onboarding = ({ |
|||
onboarding: { |
|||
step, |
|||
alias |
|||
}, |
|||
submit, |
|||
aliasProps |
|||
}) => { |
|||
const renderStep = () => { |
|||
switch (step) { |
|||
case 1: |
|||
return ( |
|||
<FormContainer |
|||
title={'1. What should we call you?'} |
|||
description={'Set your nickname to help others connect with you on the Lightning Network'} |
|||
back={null} |
|||
next={() => submit(alias)} |
|||
> |
|||
<Alias {...aliasProps} /> |
|||
</FormContainer> |
|||
) |
|||
default: |
|||
return <LoadingBolt /> |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
{renderStep()} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
Onboarding.propTypes = { |
|||
onboarding: PropTypes.object.isRequired, |
|||
aliasProps: PropTypes.object.isRequired, |
|||
submit: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default Onboarding |
@ -0,0 +1,45 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import Isvg from 'react-inlinesvg' |
|||
import zapLogo from 'icons/zap_logo.svg' |
|||
import styles from './Syncing.scss' |
|||
|
|||
|
|||
class Syncing extends Component { |
|||
componentWillMount() { |
|||
this.props.fetchBlockHeight() |
|||
} |
|||
|
|||
render() { |
|||
const { syncPercentage } = this.props |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
<div className={styles.titleBar} /> |
|||
|
|||
<div className={styles.content}> |
|||
<header> |
|||
<Isvg className={styles.bitcoinLogo} src={zapLogo} /> |
|||
</header> |
|||
<section className={styles.progressContainer}> |
|||
<h1>Syncing to the blockchain...</h1> |
|||
<div className={styles.progressBar}> |
|||
<div className={styles.progress} style={{ width: isNaN(syncPercentage) ? 0 : `${syncPercentage}%` }} /> |
|||
</div> |
|||
<h4>{isNaN(parseInt(syncPercentage, 10)) || syncPercentage.toString().length === 0 ? '' : `${syncPercentage}%`}</h4> |
|||
</section> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
Syncing.propTypes = { |
|||
fetchBlockHeight: PropTypes.func.isRequired, |
|||
syncPercentage: PropTypes.oneOfType([ |
|||
PropTypes.number, |
|||
PropTypes.string |
|||
]).isRequired |
|||
} |
|||
|
|||
export default Syncing |
@ -0,0 +1,50 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.container { |
|||
position: relative; |
|||
height: 100vh; |
|||
background: $spaceblue; |
|||
} |
|||
|
|||
.titleBar { |
|||
background: $spacegrey; |
|||
height: 20px; |
|||
-webkit-user-select: none; |
|||
-webkit-app-region: drag; |
|||
} |
|||
|
|||
.content { |
|||
padding: 20px 40px; |
|||
} |
|||
|
|||
.progressContainer { |
|||
color: $white; |
|||
text-align: center; |
|||
margin-top: 20%; |
|||
|
|||
h1 { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.progressBar { |
|||
width: 75%; |
|||
max-width: 700px; |
|||
margin: 0 auto; |
|||
height: 10px; |
|||
border-radius: 5px; |
|||
background: $spaceborder; |
|||
} |
|||
|
|||
.progress { |
|||
background: $gold; |
|||
background: #DEA326; |
|||
height: 10px; |
|||
border-radius: 5px; |
|||
transition: all 0.25s; |
|||
} |
|||
|
|||
h4 { |
|||
color: $gold; |
|||
margin-top: 10px; |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
import Onboarding from './Onboarding' |
|||
|
|||
export default Onboarding |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 322 B |
After Width: | Height: | Size: 304 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 575 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 312 B |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 4.3 KiB |
@ -0,0 +1,69 @@ |
|||
import { ipcRenderer } from 'electron' |
|||
|
|||
// ------------------------------------
|
|||
// Constants
|
|||
// ------------------------------------
|
|||
export const UPDATE_ALIAS = 'UPDATE_ALIAS' |
|||
|
|||
export const CHANGE_STEP = 'CHANGE_STEP' |
|||
|
|||
export const ONBOARDING_STARTED = 'ONBOARDING_STARTED' |
|||
export const ONBOARDING_FINISHED = 'ONBOARDING_FINISHED' |
|||
|
|||
// ------------------------------------
|
|||
// Actions
|
|||
// ------------------------------------
|
|||
export function updateAlias(alias) { |
|||
return { |
|||
type: UPDATE_ALIAS, |
|||
alias |
|||
} |
|||
} |
|||
|
|||
export function changeStep(step) { |
|||
return { |
|||
type: CHANGE_STEP, |
|||
step |
|||
} |
|||
} |
|||
|
|||
export function submit(alias) { |
|||
// alert the app we're done onboarding and it's cool to start LND
|
|||
ipcRenderer.send('onboardingFinished', { alias }) |
|||
|
|||
return { |
|||
type: ONBOARDING_FINISHED |
|||
} |
|||
} |
|||
|
|||
export const startOnboarding = () => (dispatch) => { |
|||
dispatch({ type: ONBOARDING_STARTED }) |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Action Handlers
|
|||
// ------------------------------------
|
|||
const ACTION_HANDLERS = { |
|||
[UPDATE_ALIAS]: (state, { alias }) => ({ ...state, alias }), |
|||
[CHANGE_STEP]: (state, { step }) => ({ ...state, step }), |
|||
[ONBOARDING_STARTED]: state => ({ ...state, onboarded: false }), |
|||
[ONBOARDING_FINISHED]: state => ({ ...state, onboarded: true }) |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Reducer
|
|||
// ------------------------------------
|
|||
const initialState = { |
|||
onboarded: true, |
|||
step: 1, |
|||
alias: '' |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Reducer
|
|||
// ------------------------------------
|
|||
export default function lndReducer(state = initialState, action) { |
|||
const handler = ACTION_HANDLERS[action.type] |
|||
|
|||
return handler ? handler(state, action) : state |
|||
} |
@ -1,8 +1,16 @@ |
|||
@import '../../../variables.scss'; |
|||
|
|||
.content { |
|||
position: relative; |
|||
width: 80%; |
|||
height: 100vh; |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
overflow-y: scroll; |
|||
} |
|||
|
|||
.titleBar { |
|||
background: $spacegrey; |
|||
height: 20px; |
|||
-webkit-user-select: none; |
|||
-webkit-app-region: drag; |
|||
} |
@ -1,195 +0,0 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
|
|||
import Isvg from 'react-inlinesvg' |
|||
import { MdSearch } from 'react-icons/lib/md' |
|||
import { FaAngleDown, FaRepeat } from 'react-icons/lib/fa' |
|||
|
|||
import ContactModal from 'components/Contacts/ContactModal' |
|||
import ContactsForm from 'components/Contacts/ContactsForm' |
|||
import OnlineContact from 'components/Contacts/OnlineContact' |
|||
import PendingContact from 'components/Contacts/PendingContact' |
|||
import ClosingContact from 'components/Contacts/ClosingContact' |
|||
import OfflineContact from 'components/Contacts/OfflineContact' |
|||
import LoadingContact from 'components/Contacts/LoadingContact' |
|||
|
|||
import plus from 'icons/plus.svg' |
|||
|
|||
import styles from './Contacts.scss' |
|||
|
|||
class Contacts extends Component { |
|||
constructor(props) { |
|||
super(props) |
|||
|
|||
this.state = { |
|||
refreshing: false |
|||
} |
|||
} |
|||
|
|||
componentWillMount() { |
|||
const { fetchChannels, fetchPeers, fetchDescribeNetwork } = this.props |
|||
|
|||
fetchChannels() |
|||
fetchPeers() |
|||
fetchDescribeNetwork() |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
channels: { |
|||
searchQuery, |
|||
filterPulldown, |
|||
filter, |
|||
loadingChannelPubkeys, |
|||
closingChannelIds |
|||
}, |
|||
currentChannels, |
|||
activeChannels, |
|||
fetchChannels, |
|||
updateChannelSearchQuery, |
|||
|
|||
toggleFilterPulldown, |
|||
changeFilter, |
|||
nonActiveFilters, |
|||
|
|||
openContactsForm, |
|||
openContactModal, |
|||
|
|||
contactModalProps, |
|||
contactsFormProps |
|||
} = this.props |
|||
|
|||
const refreshClicked = () => { |
|||
// turn the spinner on
|
|||
this.setState({ refreshing: true }) |
|||
|
|||
// store event in icon so we dont get an error when react clears it
|
|||
const icon = this.repeat.childNodes |
|||
|
|||
// fetch channels
|
|||
fetchChannels() |
|||
|
|||
// wait for the svg to appear as child
|
|||
const svgTimeout = setTimeout(() => { |
|||
if (icon[0].tagName === 'svg') { |
|||
// spin icon for 1 sec
|
|||
icon[0].style.animation = 'spin 1000ms linear 1' |
|||
clearTimeout(svgTimeout) |
|||
} |
|||
}, 1) |
|||
|
|||
// clear animation after the second so we can reuse it
|
|||
const refreshTimeout = setTimeout(() => { |
|||
icon[0].style.animation = '' |
|||
this.setState({ refreshing: false }) |
|||
clearTimeout(refreshTimeout) |
|||
}, 1000) |
|||
} |
|||
|
|||
return ( |
|||
<div className={styles.friendsContainer}> |
|||
<ContactModal {...contactModalProps} /> |
|||
<ContactsForm {...contactsFormProps} /> |
|||
|
|||
<header className={styles.header}> |
|||
<div className={styles.titleContainer}> |
|||
<div className={styles.left}> |
|||
<h1>Contacts <span>({activeChannels.length} online)</span></h1> |
|||
</div> |
|||
</div> |
|||
<div className={styles.newFriendContainer}> |
|||
<div className={`buttonPrimary ${styles.newFriendButton}`} onClick={openContactsForm}> |
|||
<Isvg src={plus} /> |
|||
<span>Add</span> |
|||
</div> |
|||
</div> |
|||
</header> |
|||
|
|||
<div className={styles.search}> |
|||
<label className={`${styles.label} ${styles.input}`} htmlFor='channelSearch'> |
|||
<MdSearch /> |
|||
</label> |
|||
<input |
|||
value={searchQuery} |
|||
onChange={event => updateChannelSearchQuery(event.target.value)} |
|||
className={`${styles.text} ${styles.input}`} |
|||
placeholder='Search your contacts list...' |
|||
type='text' |
|||
id='channelSearch' |
|||
/> |
|||
</div> |
|||
|
|||
<div className={styles.filtersContainer}> |
|||
<section> |
|||
<h2 onClick={toggleFilterPulldown} className={styles.filterTitle}> |
|||
{filter.name} <span className={filterPulldown && styles.pulldown}><FaAngleDown /></span> |
|||
</h2> |
|||
<ul className={`${styles.filters} ${filterPulldown && styles.active}`}> |
|||
{ |
|||
nonActiveFilters.map(f => ( |
|||
<li key={f.key} onClick={() => changeFilter(f)}> |
|||
{f.name} |
|||
</li> |
|||
)) |
|||
} |
|||
</ul> |
|||
</section> |
|||
<section className={styles.refreshContainer}> |
|||
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}> |
|||
{ |
|||
this.state.refreshing ? |
|||
<FaRepeat /> |
|||
: |
|||
'Refresh' |
|||
} |
|||
</span> |
|||
</section> |
|||
</div> |
|||
|
|||
<ul className={`${styles.friends} ${filterPulldown && styles.fade}`}> |
|||
{ |
|||
loadingChannelPubkeys.map(pubkey => <LoadingContact pubkey={pubkey} isClosing={false} key={pubkey} />) |
|||
} |
|||
|
|||
{ |
|||
currentChannels.length > 0 && currentChannels.map((channel, index) => { |
|||
if (closingChannelIds.includes(channel.chan_id)) { |
|||
return <LoadingContact pubkey={channel.remote_pubkey} isClosing key={index} /> |
|||
} else if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) { |
|||
return <PendingContact channel={channel} key={index} /> |
|||
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { |
|||
return <ClosingContact channel={channel} key={index} /> |
|||
} else if (!channel.active) { |
|||
return <OfflineContact channel={channel} key={index} openContactModal={openContactModal} /> |
|||
} |
|||
return <OnlineContact channel={channel} key={index} openContactModal={openContactModal} /> |
|||
}) |
|||
} |
|||
</ul> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
Contacts.propTypes = { |
|||
fetchPeers: PropTypes.func.isRequired, |
|||
fetchDescribeNetwork: PropTypes.func.isRequired, |
|||
|
|||
channels: PropTypes.object.isRequired, |
|||
currentChannels: PropTypes.array.isRequired, |
|||
activeChannels: PropTypes.array.isRequired, |
|||
fetchChannels: PropTypes.func.isRequired, |
|||
updateChannelSearchQuery: PropTypes.func.isRequired, |
|||
|
|||
toggleFilterPulldown: PropTypes.func.isRequired, |
|||
changeFilter: PropTypes.func.isRequired, |
|||
nonActiveFilters: PropTypes.array.isRequired, |
|||
|
|||
openContactsForm: PropTypes.func.isRequired, |
|||
openContactModal: PropTypes.func.isRequired, |
|||
|
|||
contactModalProps: PropTypes.object.isRequired, |
|||
contactsFormProps: PropTypes.object.isRequired |
|||
} |
|||
|
|||
export default Contacts |
@ -1,172 +0,0 @@ |
|||
@import '../../../variables.scss'; |
|||
|
|||
.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
background: $lightgrey; |
|||
|
|||
.titleContainer { |
|||
padding: 20px 40px; |
|||
|
|||
.left { |
|||
padding: 10px 0; |
|||
|
|||
h1 { |
|||
text-transform: uppercase; |
|||
font-size: 26px; |
|||
margin-right: 5px; |
|||
|
|||
span { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
font-size: 16px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.newFriendContainer { |
|||
padding: 20px 40px; |
|||
|
|||
.newFriendButton { |
|||
box-shadow: none; |
|||
transition: all 0.25s; |
|||
padding-top: 12px; |
|||
padding-bottom: 10px; |
|||
font-size: 14px; |
|||
|
|||
&:hover { |
|||
background: darken($main, 10%); |
|||
} |
|||
|
|||
span { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
|
|||
&:nth-child(1) svg { |
|||
width: 14px; |
|||
height: 14px; |
|||
margin-right: 5px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.search { |
|||
height: 55px; |
|||
padding: 2px 25px; |
|||
border-top: 1px solid $darkgrey; |
|||
border-bottom: 1px solid $darkgrey; |
|||
background: $white; |
|||
|
|||
.input { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
height: 100%; |
|||
} |
|||
|
|||
.label { |
|||
width: 5%; |
|||
line-height: 50px; |
|||
font-size: 20px; |
|||
text-align: center; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.text { |
|||
width: 95%; |
|||
outline: 0; |
|||
padding: 0; |
|||
border: 0; |
|||
border-radius: 0; |
|||
height: 50px; |
|||
font-size: 16px; |
|||
} |
|||
} |
|||
|
|||
.filtersContainer { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
margin-top: 20px; |
|||
padding: 20px 40px; |
|||
|
|||
h2, h2 span { |
|||
color: $bluegrey; |
|||
cursor: pointer; |
|||
transition: color 0.25s; |
|||
|
|||
&:hover { |
|||
color: lighten($bluegrey, 10%); |
|||
} |
|||
} |
|||
|
|||
h2, .filters li { |
|||
text-transform: uppercase; |
|||
letter-spacing: 1.5px; |
|||
color: $darkestgrey; |
|||
font-size: 14px; |
|||
font-weight: 400; |
|||
} |
|||
|
|||
h2 span.pulldown { |
|||
color: $main; |
|||
} |
|||
|
|||
.filters { |
|||
display: none; |
|||
|
|||
&.active { |
|||
display: block; |
|||
position: absolute; |
|||
bottom: -100px; |
|||
z-index: 10; |
|||
|
|||
li { |
|||
margin: 5px 0; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
color: $main; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.refreshContainer { |
|||
text-align: right; |
|||
cursor: pointer; |
|||
|
|||
.refresh { |
|||
cursor: pointer; |
|||
color: $darkestgrey; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
color: $main; |
|||
} |
|||
|
|||
svg { |
|||
font-size: 12px; |
|||
color: $darkestgrey; |
|||
|
|||
&:hover { |
|||
color: $darkestgrey; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.friends { |
|||
padding: 10px 0 60px 0; |
|||
opacity: 1; |
|||
transition: all 0.25s; |
|||
|
|||
&.fade { |
|||
opacity: 0.05; |
|||
} |
|||
} |
@ -1,109 +0,0 @@ |
|||
import { withRouter } from 'react-router' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { |
|||
fetchChannels, |
|||
openChannel, |
|||
closeChannel, |
|||
|
|||
updateChannelSearchQuery, |
|||
toggleFilterPulldown, |
|||
changeFilter, |
|||
openContactModal, |
|||
closeContactModal, |
|||
currentChannels, |
|||
|
|||
channelsSelectors |
|||
} from 'reducers/channels' |
|||
|
|||
import { fetchPeers } from 'reducers/peers' |
|||
|
|||
import { fetchDescribeNetwork } from 'reducers/network' |
|||
|
|||
import { |
|||
openContactsForm, |
|||
closeContactsForm, |
|||
updateContactFormSearchQuery, |
|||
updateContactCapacity, |
|||
contactFormSelectors |
|||
} from 'reducers/contactsform' |
|||
|
|||
import Contacts from '../components/Contacts' |
|||
|
|||
const mapDispatchToProps = { |
|||
openContactsForm, |
|||
closeContactsForm, |
|||
openContactModal, |
|||
closeContactModal, |
|||
updateContactFormSearchQuery, |
|||
updateContactCapacity, |
|||
openChannel, |
|||
closeChannel, |
|||
updateChannelSearchQuery, |
|||
toggleFilterPulldown, |
|||
changeFilter, |
|||
|
|||
fetchChannels, |
|||
fetchPeers, |
|||
fetchDescribeNetwork |
|||
} |
|||
|
|||
const mapStateToProps = state => ({ |
|||
channels: state.channels, |
|||
peers: state.peers, |
|||
network: state.network, |
|||
contactsform: state.contactsform, |
|||
|
|||
currentChannels: currentChannels(state), |
|||
activeChannels: channelsSelectors.activeChannels(state), |
|||
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state), |
|||
nonActiveChannels: channelsSelectors.nonActiveChannels(state), |
|||
nonActiveChannelPubkeys: channelsSelectors.nonActiveChannelPubkeys(state), |
|||
pendingOpenChannels: channelsSelectors.pendingOpenChannels(state), |
|||
pendingOpenChannelPubkeys: channelsSelectors.pendingOpenChannelPubkeys(state), |
|||
closingPendingChannels: channelsSelectors.closingPendingChannels(state), |
|||
nonActiveFilters: channelsSelectors.nonActiveFilters(state), |
|||
channelNodes: channelsSelectors.channelNodes(state), |
|||
|
|||
filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state), |
|||
showManualForm: contactFormSelectors.showManualForm(state) |
|||
}) |
|||
|
|||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
|||
const contactModalProps = { |
|||
closeContactModal: dispatchProps.closeContactModal, |
|||
closeChannel: dispatchProps.closeChannel, |
|||
|
|||
isOpen: stateProps.channels.contactModal.isOpen, |
|||
channel: stateProps.channels.contactModal.channel, |
|||
channelNodes: stateProps.channelNodes, |
|||
closingChannelIds: stateProps.channels.closingChannelIds |
|||
} |
|||
|
|||
const contactsFormProps = { |
|||
closeContactsForm: dispatchProps.closeContactsForm, |
|||
updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery, |
|||
updateContactCapacity: dispatchProps.updateContactCapacity, |
|||
openChannel: dispatchProps.openChannel, |
|||
|
|||
contactsform: stateProps.contactsform, |
|||
filteredNetworkNodes: stateProps.filteredNetworkNodes, |
|||
loadingChannelPubkeys: stateProps.channels.loadingChannelPubkeys, |
|||
showManualForm: stateProps.showManualForm, |
|||
|
|||
activeChannelPubkeys: stateProps.activeChannelPubkeys, |
|||
nonActiveChannelPubkeys: stateProps.nonActiveChannelPubkeys, |
|||
pendingOpenChannelPubkeys: stateProps.pendingOpenChannelPubkeys |
|||
} |
|||
|
|||
return { |
|||
...stateProps, |
|||
...dispatchProps, |
|||
...ownProps, |
|||
|
|||
contactModalProps, |
|||
contactsFormProps |
|||
} |
|||
} |
|||
|
|||
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Contacts)) |
@ -1,3 +0,0 @@ |
|||
import ContactsContainer from './containers/ContactsContainer' |
|||
|
|||
export default ContactsContainer |
@ -1,168 +0,0 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
|
|||
import CanvasNetworkGraph from 'components/Network/CanvasNetworkGraph' |
|||
import PeersList from 'components/Network/PeersList' |
|||
import ChannelsList from 'components/Network/ChannelsList' |
|||
import TransactionForm from 'components/Network/TransactionForm' |
|||
|
|||
import styles from './Network.scss' |
|||
|
|||
class Network extends Component { |
|||
componentWillMount() { |
|||
const { fetchDescribeNetwork, fetchPeers, fetchChannels } = this.props |
|||
|
|||
fetchPeers() |
|||
fetchChannels() |
|||
fetchDescribeNetwork() |
|||
} |
|||
|
|||
componentDidUpdate(prevProps) { |
|||
const { |
|||
payReqIsLn, network: { pay_req }, fetchInvoiceAndQueryRoutes, clearQueryRoutes |
|||
} = this.props |
|||
|
|||
// If LN go retrieve invoice details
|
|||
if ((prevProps.network.pay_req !== pay_req) && payReqIsLn) { |
|||
fetchInvoiceAndQueryRoutes(pay_req) |
|||
} |
|||
|
|||
if (prevProps.payReqIsLn && !payReqIsLn) { |
|||
clearQueryRoutes() |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
const { |
|||
clearQueryRoutes, resetPayReq, clearSelectedChannels, clearSelectedPeers |
|||
} = this.props |
|||
|
|||
clearQueryRoutes() |
|||
resetPayReq() |
|||
clearSelectedChannels() |
|||
clearSelectedPeers() |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
setCurrentTab, |
|||
updateSelectedPeers, |
|||
setCurrentRoute, |
|||
|
|||
network, |
|||
selectedPeerPubkeys, |
|||
currentRouteChanIds, |
|||
|
|||
peers: { peers }, |
|||
|
|||
activeChannels, |
|||
selectedChannelIds, |
|||
updateSelectedChannels, |
|||
|
|||
updatePayReq, |
|||
|
|||
identity_pubkey |
|||
} = this.props |
|||
|
|||
const renderContent = () => { |
|||
switch (network.currentTab) { |
|||
case 1: |
|||
return <PeersList peers={peers} updateSelectedPeers={updateSelectedPeers} selectedPeerPubkeys={selectedPeerPubkeys} /> |
|||
case 2: |
|||
return <ChannelsList channels={activeChannels} updateSelectedChannels={updateSelectedChannels} selectedChannelIds={selectedChannelIds} /> |
|||
case 3: |
|||
return ( |
|||
<TransactionForm |
|||
updatePayReq={updatePayReq} |
|||
pay_req={network.pay_req} |
|||
loadingRoutes={network.fetchingInvoiceAndQueryingRoutes} |
|||
payReqRoutes={network.payReqRoutes} |
|||
setCurrentRoute={setCurrentRoute} |
|||
currentRoute={network.currentRoute} |
|||
/> |
|||
) |
|||
default: |
|||
return <span /> |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
{ |
|||
!network.networkLoading && |
|||
<section className={styles.stats}> |
|||
<span>{network.nodes.length} nodes</span> |
|||
<span>|</span> |
|||
<span>{network.edges.length} channels</span> |
|||
</section> |
|||
} |
|||
|
|||
<CanvasNetworkGraph |
|||
className={styles.network} |
|||
network={network} |
|||
identity_pubkey={identity_pubkey} |
|||
selectedPeerPubkeys={selectedPeerPubkeys} |
|||
selectedChannelIds={selectedChannelIds} |
|||
currentRouteChanIds={currentRouteChanIds} |
|||
/> |
|||
|
|||
<section className={styles.toolbox}> |
|||
<ul className={styles.tabs}> |
|||
<li |
|||
className={`${styles.tab} ${styles.peersTab} ${network.currentTab === 1 && styles.active}`} |
|||
onClick={() => setCurrentTab(1)} |
|||
> |
|||
Peers |
|||
</li> |
|||
<li |
|||
className={`${styles.tab} ${styles.channelsTab} ${network.currentTab === 2 && styles.active}`} |
|||
onClick={() => setCurrentTab(2)} |
|||
> |
|||
Channels |
|||
</li> |
|||
<li |
|||
className={`${styles.tab} ${styles.transactionsTab} ${network.currentTab === 3 && styles.active}`} |
|||
onClick={() => setCurrentTab(3)} |
|||
> |
|||
Transactions |
|||
</li> |
|||
</ul> |
|||
|
|||
<div className={styles.content}> |
|||
{renderContent()} |
|||
</div> |
|||
</section> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
Network.propTypes = { |
|||
fetchDescribeNetwork: PropTypes.func.isRequired, |
|||
fetchPeers: PropTypes.func.isRequired, |
|||
setCurrentTab: PropTypes.func.isRequired, |
|||
fetchChannels: PropTypes.func.isRequired, |
|||
fetchInvoiceAndQueryRoutes: PropTypes.func.isRequired, |
|||
clearQueryRoutes: PropTypes.func.isRequired, |
|||
resetPayReq: PropTypes.func.isRequired, |
|||
clearSelectedChannels: PropTypes.func.isRequired, |
|||
clearSelectedPeers: PropTypes.func.isRequired, |
|||
updateSelectedPeers: PropTypes.func.isRequired, |
|||
setCurrentRoute: PropTypes.func.isRequired, |
|||
updateSelectedChannels: PropTypes.func.isRequired, |
|||
updatePayReq: PropTypes.func.isRequired, |
|||
|
|||
network: PropTypes.object.isRequired, |
|||
peers: PropTypes.object.isRequired, |
|||
|
|||
selectedPeerPubkeys: PropTypes.array.isRequired, |
|||
currentRouteChanIds: PropTypes.array.isRequired, |
|||
activeChannels: PropTypes.array.isRequired, |
|||
selectedChannelIds: PropTypes.array.isRequired, |
|||
|
|||
identity_pubkey: PropTypes.string.isRequired, |
|||
|
|||
payReqIsLn: PropTypes.bool.isRequired |
|||
} |
|||
|
|||
export default Network |
@ -1,99 +0,0 @@ |
|||
@import '../../../variables.scss'; |
|||
|
|||
@keyframes dash { |
|||
to { |
|||
stroke-dashoffset: 1000; |
|||
} |
|||
} |
|||
|
|||
@keyframes fadein { |
|||
0% { background: $white; } |
|||
50% { background: lighten($secondary, 50%); } |
|||
100% { background: $secondary; animation-fill-mode:forwards; } |
|||
} |
|||
|
|||
.container { |
|||
width: 100%; |
|||
height: 100vh; |
|||
animation: fadein 0.5s; |
|||
animation-timing-function:linear; |
|||
animation-fill-mode:forwards; |
|||
animation-iteration-count: 1; |
|||
|
|||
line.active { |
|||
opacity: 1; |
|||
stroke: green; |
|||
stroke-width: 5; |
|||
stroke-dasharray: 100; |
|||
animation: dash 2.5s infinite linear; |
|||
} |
|||
|
|||
circle { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.stats { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 30%; |
|||
padding: 20px; |
|||
|
|||
span { |
|||
color: $main; |
|||
margin: 0 2.5px; |
|||
line-height: 25px; |
|||
vertical-align: middle; |
|||
|
|||
&:nth-child(2) { |
|||
font-size: 25px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.network, .toolbox { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.network { |
|||
width: 70%; |
|||
} |
|||
|
|||
.toolbox { |
|||
width: 30%; |
|||
height: 100%; |
|||
background: #353535; |
|||
overflow-y: scroll; |
|||
} |
|||
|
|||
.tabs { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding-top: 40px; |
|||
|
|||
.tab { |
|||
color: $white; |
|||
text-align: center; |
|||
cursor: pointer; |
|||
width: 100%; |
|||
padding: 10px 0; |
|||
border-bottom: 1px solid #464646; |
|||
transition: all 0.5s; |
|||
|
|||
&.peersTab:hover, &.peersTab.active { |
|||
border-bottom: 1px solid #588CF0; |
|||
} |
|||
|
|||
&.channelsTab:hover, &.channelsTab.active { |
|||
border-bottom: 1px solid #88D4A2; |
|||
} |
|||
|
|||
&.transactionsTab:hover, &.transactionsTab.active { |
|||
border-bottom: 1px solid #FFDC53; |
|||
} |
|||
} |
|||
} |
@ -1,62 +0,0 @@ |
|||
import { withRouter } from 'react-router' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { |
|||
fetchDescribeNetwork, |
|||
|
|||
setCurrentTab, |
|||
|
|||
updateSelectedPeers, |
|||
clearSelectedPeers, |
|||
|
|||
updateSelectedChannels, |
|||
clearSelectedChannels, |
|||
|
|||
setCurrentRoute, |
|||
|
|||
updatePayReq, |
|||
resetPayReq, |
|||
|
|||
fetchInvoiceAndQueryRoutes, |
|||
clearQueryRoutes, |
|||
|
|||
networkSelectors |
|||
} from '../../../reducers/network' |
|||
import { fetchPeers } from '../../../reducers/peers' |
|||
import { fetchChannels, channelsSelectors } from '../../../reducers/channels' |
|||
|
|||
import Network from '../components/Network' |
|||
|
|||
const mapDispatchToProps = { |
|||
fetchDescribeNetwork, |
|||
setCurrentTab, |
|||
|
|||
updateSelectedPeers, |
|||
clearSelectedPeers, |
|||
|
|||
updatePayReq, |
|||
fetchInvoiceAndQueryRoutes, |
|||
setCurrentRoute, |
|||
clearQueryRoutes, |
|||
resetPayReq, |
|||
|
|||
fetchPeers, |
|||
|
|||
fetchChannels, |
|||
updateSelectedChannels, |
|||
clearSelectedChannels |
|||
} |
|||
|
|||
const mapStateToProps = state => ({ |
|||
network: state.network, |
|||
peers: state.peers, |
|||
identity_pubkey: state.info.data.identity_pubkey, |
|||
|
|||
selectedPeerPubkeys: networkSelectors.selectedPeerPubkeys(state), |
|||
selectedChannelIds: networkSelectors.selectedChannelIds(state), |
|||
payReqIsLn: networkSelectors.payReqIsLn(state), |
|||
currentRouteChanIds: networkSelectors.currentRouteChanIds(state), |
|||
activeChannels: channelsSelectors.activeChannels(state) |
|||
}) |
|||
|
|||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Network)) |
@ -1,3 +0,0 @@ |
|||
import NetworkContainer from './containers/NetworkContainer' |
|||
|
|||
export default NetworkContainer |
After Width: | Height: | Size: 263 KiB |
After Width: | Height: | Size: 24 KiB |
@ -1,31 +0,0 @@ |
|||
import React from 'react' |
|||
import { shallow } from 'enzyme' |
|||
import { NavLink } from 'react-router-dom' |
|||
import Nav from 'components/Nav' |
|||
|
|||
const defaultProps = { |
|||
ticker: { |
|||
currency: 'usd', |
|||
crypto: 'btc' |
|||
}, |
|||
balance: {}, |
|||
setCurrency: () => {}, |
|||
currentTicker: {}, |
|||
openPayForm: () => {}, |
|||
openRequestForm: () => {} |
|||
} |
|||
|
|||
describe('default elements', () => { |
|||
const props = { ...defaultProps } |
|||
const el = shallow(<Nav {...props} />) |
|||
|
|||
it('should render nav links', () => { |
|||
expect(el.find(NavLink).at(0).props().to).toBe('/') |
|||
expect(el.find(NavLink).at(1).props().to).toBe('/contacts') |
|||
expect(el.find(NavLink).at(2).props().to).toBe('/network') |
|||
}) |
|||
it('should render buttons', () => { |
|||
expect(el.find('.button').at(0).text()).toContain('Pay') |
|||
expect(el.find('.button').at(1).text()).toContain('Request') |
|||
}) |
|||
}) |