@ -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,186 @@ |
|||
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} onClick={openContactsForm}> |
|||
<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)]}><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,179 @@ |
|||
@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; |
|||
|
|||
.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; |
|||
} |
|||
|
|||
.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; |
|||
} |
|||
|
|||
svg { |
|||
width: 5px; |
|||
height: 5px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.search { |
|||
position: absolute; |
|||
bottom: 20px; |
|||
width: calc(100% - 40px); |
|||
padding: 10px 20px; |
|||
border-top: 1px solid $darkestgrey; |
|||
|
|||
.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,61 +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_2.svg' |
|||
import peersIcon from 'icons/contacts.svg' |
|||
import networkIcon from 'icons/network.svg' |
|||
import helpIcon from 'icons/help_2.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> |
|||
<NavLink exact to='/help' activeClassName={styles.active} className={styles.link}> |
|||
<li> |
|||
<Isvg styles={{ verticalAlign: 'middle' }} src={helpIcon} /> |
|||
<span>Help</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,188 +0,0 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.nav { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
width: 15%; |
|||
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; |
|||
transition: all 0.25s; |
|||
|
|||
li { |
|||
margin: 12.5px 0; |
|||
min-width: 150px; |
|||
} |
|||
|
|||
&.active { |
|||
color: $main; |
|||
opacity: 1.0; |
|||
|
|||
svg g { |
|||
stroke: $main; |
|||
} |
|||
} |
|||
|
|||
svg { |
|||
width: 28px; |
|||
height: 28px; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
span { |
|||
margin-left: 15px; |
|||
line-height: 22px; |
|||
font-size: 18px; |
|||
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; |
|||
font-size: 0.75em; |
|||
} |
|||
} |
|||
|
|||
|
|||
.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; |
|||
} |
|||
} |
|||
} |
|||
} |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 322 B |
After Width: | Height: | Size: 304 B |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 312 B |
After Width: | Height: | Size: 4.3 KiB |
@ -1,196 +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 |
|||
|
|||
console.log('currentChannels: ', currentChannels) |
|||
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, 'confirmation_height')) { |
|||
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,115 +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, |
|||
updateManualFormSearchQuery, |
|||
updateContactCapacity, |
|||
contactFormSelectors, |
|||
updateManualFormErrors |
|||
} from 'reducers/contactsform' |
|||
|
|||
import Contacts from '../components/Contacts' |
|||
|
|||
const mapDispatchToProps = { |
|||
openContactsForm, |
|||
closeContactsForm, |
|||
openContactModal, |
|||
closeContactModal, |
|||
updateContactFormSearchQuery, |
|||
updateManualFormSearchQuery, |
|||
updateContactCapacity, |
|||
openChannel, |
|||
closeChannel, |
|||
updateChannelSearchQuery, |
|||
toggleFilterPulldown, |
|||
changeFilter, |
|||
updateManualFormErrors, |
|||
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), |
|||
manualFormIsValid: contactFormSelectors.manualFormIsValid(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, |
|||
manualFormIsValid: stateProps.manualFormIsValid |
|||
} |
|||
|
|||
const contactsFormProps = { |
|||
closeContactsForm: dispatchProps.closeContactsForm, |
|||
updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery, |
|||
updateManualFormSearchQuery: dispatchProps.updateManualFormSearchQuery, |
|||
updateContactCapacity: dispatchProps.updateContactCapacity, |
|||
openChannel: dispatchProps.openChannel, |
|||
contactsform: stateProps.contactsform, |
|||
filteredNetworkNodes: stateProps.filteredNetworkNodes, |
|||
loadingChannelPubkeys: stateProps.channels.loadingChannelPubkeys, |
|||
showManualForm: stateProps.showManualForm, |
|||
manualFormIsValid: stateProps.manualFormIsValid, |
|||
activeChannelPubkeys: stateProps.activeChannelPubkeys, |
|||
nonActiveChannelPubkeys: stateProps.nonActiveChannelPubkeys, |
|||
pendingOpenChannelPubkeys: stateProps.pendingOpenChannelPubkeys, |
|||
updateManualFormErrors: dispatchProps.updateManualFormErrors |
|||
} |
|||
|
|||
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,88 +0,0 @@ |
|||
import { shell } from 'electron' |
|||
import React, { Component } from 'react' |
|||
|
|||
import { MdSearch } from 'react-icons/lib/md' |
|||
|
|||
import styles from './Help.scss' |
|||
|
|||
class Help extends Component { |
|||
constructor(props) { |
|||
super(props) |
|||
|
|||
this.state = { |
|||
videos: [ |
|||
{ |
|||
id: '8kZq6eec49A', |
|||
title: 'Syncing and Depositing - Zap Lightning Network Wallet Tutorial (Video 1)' |
|||
}, |
|||
{ |
|||
id: 'xSiTH63fOQM', |
|||
title: 'Adding a contact - Zap Lightning Network Wallet Tutorial (Video 2)' |
|||
}, |
|||
{ |
|||
id: 'c0SLmywYDHU', |
|||
title: 'Making a Lightning Network payment - Zap Lightning Network Wallet Tutorial (Video 3)' |
|||
}, |
|||
{ |
|||
id: 'Xrx2TiiF90Q', |
|||
title: 'Receive Lightning Network payment - Zap Lightning Network Wallet Tutorial (Video 4)' |
|||
}, |
|||
{ |
|||
id: 'YfxukBHnwUM', |
|||
title: 'Network Map - Zap Lightning Network Wallet Tutorial (Video 5)' |
|||
}, |
|||
{ |
|||
id: 'NORklrrYzOg', |
|||
title: 'Using an explorer to add Zap contacts - Zap Lightning Network Wallet Tutorial (Video 6)' |
|||
} |
|||
], |
|||
searchQuery: '' |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { videos, searchQuery } = this.state |
|||
const filteredVideos = videos.filter(video => video.title.includes(searchQuery)) |
|||
|
|||
return ( |
|||
<div className={styles.helpContainer}> |
|||
<header className={styles.header}> |
|||
<h1>Video tutorials</h1> |
|||
</header> |
|||
|
|||
<div className={styles.search}> |
|||
<label className={`${styles.label} ${styles.input}`} htmlFor='helpSearch'> |
|||
<MdSearch /> |
|||
</label> |
|||
<input |
|||
value={searchQuery} |
|||
onChange={event => this.setState({ searchQuery: event.target.value })} |
|||
className={`${styles.text} ${styles.input}`} |
|||
placeholder='Search the video library...' |
|||
type='text' |
|||
id='helpSearch' |
|||
/> |
|||
</div> |
|||
|
|||
<ul className={styles.videos}> |
|||
{ |
|||
filteredVideos.map((video, index) => ( |
|||
<li key={index}> |
|||
<iframe |
|||
src={`https://www.youtube.com/embed/${video.id}`} |
|||
frameBorder='0' |
|||
title={video.id} |
|||
/> |
|||
<section className={styles.info} onClick={() => shell.openExternal(`https://www.youtube.com/watch?v=${video.id}`)}> |
|||
<h2>{video.title}</h2> |
|||
</section> |
|||
</li> |
|||
)) |
|||
} |
|||
</ul> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default Help |
@ -1,90 +0,0 @@ |
|||
@import '../../../variables.scss'; |
|||
|
|||
.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
background: $lightgrey; |
|||
padding: 20px 40px; |
|||
|
|||
h1 { |
|||
text-transform: uppercase; |
|||
font-size: 26px; |
|||
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; |
|||
} |
|||
} |
|||
|
|||
.videos { |
|||
display: flex; |
|||
flex-direction: row; |
|||
flex-wrap: wrap; |
|||
justify-content: space-around; |
|||
padding: 20px 40px; |
|||
|
|||
li { |
|||
position: relative; |
|||
width: 50%; |
|||
text-align: center; |
|||
margin: 20px 0; |
|||
width: 400px; |
|||
height: 400px; |
|||
transition: all 0.25s; |
|||
|
|||
&:hover { |
|||
opacity: 0.5; |
|||
|
|||
.info { |
|||
padding: 50px 15px; |
|||
} |
|||
} |
|||
|
|||
iframe { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.info { |
|||
position: absolute; |
|||
bottom: 0; |
|||
width: calc(100% - 30px); |
|||
padding: 15px; |
|||
background: $darkgrey; |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
text-align: left; |
|||
line-height: 1.5; |
|||
} |
|||
} |
|||
} |
@ -1,10 +0,0 @@ |
|||
import { withRouter } from 'react-router' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import Help from '../components/Help' |
|||
|
|||
const mapDispatchToProps = {} |
|||
|
|||
const mapStateToProps = () => ({}) |
|||
|
|||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Help)) |
@ -1,3 +0,0 @@ |
|||
import HelpContainer from './containers/HelpContainer' |
|||
|
|||
export default HelpContainer |
@ -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 |
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 263 KiB |
@ -1,30 +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') |
|||
}) |
|||
it('should render buttons', () => { |
|||
expect(el.find('.button').at(0).text()).toContain('Pay') |
|||
expect(el.find('.button').at(1).text()).toContain('Request') |
|||
}) |
|||
}) |