Browse Source

Merge pull request #289 from LN-Zap/feature/v0.2.0-design

Feature/v0.2.0 design
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
067e7a6de6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 38
      app/components/Contacts/ClosingContact.js
  2. 156
      app/components/Contacts/Contact.scss
  3. 2
      app/components/Contacts/ContactModal.js
  4. 2
      app/components/Contacts/ContactsForm.scss
  5. 35
      app/components/Contacts/LoadingContact.js
  6. 186
      app/components/Contacts/Network.js
  7. 179
      app/components/Contacts/Network.scss
  8. 34
      app/components/Contacts/OfflineContact.js
  9. 34
      app/components/Contacts/OnlineContact.js
  10. 0
      app/components/Contacts/OnlineContact.scss
  11. 38
      app/components/Contacts/PendingContact.js
  12. 2
      app/components/LoadingBolt/LoadingBolt.scss
  13. 61
      app/components/Nav/Nav.js
  14. 188
      app/components/Nav/Nav.scss
  15. 3
      app/components/Nav/index.js
  16. 274
      app/components/Network/CanvasNetworkGraph.js
  17. 156
      app/components/Network/CanvasNetworkGraph.scss
  18. 45
      app/components/Network/ChannelsList.js
  19. 78
      app/components/Network/ChannelsList.scss
  20. 25
      app/components/Network/PeersList.js
  21. 46
      app/components/Network/PeersList.scss
  22. 63
      app/components/Network/TransactionForm.js
  23. 125
      app/components/Network/TransactionForm.scss
  24. 2
      app/components/Wallet/ReceiveModal.js
  25. 50
      app/components/Wallet/Wallet.js
  26. 110
      app/components/Wallet/Wallet.scss
  27. 16
      app/icons/bitcoin.svg
  28. 1
      app/icons/check_circle.svg
  29. 1
      app/icons/clock.svg
  30. 27
      app/icons/qrcode.svg
  31. 1
      app/icons/search.svg
  32. 16
      app/icons/zap_logo.svg
  33. 3
      app/main.dev.js
  34. 50
      app/reducers/activity.js
  35. 12
      app/reducers/channels.js
  36. 6
      app/routes.js
  37. 53
      app/routes/activity/components/Activity.js
  38. 100
      app/routes/activity/components/Activity.scss
  39. 79
      app/routes/activity/components/components/Activity.scss
  40. 36
      app/routes/activity/components/components/Invoice/Invoice.js
  41. 2
      app/routes/activity/components/components/Modal/Modal.js
  42. 16
      app/routes/activity/components/components/Payment/Payment.js
  43. 16
      app/routes/activity/components/components/Transaction/Transaction.js
  44. 17
      app/routes/activity/containers/ActivityContainer.js
  45. 47
      app/routes/app/components/App.js
  46. 3
      app/routes/app/components/App.scss
  47. 135
      app/routes/app/containers/AppContainer.js
  48. 196
      app/routes/contacts/components/Contacts.js
  49. 172
      app/routes/contacts/components/Contacts.scss
  50. 115
      app/routes/contacts/containers/ContactsContainer.js
  51. 3
      app/routes/contacts/index.js
  52. 88
      app/routes/help/components/Help.js
  53. 90
      app/routes/help/components/Help.scss
  54. 10
      app/routes/help/containers/HelpContainer.js
  55. 3
      app/routes/help/index.js
  56. 168
      app/routes/network/components/Network.js
  57. 99
      app/routes/network/components/Network.scss
  58. 62
      app/routes/network/containers/NetworkContainer.js
  59. 3
      app/routes/network/index.js
  60. 3
      app/utils/btc.js
  61. 5
      app/variables.scss
  62. BIN
      resources/icon.icns
  63. BIN
      resources/icon.ico
  64. 30
      test/components/Nav.spec.js
  65. 72
      test/reducers/__snapshots__/channels.spec.js.snap

38
app/components/Contacts/ClosingContact.js

@ -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

156
app/components/Contacts/Contact.scss

@ -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;
}

2
app/components/Contacts/ContactModal.js

@ -26,7 +26,7 @@ const ContactModal = ({
},
content: {
top: 'auto',
left: '20%',
left: '0',
right: '0',
bottom: 'auto',
width: '40%',

2
app/components/Contacts/ContactsForm.scss

@ -6,7 +6,7 @@
margin: 50px auto;
position: absolute;
top: auto;
left: 20%;
left: 0;
right: 0;
bottom: auto;
background: $white;

35
app/components/Contacts/LoadingContact.js

@ -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

186
app/components/Contacts/Network.js

@ -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

179
app/components/Contacts/Network.scss

@ -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;
}
}

34
app/components/Contacts/OfflineContact.js

@ -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

34
app/components/Contacts/OnlineContact.js

@ -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

0
app/components/Contacts/OnlineContact.scss

38
app/components/Contacts/PendingContact.js

@ -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

2
app/components/LoadingBolt/LoadingBolt.scss

@ -7,7 +7,7 @@
left: 0;
width: 100%;
height: 100%;
background: $white;
background: $spaceblue;
text-align: center;
}

61
app/components/Nav/Nav.js

@ -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

188
app/components/Nav/Nav.scss

@ -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%;
}

3
app/components/Nav/index.js

@ -1,3 +0,0 @@
import Nav from './Nav'
export default Nav

274
app/components/Network/CanvasNetworkGraph.js

@ -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

156
app/components/Network/CanvasNetworkGraph.scss

@ -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;}
}

45
app/components/Network/ChannelsList.js

@ -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

78
app/components/Network/ChannelsList.scss

@ -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;
}
}

25
app/components/Network/PeersList.js

@ -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

46
app/components/Network/PeersList.scss

@ -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;
}
}

63
app/components/Network/TransactionForm.js

@ -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

125
app/components/Network/TransactionForm.scss

@ -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;
}
}
}
}

2
app/components/Wallet/ReceiveModal.js

@ -17,7 +17,7 @@ const ReceiveModal = ({
},
content: {
top: 'auto',
left: '20%',
left: '0',
right: '0',
bottom: 'auto',
width: '40%',

50
app/components/Wallet/Wallet.js

@ -1,9 +1,11 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FaQrcode } from 'react-icons/lib/fa'
import { FaAngleDown } from 'react-icons/lib/fa'
import Isvg from 'react-inlinesvg'
import { btc } from 'utils'
import skinnyBitcoinIcon from 'icons/skinny_bitcoin.svg'
import bitcoinIcon from 'icons/bitcoin.svg'
import zapLogo from 'icons/zap_logo.svg'
import qrCode from 'icons/qrcode.svg'
import ReceiveModal from './ReceiveModal'
import styles from './Wallet.scss'
@ -23,10 +25,14 @@ class Wallet extends Component {
balance,
address,
info,
newAddress
newAddress,
currentTicker,
openPayForm,
openRequestForm
} = this.props
const { modalOpen, qrCodeType } = this.state
const usdAmount = parseFloat(btc.satoshisToUsd(balance.walletBalance, currentTicker.price_usd))
const changeQrCode = () => {
const qrCodeNum = this.state.qrCodeType === 1 ? 2 : 1
@ -51,22 +57,39 @@ class Wallet extends Component {
)
}
<div className={styles.content}>
<header className={styles.header}>
<section className={styles.logo}>
<Isvg className={styles.bitcoinLogo} src={zapLogo} />
</section>
<section className={styles.user}>
<div>
<span>{info.data.alias}</span>
<FaAngleDown />
</div>
</section>
</header>
<div className={styles.left}>
<div className={styles.leftContent}>
<Isvg className={styles.bitcoinLogo} src={skinnyBitcoinIcon} />
<Isvg className={styles.bitcoinLogo} src={bitcoinIcon} />
<div className={styles.details}>
<h1>{btc.satoshisToBtc(parseFloat(balance.walletBalance) + parseFloat(balance.channelBalance))} BTC</h1>
<span>{btc.satoshisToBtc(balance.walletBalance)} available</span>
<span>{btc.satoshisToBtc(balance.channelBalance)} in channels</span>
<h1>
<span>
{btc.satoshisToBtc(parseFloat(balance.walletBalance) + parseFloat(balance.channelBalance))}BTC
</span>
<span onClick={() => this.setState({ modalOpen: true })}>
<Isvg className={styles.bitcoinLogo} src={qrCode} />
</span>
</h1>
<span className={styles.usdValue}> ${usdAmount ? usdAmount.toLocaleString() : ''}</span>
</div>
</div>
</div>
<div className={styles.right}>
<div className={styles.rightContent}>
<div className={`buttonPrimary ${styles.addressButton}`} onClick={() => this.setState({ modalOpen: true })}>
<FaQrcode />
Address
</div>
<div className={styles.pay} onClick={openPayForm}>Pay</div>
<div className={styles.request} onClick={openRequestForm}>Request</div>
</div>
</div>
</div>
@ -79,7 +102,10 @@ Wallet.propTypes = {
balance: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
info: PropTypes.object.isRequired,
newAddress: PropTypes.func.isRequired
newAddress: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired
}
export default Wallet

110
app/components/Wallet/Wallet.scss

@ -1,10 +1,31 @@
@import '../../variables.scss';
.wallet {
cursor: pointer;
background: $lightgrey;
background: $bluegrey;
color: $white;
transition: background 0.25s;
height: 150px;
padding: 20px 40px;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
.logo span svg {
width: 64px;
height: 24px;
}
.user {
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
}
}
.left, .right {
@ -14,7 +35,7 @@
height: 150px;
.leftContent, .rightContent {
padding: 25px;
padding: 25px 0;
}
}
@ -23,8 +44,8 @@
flex-direction: row;
.bitcoinLogo svg {
width: 100px;
height: 100px;
width: 32px;
height: 32px;
}
.details {
@ -33,14 +54,37 @@
justify-content: center;
h1 {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
letter-spacing: 1.5px;
display: flex;
flex-direction: row;
span:nth-child(1) {
font-size: 24px;
line-height: 32px;
font-weight: 500;
margin-left: 10px;
margin-bottom: 5px;
letter-spacing: 1.5px;
}
span:nth-child(2) svg {
color: $white;
width: 20px;
height: 32px;
opacity: 1;
margin-left: 5px;
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
}
}
span {
margin: 2.5px 0;
.usdValue {
font-size: 12px;
margin-left: 10px;
font-style: italic;
}
}
@ -56,42 +100,24 @@
justify-content: flex-end;
align-items: right;
.addressButton {
box-shadow: none;
.pay, .request {
font-size: 16px;
font-weight: bold;
color: $white;
background: $spaceblue;
padding: 15px;
width: 100px;
text-align: center;
border-radius: 5px;
cursor: pointer;
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;
}
}
}
div {
padding: 7px 20px;
background: $main;
transition: background 0.25s;
color: $black;
&:hover {
background: darken($main, 10%);
opacity: 0.5;
}
svg {
font-size: 35px;
margin-right: 10px;
&:nth-child(1) {
margin-right: 20px;
}
}
}

16
app/icons/bitcoin.svg

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="33px" viewBox="0 0 32 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Bitcoin</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Pay-Hover-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-56.000000, -92.000000)">
<g id="Group" transform="translate(56.000000, 90.000000)" fill="#F5B300">
<g id="Bitcoin" transform="translate(0.000000, 2.000000)">
<g id="btc">
<path d="M15.85,32.1 C7.09628672,32.1 0,25.0037133 0,16.25 C0,7.49628672 7.09628672,0.4 15.85,0.4 C24.6037133,0.4 31.7,7.49628672 31.7,16.25 C31.7,25.0037133 24.6037133,32.1 15.85,32.1 Z M11.55,8.15 C11.5,8.2 11.5,8.2 11.7,8.05 C11.55,8.6 11.4,9.2 11.25,9.75 C11.2,9.85 11.25,9.9 11.35,9.9 C11.75,10 12.2,10.1 12.65,10.25 C13.1,10.4 13.35,10.85 13.25,11.25 C12.65,13.85 12,16.4 11.35,18.95 C11.3,19.3 11,19.5 10.6,19.4 C10.25,19.35 9.85,19.25 9.5,19.15 C9.35,19.15 9.3,19.15 9.25,19.3 C9.1,19.7 8.9,20.15 8.7,20.6 C8.6,20.8 8.55,20.95 8.45,21.15 C9.65,21.45 10.8,21.75 11.95,22.05 C11.95,22.15 11.9,22.2 11.9,22.25 C11.65,23.05 11.45,23.9 11.25,24.75 C11.2,24.85 11.2,24.9 11.35,24.95 C11.85,25.05 12.35,25.15 12.85,25.3 C13,25.35 13,25.3 13.05,25.2 C13.15,24.75 13.3,24.3 13.4,23.85 C13.5,23.4 13.65,22.95 13.75,22.5 C14.2,22.6 14.6,22.7 15,22.8 C15.1,22.85 15.05,22.9 15.05,23 C14.8,23.8 14.6,24.65 14.4,25.5 C14.35,25.6 14.35,25.65 14.5,25.7 C14.95,25.8 15.45,25.95 15.9,26.05 C16.15,26.15 16.15,26.1 16.2,25.9 C16.4,25.1 16.6,24.25 16.8,23.45 C16.85,23.3 16.95,23.25 17.1,23.3 C18.1,23.45 19.1,23.55 20.15,23.4 C20.95,23.3 21.65,22.95 22.2,22.3 C22.65,21.75 22.9,21.15 23.1,20.45 C23.25,19.9 23.3,19.3 23.2,18.75 C23,18.05 22.6,17.5 22,17.05 C21.8,16.9 21.6,16.8 21.4,16.65 C21.5,16.65 21.55,16.6 21.6,16.6 C22,16.45 22.4,16.25 22.7,15.95 C23.1,15.55 23.3,15.05 23.45,14.5 C23.65,13.7 23.6,12.9 23.15,12.15 C22.8,11.65 22.35,11.25 21.85,10.95 C21.3,10.7 20.75,10.45 20.2,10.2 C20.05,10.15 20,10.1 20.05,9.95 C20.15,9.7 20.2,9.45 20.25,9.2 C20.4,8.6 20.55,8 20.7,7.35 C20.65,7.35 20.65,7.3 20.6,7.3 C20.05,7.15 19.55,7.05 19.05,6.9 C18.95,6.85 18.9,6.9 18.9,7 C18.85,7.2 18.8,7.4 18.75,7.6 C18.55,8.25 18.4,8.85 18.25,9.5 C18.2,9.55 18.15,9.65 18.1,9.65 C17.7,9.55 17.25,9.45 16.85,9.35 C16.9,9.25 16.9,9.2 16.9,9.15 C17.1,8.35 17.3,7.5 17.5,6.7 C17.55,6.6 17.55,6.55 17.4,6.5 C16.9,6.4 16.45,6.3 15.95,6.15 C15.8,6.1 15.75,6.15 15.7,6.3 C15.5,7.15 15.3,7.95 15.1,8.8 C15.05,8.95 15,9 14.85,8.95 C14.3,8.8 13.75,8.7 13.2,8.55 C12.65,8.4 12.1,8.3 11.55,8.15 Z M14.1,20.5 C14.3,19.75 14.45,19.05 14.65,18.35 C14.75,17.85 14.9,17.4 15,16.9 C15.05,16.75 15.05,16.7 15.2,16.75 C16.2,16.95 17.25,17.15 18.15,17.65 C18.55,17.9 18.95,18.15 19.2,18.55 C19.75,19.35 19.35,20.6 18.4,20.9 C17.85,21.1 17.3,21.1 16.7,21.05 C15.85,21 15,20.75 14.2,20.55 C14.2,20.55 14.15,20.5 14.1,20.5 Z M18.3,15.4 C17.5,15.4 16.85,15.25 16.1,15.15 C15.9,15.1 15.75,15.05 15.55,15 C15.45,14.95 15.4,14.9 15.45,14.8 C15.75,13.75 16,12.75 16.25,11.7 C16.3,11.55 16.35,11.5 16.5,11.55 C17.25,11.75 17.95,11.9 18.65,12.2 C19.1,12.4 19.55,12.7 19.8,13.15 C20.3,14 19.8,15.1 18.85,15.3 C18.65,15.35 18.4,15.4 18.3,15.4 Z" id="Combined-Shape" fill-rule="nonzero"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
app/icons/check_circle.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-circle"><path d="M22 11.07V12a10 10 0 1 1-5.93-9.14"></path><polyline points="23 3 12 14 9 11"></polyline></svg>

After

Width:  |  Height:  |  Size: 322 B

1
app/icons/clock.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 15 15"></polyline></svg>

After

Width:  |  Height:  |  Size: 304 B

27
app/icons/qrcode.svg

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Group 23</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="v1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-276.000000, -96.000000)" opacity="0.5">
<g id="Group-23" transform="translate(276.000000, 96.000000)" fill="#FFFFFF" fill-rule="nonzero">
<g id="Group-22">
<g id="Group">
<path d="M0.4,4.80896 C0.17904,4.80896 0,4.62992 0,4.40896 L0,0.4 C0,0.17904 0.17904,0 0.4,0 L4.40896,0 C4.62992,0 4.80896,0.17904 4.80896,0.4 C4.80896,0.62096 4.62992,0.8 4.40896,0.8 L0.8,0.8 L0.8,4.40896 C0.8,4.62992 0.62096,4.80896 0.4,4.80896 Z" id="Shape"></path>
<path d="M15.6,16 L11.59088,16 C11.36992,16 11.19088,15.82096 11.19088,15.6 C11.19088,15.37904 11.36992,15.2 11.59088,15.2 L15.2,15.2 L15.2,11.59088 C15.2,11.36992 15.37904,11.19088 15.6,11.19088 C15.82096,11.19088 16,11.36992 16,11.59088 L16,15.6 C16,15.82096 15.82096,16 15.6,16 Z" id="Shape"></path>
</g>
<g id="Group">
<path d="M15.6,4.80896 C15.37904,4.80896 15.2,4.62992 15.2,4.40896 L15.2,0.8 L11.59088,0.8 C11.36992,0.8 11.19088,0.62096 11.19088,0.4 C11.19088,0.17904 11.36992,0 11.59088,0 L15.6,0 C15.82096,0 16,0.17904 16,0.4 L16,4.40896 C16,4.62992 15.82096,4.80896 15.6,4.80896 Z" id="Shape"></path>
<path d="M4.40896,16 L0.4,16 C0.17904,16 0,15.82096 0,15.6 L0,11.59088 C0,11.36992 0.17904,11.19088 0.4,11.19088 C0.62096,11.19088 0.8,11.36992 0.8,11.59088 L0.8,15.2 L4.40896,15.2 C4.62992,15.2 4.80896,15.37904 4.80896,15.6 C4.80896,15.82096 4.62992,16 4.40896,16 Z" id="Shape"></path>
</g>
<path d="M11.5875,11.9803571 L7.6,11.9803571 C7.38298571,11.9803571 7.20714286,11.8045143 7.20714286,11.5875 L7.20714286,7.99285714 L3.6125,7.99285714 C3.39548571,7.99285714 3.21964286,7.81701429 3.21964286,7.6 C3.21964286,7.38298571 3.39548571,7.20714286 3.6125,7.20714286 L7.6,7.20714286 C7.81701429,7.20714286 7.99285714,7.38298571 7.99285714,7.6 L7.99285714,11.1946429 L11.5875,11.1946429 C11.8045143,11.1946429 11.9803571,11.3704857 11.9803571,11.5875 C11.9803571,11.8045143 11.8045143,11.9803571 11.5875,11.9803571 Z" id="Shape"></path>
<path d="M11.5875,9.87857143 C11.3704857,9.87857143 11.1946429,9.70272857 11.1946429,9.48571429 L11.1946429,7.99285714 L9.87857143,7.99285714 L9.87857143,9.48571429 C9.87857143,9.70272857 9.70272857,9.87857143 9.48571429,9.87857143 C9.2687,9.87857143 9.09285714,9.70272857 9.09285714,9.48571429 L9.09285714,7.6 C9.09285714,7.38298571 9.2687,7.20714286 9.48571429,7.20714286 L11.5875,7.20714286 C11.8045143,7.20714286 11.9803571,7.38298571 11.9803571,7.6 L11.9803571,9.48571429 C11.9803571,9.70272857 11.8045143,9.87857143 11.5875,9.87857143 Z" id="Shape"></path>
<path d="M5.71428571,6.10714286 L3.6125,6.10714286 C3.39548571,6.10714286 3.21964286,5.9313 3.21964286,5.71428571 L3.21964286,3.6125 C3.21964286,3.39548571 3.39548571,3.21964286 3.6125,3.21964286 L5.71428571,3.21964286 C5.9313,3.21964286 6.10714286,3.39548571 6.10714286,3.6125 L6.10714286,5.71428571 C6.10714286,5.9313 5.9313,6.10714286 5.71428571,6.10714286 Z M4.00535714,5.32142857 L5.32142857,5.32142857 L5.32142857,4.00535714 L4.00535714,4.00535714 L4.00535714,5.32142857 Z" id="Shape"></path>
<path d="M11.5875,6.10714286 L9.48571429,6.10714286 C9.2687,6.10714286 9.09285714,5.9313 9.09285714,5.71428571 L9.09285714,3.6125 C9.09285714,3.39548571 9.2687,3.21964286 9.48571429,3.21964286 L11.5875,3.21964286 C11.8045143,3.21964286 11.9803571,3.39548571 11.9803571,3.6125 L11.9803571,5.71428571 C11.9803571,5.9313 11.8045143,6.10714286 11.5875,6.10714286 Z M9.87857143,5.32142857 L11.1946429,5.32142857 L11.1946429,4.00535714 L9.87857143,4.00535714 L9.87857143,5.32142857 Z" id="Shape"></path>
<path d="M5.71428571,11.9803571 L3.6125,11.9803571 C3.39548571,11.9803571 3.21964286,11.8045143 3.21964286,11.5875 L3.21964286,9.48571429 C3.21964286,9.2687 3.39548571,9.09285714 3.6125,9.09285714 L5.71428571,9.09285714 C5.9313,9.09285714 6.10714286,9.2687 6.10714286,9.48571429 L6.10714286,11.5875 C6.10714286,11.8045143 5.9313,11.9803571 5.71428571,11.9803571 Z M4.00535714,11.1946429 L5.32142857,11.1946429 L5.32142857,9.87857143 L4.00535714,9.87857143 L4.00535714,11.1946429 Z" id="Shape"></path>
<path d="M7.6,6.10714286 C7.38298571,6.10714286 7.20714286,5.9313 7.20714286,5.71428571 L7.20714286,3.6125 C7.20714286,3.39548571 7.38298571,3.21964286 7.6,3.21964286 C7.81701429,3.21964286 7.99285714,3.39548571 7.99285714,3.6125 L7.99285714,5.71428571 C7.99285714,5.9313 7.81701429,6.10714286 7.6,6.10714286 Z" id="Shape"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

1
app/icons/search.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="10.5" cy="10.5" r="7.5"></circle><line x1="21" y1="21" x2="15.8" y2="15.8"></line></svg>

After

Width:  |  Height:  |  Size: 312 B

16
app/icons/zap_logo.svg

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="60px" height="21px" viewBox="0 0 60 21" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Group 5</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Pay-Hover-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-56.000000, -23.000000)">
<g id="Group-5" transform="translate(56.000000, 20.000000)">
<path d="M29.5332033,18.0136719 L36.3984377,18.0136719 L36.3984377,19 L28.0878908,19 L28.0878908,18.140625 L34.4941408,9.43945312 L28.2832033,9.43945312 L28.2832033,8.43359375 L35.9687502,8.43359375 L35.9687502,9.30273438 L29.5332033,18.0136719 Z M46.1582032,19 C46.0410151,18.6679671 45.9661461,18.1764355 45.9335938,17.5253906 C45.5234355,18.0592475 45.0009798,18.4710272 44.366211,18.7607422 C43.7314422,19.0504572 43.0592484,19.1953125 42.3496094,19.1953125 C41.3339794,19.1953125 40.5120475,18.9121122 39.8837891,18.3457031 C39.2555308,17.779294 38.9414063,17.0631554 38.9414063,16.1972656 C38.9414063,15.1686146 39.3694619,14.3548207 40.225586,13.7558594 C41.0817101,13.156898 42.274732,12.8574219 43.8046876,12.8574219 L45.9238282,12.8574219 L45.9238282,11.65625 C45.9238282,10.9010379 45.6910831,10.3069683 45.225586,9.87402344 C44.7600889,9.44107856 44.0813847,9.22460938 43.1894532,9.22460938 C42.375647,9.22460938 41.7018256,9.43294063 41.1679688,9.84960938 C40.634112,10.2662781 40.3671876,10.7675752 40.3671876,11.3535156 L39.1953126,11.34375 C39.1953126,10.5039021 39.5859337,9.77637026 40.3671876,9.16113281 C41.1484415,8.54589536 42.1087183,8.23828125 43.2480469,8.23828125 C44.4264382,8.23828125 45.3557909,8.53287466 46.0361329,9.12207031 C46.7164748,9.71126597 47.0664062,10.5331979 47.0859376,11.5878906 L47.0859376,16.5878906 C47.0859376,17.6100312 47.1933584,18.3749975 47.4082032,18.8828125 L47.4082032,19 L46.1582032,19 Z M42.4863282,18.1601562 C43.2675821,18.1601562 43.9658173,17.9713561 44.5810547,17.59375 C45.1962922,17.2161439 45.6438789,16.7115917 45.9238282,16.0800781 L45.9238282,13.7558594 L43.8339844,13.7558594 C42.668614,13.7688803 41.7571648,13.9820943 41.0996094,14.3955078 C40.4420541,14.8089213 40.1132813,15.3769495 40.1132813,16.0996094 C40.1132813,16.6920603 40.3330057,17.1835918 40.772461,17.5742188 C41.2119163,17.9648457 41.7831997,18.1601562 42.4863282,18.1601562 Z M59.6972656,13.8242188 C59.6972656,15.4713624 59.3326859,16.7783155 58.6035156,17.7451172 C57.8743453,18.7119189 56.8977925,19.1953125 55.6738281,19.1953125 C54.2285083,19.1953125 53.1217486,18.6875051 52.3535156,17.671875 L52.3535156,23.0625 L51.1914062,23.0625 L51.1914062,8.43359375 L52.2753906,8.43359375 L52.3339843,9.92773438 C53.0957069,8.80142666 54.1992115,8.23828125 55.6445312,8.23828125 C56.9075583,8.23828125 57.8987594,8.71679209 58.618164,9.67382812 C59.3375686,10.6308642 59.6972656,11.9589759 59.6972656,13.6582031 L59.6972656,13.8242188 Z M58.5253906,13.6191406 C58.5253906,12.2714776 58.2487006,11.2070352 57.6953124,10.4257812 C57.1419243,9.64452734 56.3704476,9.25390625 55.3808593,9.25390625 C54.6647099,9.25390625 54.0494817,9.42643057 53.5351562,9.77148438 C53.0208307,10.1165382 52.6269544,10.6178353 52.3535156,11.2753906 L52.3535156,16.34375 C52.6334649,16.9492218 53.0338515,17.4114567 53.5546874,17.7304688 C54.0755234,18.0494808 54.6907516,18.2089844 55.4003906,18.2089844 C56.3834684,18.2089844 57.1500623,17.8167357 57.7001953,17.0322266 C58.2503282,16.2477174 58.5253906,15.1100335 58.5253906,13.6191406 Z" id="zap" fill="#FFFFFF"></path>
<g id="cloud-lightning" transform="translate(0.000000, 4.000000)" stroke-linecap="round" stroke-linejoin="round" stroke-width="0.77">
<path d="M15.0416667,12.2863636 C17.0346026,11.8913673 18.396646,10.0866143 18.1922712,8.11170759 C17.9878963,6.13680088 16.283606,4.63436876 14.25,4.63636364 L13.2525,4.63636364 C12.5303096,1.90723362 10.0081977,0.000523827862 7.12041491,0.000523855726 C4.23263213,0.000523883591 1.71052026,1.90723373 0.988329904,4.63636375 C0.26613955,7.36549378 1.52676812,10.2258816 4.05333333,11.5909091" id="Shape" stroke="#FFFFFF"></path>
<polyline id="Shape" stroke="#F5B300" points="10.2916667 7.72727273 7.125 12.3636364 11.875 12.3636364 8.70833333 17"></polyline>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

3
app/main.dev.js

@ -146,7 +146,8 @@ const startLnd = () => {
'--neutrino.connect=127.0.0.1:18333',
'--autopilot.active',
'--debuglevel=debug',
'--noencryptwallet'
'--noencryptwallet',
'--alias=jimmymow'
]
)
.on('error', error => console.log(`lnd error: ${error}`))

50
app/reducers/activity.js

@ -7,11 +7,11 @@ const initialState = {
filterPulldown: false,
filter: { key: 'ALL_ACTIVITY', name: 'All Activity' },
filters: [
{ key: 'ALL_ACTIVITY', name: 'All Activity' },
{ key: 'LN_ACTIVITY', name: 'LN Activity' },
{ key: 'PAYMENT_ACTIVITY', name: 'LN Payments' },
{ key: 'INVOICE_ACTIVITY', name: 'LN Invoices' },
{ key: 'TRANSACTION_ACTIVITY', name: 'On-chain Activity' }
{ key: 'ALL_ACTIVITY', name: 'All' },
{ key: 'SENT_ACTIVITY', name: 'Sent' },
{ key: 'REQUESTED_ACTIVITY', name: 'Requested' },
{ key: 'PENDING_ACTIVITY', name: 'Pending' },
{ key: 'FUNDED_ACTIVITY', name: 'Funding Transactions' }
],
modal: {
modalType: null,
@ -91,6 +91,7 @@ const searchSelector = state => state.activity.searchText
const paymentsSelector = state => state.payment.payments
const invoicesSelector = state => state.invoice.invoices
const transactionsSelector = state => state.transaction.transactions
const channelsSelector = state => state.channels.channels
const allActivity = createSelector(
searchSelector,
@ -117,33 +118,46 @@ const allActivity = createSelector(
}
)
const lnActivity = createSelector(
paymentsSelector,
const invoiceActivity = createSelector(
invoicesSelector,
(payments, invoices) => [...payments, ...invoices].sort((a, b) => b.creation_date - a.creation_date)
invoices => invoices
)
const paymentActivity = createSelector(
const sentActivity = createSelector(
transactionsSelector,
paymentsSelector,
payments => payments
(transactions, payments) => {
const sentTransactions = transactions.filter(transaction => transaction.amount < 0)
return [...sentTransactions, ...payments].sort((a, b) => {
const aTimestamp = Object.prototype.hasOwnProperty.call(a, 'time_stamp') ? a.time_stamp : a.creation_date
const bTimestamp = Object.prototype.hasOwnProperty.call(b, 'time_stamp') ? b.time_stamp : b.creation_date
return bTimestamp - aTimestamp
})
}
)
const invoiceActivity = createSelector(
const pendingActivity = createSelector(
invoicesSelector,
invoices => invoices
invoices => invoices.filter(invoice => !invoice.settled)
)
const transactionActivity = createSelector(
const fundedActivity = createSelector(
transactionsSelector,
transactions => transactions
channelsSelector,
(transactions, channels) => {
const fundingTxIds = channels.map(channel => channel.channel_point.split(':')[0])
return transactions.filter(transaction => fundingTxIds.includes(transaction.tx_hash))
}
)
const FILTERS = {
ALL_ACTIVITY: allActivity,
LN_ACTIVITY: lnActivity,
PAYMENT_ACTIVITY: paymentActivity,
INVOICE_ACTIVITY: invoiceActivity,
TRANSACTION_ACTIVITY: transactionActivity
SENT_ACTIVITY: sentActivity,
REQUESTED_ACTIVITY: invoiceActivity,
PENDING_ACTIVITY: pendingActivity,
FUNDED_ACTIVITY: fundedActivity
}
activitySelectors.currentActivity = createSelector(

12
app/reducers/channels.js

@ -512,13 +512,13 @@ const initialState = {
viewType: 0,
filterPulldown: false,
filter: { key: 'ALL_CHANNELS', name: 'All Contacts' },
filter: { key: 'ALL_CHANNELS', name: 'All' },
filters: [
{ key: 'ALL_CHANNELS', name: 'All Contacts' },
{ key: 'ACTIVE_CHANNELS', name: 'Online Contacts' },
{ key: 'NON_ACTIVE_CHANNELS', name: 'Offline Contacts' },
{ key: 'OPEN_PENDING_CHANNELS', name: 'Pending Contacts' },
{ key: 'CLOSING_PENDING_CHANNELS', name: 'Closing Contacts' }
{ key: 'ALL_CHANNELS', name: 'All' },
{ key: 'ACTIVE_CHANNELS', name: 'Online' },
{ key: 'NON_ACTIVE_CHANNELS', name: 'Offline' },
{ key: 'OPEN_PENDING_CHANNELS', name: 'Pending' },
{ key: 'CLOSING_PENDING_CHANNELS', name: 'Closing' }
],
loadingChannelPubkeys: [],

6
app/routes.js

@ -3,16 +3,10 @@ import React from 'react'
import { Switch, Route } from 'react-router'
import App from './routes/app'
import Activity from './routes/activity'
import Contacts from './routes/contacts'
import Network from './routes/network'
import Help from './routes/help'
const routes = () => (
<App>
<Switch>
<Route path='/contacts' component={Contacts} />
<Route path='/network' component={Network} />
<Route path='/help' component={Help} />
<Route path='/' component={Activity} />
</Switch>
</App>

53
app/routes/activity/components/Activity.js

@ -1,7 +1,5 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { MdSearch } from 'react-icons/lib/md'
import { FaAngleDown } from 'react-icons/lib/fa'
import Wallet from 'components/Wallet'
import LoadingBolt from 'components/LoadingBolt'
@ -47,20 +45,19 @@ class Activity extends Component {
render() {
const {
ticker,
updateSearchText,
invoice: { invoiceLoading },
address: { address },
balance,
info,
payment: { paymentLoading },
currentTicker,
activity: { modal, filter, filterPulldown, searchText },
activity: { modal, filters, filter, filterPulldown },
hideActivityModal,
changeFilter,
toggleFilterPulldown,
currentActivity,
nonActiveFilters,
newAddress
newAddress,
openPayForm,
openRequestForm
} = this.props
if (invoiceLoading || paymentLoading) { return <LoadingBolt /> }
@ -77,33 +74,26 @@ class Activity extends Component {
currentTicker={currentTicker}
/>
<Wallet balance={balance} address={address} info={info} newAddress={newAddress} />
<div className={styles.search}>
<label className={`${styles.label} ${styles.input}`} htmlFor='invoiceSearch'>
<MdSearch />
</label>
<input
value={searchText}
onChange={event => updateSearchText(event.target.value)}
className={`${styles.text} ${styles.input}`}
placeholder='Search by hash...'
type='text'
id='invoiceSearch'
/>
</div>
<Wallet
balance={balance}
address={address}
info={info}
newAddress={newAddress}
currentTicker={currentTicker}
openPayForm={openPayForm}
openRequestForm={openRequestForm}
/>
<div className={styles.activities}>
<header className={styles.header}>
<section>
<h2 onClick={toggleFilterPulldown}>
{filter.name} <span className={filterPulldown && styles.pulldown}><FaAngleDown /></span>
</h2>
<ul className={`${styles.filters} ${filterPulldown && styles.active}`}>
<ul className={styles.filters}>
{
nonActiveFilters.map(f => (
<li key={f.key} onClick={() => changeFilter(f)}>
{f.name}
filters.map(f => (
<li key={f.key} className={f.key === filter.key && styles.activeFilter} onClick={() => changeFilter(f)}>
<span>{f.name}</span>
<div className={f.key === filter.key && styles.activeBorder} />
</li>
))
}
@ -132,7 +122,6 @@ Activity.propTypes = {
fetchBalance: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
updateSearchText: PropTypes.func.isRequired,
invoice: PropTypes.object.isRequired,
payment: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
@ -141,11 +130,11 @@ Activity.propTypes = {
hideActivityModal: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired,
newAddress: PropTypes.func.isRequired,
toggleFilterPulldown: PropTypes.func.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired,
activity: PropTypes.object.isRequired,
currentActivity: PropTypes.array.isRequired,
nonActiveFilters: PropTypes.array.isRequired,
address: PropTypes.object.isRequired,
balance: PropTypes.object.isRequired,
info: PropTypes.object.isRequired

100
app/routes/activity/components/Activity.scss

@ -32,91 +32,67 @@
.activities {
background: $white;
height: 100%;
.header {
padding: 60px 0 20px 0;
background: $spaceblue;
color: $white;
margin: 0 auto;
padding: 0 40px;
border-bottom: 1px solid $spaceborder;
section {
position: relative;
margin-left: auto;
margin-right: auto;
padding-left: 100px;
padding-right: 100px;
max-width: 964px;
h2 {
color: $bluegrey;
}
}
h2, h2 span {
color: $bluegrey;
cursor: pointer;
transition: color 0.25s;
.filters {
display: flex;
flex-direction: row;
li {
position: relative;
margin: 0 15px;
opacity: 0.5;
font-size: 14px;
cursor: pointer;
padding: 20px 0;
&:hover {
color: lighten($bluegrey, 10%);
&.activeFilter {
opacity: 1;
}
&:nth-child(1) {
margin-left: 0;
}
}
}
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;
.activeBorder {
width: 100%;
height: 1px;
background: white;
position: absolute;
bottom: -100px;
z-index: 10;
li {
margin: 5px 0;
cursor: pointer;
&:hover {
color: $main;
}
}
}
}
bottom: -1px;
}
}
}
}
.activityContainer {
background: $white;
background: $spaceblue;
transition: opacity 0.25s;
padding-bottom: 50px;
min-height: 100vh;
height: 100vh;
overflow-y: scroll;
padding-top: 20px;
&.pulldown {
opacity: 0.15;
}
}
.activityList {
width: 75%;
margin: 0 auto;
background: $white;
}
.activity {
padding: 0 100px;
padding: 0 40px;
&:hover {
background-color: #f0f0f0;
transition-delay: 0s;
outline: $grey solid 1px;
}
.left, .center, .right {

79
app/routes/activity/components/components/Activity.scss

@ -4,15 +4,12 @@
display: flex;
flex-direction: row;
cursor: pointer;
max-width: 960px;
margin: 0 auto;
height: 76px;
align-items: center;
border-bottom: 1px solid $grey;
font-size: 14px;
transition: background-color .1s linear;
transition-delay: .1s;
color: $darkestgrey;
color: $white;
position: relative;
}
@ -37,22 +34,20 @@
justify-content: center;
align-items: center;
padding-right: 50px;
min-width: 90px;
time {
text-transform: uppercase;
section {
margin: 2.5px 0;
}
&:nth-child(1) {
display: flex;
align-items: center;
height: 38px;
font-size: 18px;
margin-bottom: 5px;
}
svg {
width: 12.5px;
height: 12.5px;
}
&:nth-child(2) {
font-size: 12px;
}
}
time {
font-size: 12px;
}
}
.data {
@ -61,6 +56,10 @@
flex: 6;
justify-content: space-evenly;
&:nth-child(2) {
font-size: 10px;
}
.title {
margin-bottom: 5px;
}
@ -70,23 +69,24 @@
}
h3, span {
font-size: 18px;
font-size: 14px;
font-weight: bold;
letter-spacing: 1.2px;
}
.icon {
display: inline-block;
flex: none;
position: relative;
width: 36px;
height: 36px;
border: 1px solid $darkestgrey;
width: 20px;
height: 20px;
background: #31343f;
border-radius: 50%;
margin-right: 15px;
margin-right: 5px;
svg {
color: $main;
font-size: 16px;
color: $white;
font-size: 10px;
vertical-align: middle;
display: flex;
top: 0;
@ -116,16 +116,33 @@
flex-direction: column;
flex: 1;
text-align: right;
font-size: 16px;
&.positive span:nth-child(1) {
font-weight: bold;
color: $main;
}
font-size: 12px;
color: $white;
span {
&:nth-child(1) {
margin-bottom: 5px;
}
&:nth-child(2) {
font-size: 12px;
font-size: 10px;
opacity: 0.5;
}
.plus, .minus {
margin-right: 2px;
}
.plus {
color: $green;
}
.minus {
color: $red;
}
}
}
.unpaid svg {
opacity: 0.5;
}

36
app/routes/activity/components/components/Invoice/Invoice.js

@ -2,31 +2,31 @@ import React from 'react'
import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import { FaBolt, FaClockO } from 'react-icons/lib/fa'
import Isvg from 'react-inlinesvg'
import { FaBolt } from 'react-icons/lib/fa'
import { btc } from 'utils'
import checkmarkIcon from 'icons/check_circle.svg'
import clockIcon from 'icons/clock.svg'
import styles from '../Activity.scss'
const Invoice = ({
invoice, ticker, currentTicker, showActivityModal
}) => (
<div className={styles.container} onClick={() => showActivityModal('INVOICE', { invoice })}>
{
!invoice.settled ?
<div className={styles.clock}>
<i className='hint--top' data-hint='Request has not been paid'>
<FaClockO />
</i>
</div>
:
null
}
<div className={styles.date}>
<Moment format='D'>
{invoice.creation_date * 1000}
</Moment>
<Moment format='MMMM'>
{invoice.creation_date * 1000}
</Moment>
<section className={!invoice.settled && styles.unpaid}>
{
invoice.settled ?
<Isvg src={checkmarkIcon} />
:
<i className='hint--top' data-hint='Request has not been paid'>
<Isvg src={clockIcon} />
</i>
}
</section>
<section>
<Moment format='MMM'>{invoice.creation_date * 1000}</Moment> <Moment format='D'>{invoice.creation_date * 1000}</Moment>
</section>
</div>
<div className={styles.data}>
<div className={styles.title}>
@ -46,7 +46,7 @@ const Invoice = ({
</div>
<div className={`${styles.amount} ${invoice.settled ? styles.positive : styles.negative}`}>
<span className='hint--top' data-hint='Invoice amount'>
+
<i className={styles.plus}>+</i>
{
ticker.currency === 'usd' ?
btc.satoshisToUsd(invoice.value, currentTicker.price_usd)

2
app/routes/activity/components/components/Modal/Modal.js

@ -24,7 +24,7 @@ const Modal = ({
},
content: {
top: 'auto',
left: '20%',
left: '0',
right: '0',
bottom: 'auto',
width: '40%',

16
app/routes/activity/components/components/Payment/Payment.js

@ -2,8 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import Isvg from 'react-inlinesvg'
import { FaBolt } from 'react-icons/lib/fa'
import { btc } from 'utils'
import checkmarkIcon from 'icons/check_circle.svg'
import styles from '../Activity.scss'
const Payment = ({
@ -11,12 +13,12 @@ const Payment = ({
}) => (
<div className={styles.container} onClick={() => showActivityModal('PAYMENT', { payment })}>
<div className={styles.date}>
<Moment format='D'>
{payment.creation_date * 1000}
</Moment>
<Moment format='MMMM'>
{payment.creation_date * 1000}
</Moment>
<section>
<Isvg src={checkmarkIcon} />
</section>
<section>
<Moment format='MMM'>{payment.creation_date * 1000}</Moment> <Moment format='D'>{payment.creation_date * 1000}</Moment>
</section>
</div>
<div className={styles.data}>
<div className={styles.title}>
@ -36,7 +38,7 @@ const Payment = ({
</div>
<div className={styles.amount}>
<span className='hint--top' data-hint='Payment amount'>
-
<i className={styles.minus}>-</i>
{
ticker.currency === 'usd' ?
btc.satoshisToUsd(payment.value, currentTicker.price_usd)

16
app/routes/activity/components/components/Transaction/Transaction.js

@ -2,8 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import Isvg from 'react-inlinesvg'
import { FaChain } from 'react-icons/lib/fa'
import { btc } from 'utils'
import checkmarkIcon from 'icons/check_circle.svg'
import styles from '../Activity.scss'
const Transaction = ({
@ -11,12 +13,12 @@ const Transaction = ({
}) => (
<div className={styles.container} onClick={() => showActivityModal('TRANSACTION', { transaction })}>
<div className={styles.date}>
<Moment format='D'>
{transaction.time_stamp * 1000}
</Moment>
<Moment format='MMMM'>
{transaction.time_stamp * 1000}
</Moment>
<section>
<Isvg src={checkmarkIcon} />
</section>
<section>
<Moment format='MMM'>{transaction.time_stamp * 1000}</Moment> <Moment format='D'>{transaction.time_stamp * 1000}</Moment>
</section>
</div>
<div className={styles.data}>
<div className={styles.title}>
@ -36,7 +38,7 @@ const Transaction = ({
</div>
<div className={`${styles.amount} ${transaction.amount > 0 ? styles.positive : styles.negative}`}>
<span className='hint--top' data-hint='Transaction amount'>
{ transaction.amount > 0 ? '+' : '' }
<i className={transaction.amount > 0 ? styles.plus : styles.minus}>{ transaction.amount > 0 ? '+' : '-' }</i>
{
ticker.currency === 'usd' ?
btc.satoshisToUsd(transaction.amount, currentTicker.price_usd)

17
app/routes/activity/containers/ActivityContainer.js

@ -21,6 +21,7 @@ import {
updateSearchText
} from 'reducers/activity'
import { newAddress } from 'reducers/address'
import { setFormType } from 'reducers/form'
import Activity from '../components/Activity'
@ -36,7 +37,8 @@ const mapDispatchToProps = {
toggleFilterPulldown,
newAddress,
fetchBalance,
updateSearchText
updateSearchText,
setFormType
}
const mapStateToProps = state => ({
@ -62,4 +64,15 @@ const mapStateToProps = state => ({
nonActiveFilters: activitySelectors.nonActiveFilters(state)
})
export default connect(mapStateToProps, mapDispatchToProps)(Activity)
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...stateProps,
...dispatchProps,
...ownProps,
// action to open the pay form
openPayForm: () => dispatchProps.setFormType('PAY_FORM'),
// action to open the request form
openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM')
})
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Activity)

47
app/routes/app/components/App.js

@ -1,19 +1,34 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import GlobalError from 'components/GlobalError'
import LoadingBolt from 'components/LoadingBolt'
import Form from 'components/Form'
import ModalRoot from 'components/ModalRoot'
import Nav from 'components/Nav'
import Network from 'components/Contacts/Network'
import ContactModal from 'components/Contacts/ContactModal'
import ContactsForm from 'components/Contacts/ContactsForm'
import styles from './App.scss'
class App extends Component {
componentWillMount() {
const { fetchTicker, fetchInfo, newAddress } = this.props
const { fetchTicker, fetchInfo, newAddress, fetchChannels, fetchBalance, fetchDescribeNetwork } = this.props
// fetch price ticker
fetchTicker()
// fetch node info
fetchInfo()
// fetch new address for wallet
newAddress('np2wkh')
// fetch nodes channels
fetchChannels()
// fetch nodes balance
fetchBalance()
// fetch LN network from nides POV
fetchDescribeNetwork()
}
render() {
@ -24,14 +39,16 @@ class App extends Component {
currentTicker,
form,
openPayForm,
openRequestForm,
formProps,
closeForm,
error: { error },
clearError,
contactModalProps,
contactsFormProps,
networkTabProps,
children
} = this.props
@ -49,16 +66,16 @@ class App extends Component {
currency={ticker.currency}
/>
<Form formType={form.formType} formProps={formProps} closeForm={closeForm} />
<ContactModal {...contactModalProps} />
<ContactsForm {...contactsFormProps} />
<Nav
openPayForm={openPayForm}
openRequestForm={openRequestForm}
/>
<Form formType={form.formType} formProps={formProps} closeForm={closeForm} />
<div className={styles.content}>
{children}
</div>
<Network {...networkTabProps} />
</div>
)
}
@ -71,17 +88,19 @@ App.propTypes = {
formProps: PropTypes.object.isRequired,
closeForm: PropTypes.func.isRequired,
error: PropTypes.object.isRequired,
currentTicker: PropTypes.object,
contactModalProps: PropTypes.object,
contactsFormProps: PropTypes.object,
networkTabProps: PropTypes.object,
newAddress: PropTypes.func.isRequired,
fetchInfo: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired,
fetchTicker: PropTypes.func.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired,
clearError: PropTypes.func.isRequired,
currentTicker: PropTypes.object,
fetchChannels: PropTypes.func.isRequired,
fetchBalance: PropTypes.func.isRequired,
fetchDescribeNetwork: PropTypes.func.isRequired,
children: PropTypes.object.isRequired
}

3
app/routes/app/components/App.scss

@ -2,8 +2,7 @@
.content {
position: relative;
width: 85%;
height: 100vh;
width: 80%;
display: inline-block;
vertical-align: top;
overflow-y: scroll;

135
app/routes/app/containers/AppContainer.js

@ -1,6 +1,8 @@
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { fetchTicker, setCurrency, tickerSelectors } from 'reducers/ticker'
import { newAddress } from 'reducers/address'
import { fetchInfo } from 'reducers/info'
@ -8,23 +10,54 @@ import { fetchInfo } from 'reducers/info'
import { showModal, hideModal } from 'reducers/modal'
import { setFormType } from 'reducers/form'
import { setPayAmount, setPayInput, updatePayErrors, payFormSelectors } from 'reducers/payform'
import { setRequestAmount, setRequestMemo } from 'reducers/requestform'
import { sendCoins } from 'reducers/transaction'
import { payInvoice } from 'reducers/payment'
import { createInvoice, fetchInvoice } from 'reducers/invoice'
import { fetchBlockHeight, lndSelectors } from 'reducers/lnd'
import { clearError } from 'reducers/error'
import {
fetchChannels,
openChannel,
closeChannel,
channelsSelectors,
currentChannels,
toggleFilterPulldown,
changeFilter,
updateChannelSearchQuery,
openContactModal,
closeContactModal
} from 'reducers/channels'
import {
openContactsForm,
closeContactsForm,
updateContactFormSearchQuery,
updateManualFormSearchQuery,
updateContactCapacity,
contactFormSelectors,
updateManualFormErrors
} from 'reducers/contactsform'
import { fetchBalance } from 'reducers/balance'
import { fetchDescribeNetwork } from 'reducers/network'
import { clearError } from 'reducers/error'
import App from '../components/App'
const mapDispatchToProps = {
fetchTicker,
setCurrency,
newAddress,
fetchInfo,
@ -48,7 +81,28 @@ const mapDispatchToProps = {
fetchInvoice,
fetchBlockHeight,
clearError
clearError,
fetchBalance,
fetchChannels,
openChannel,
closeChannel,
toggleFilterPulldown,
changeFilter,
updateChannelSearchQuery,
openContactModal,
closeContactModal,
openContactsForm,
closeContactsForm,
updateContactFormSearchQuery,
updateManualFormSearchQuery,
updateContactCapacity,
contactFormSelectors,
updateManualFormErrors,
fetchDescribeNetwork
}
const mapStateToProps = state => ({
@ -59,6 +113,10 @@ const mapStateToProps = state => ({
info: state.info,
payment: state.payment,
transaction: state.transaction,
peers: state.peers,
channels: state.channels,
contactsform: state.contactsform,
balance: state.balance,
form: state.form,
payform: state.payform,
@ -69,6 +127,8 @@ const mapStateToProps = state => ({
error: state.error,
network: state.network,
currentTicker: tickerSelectors.currentTicker(state),
isOnchain: payFormSelectors.isOnchain(state),
isLn: payFormSelectors.isLn(state),
@ -76,7 +136,18 @@ const mapStateToProps = state => ({
inputCaption: payFormSelectors.inputCaption(state),
showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state),
payFormIsValid: payFormSelectors.payFormIsValid(state),
syncPercentage: lndSelectors.syncPercentage(state)
syncPercentage: lndSelectors.syncPercentage(state),
filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state),
showManualForm: contactFormSelectors.showManualForm(state),
manualFormIsValid: contactFormSelectors.manualFormIsValid(state),
currentChannels: currentChannels(state),
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state),
nonActiveChannelPubkeys: channelsSelectors.nonActiveChannelPubkeys(state),
pendingOpenChannelPubkeys: channelsSelectors.pendingOpenChannelPubkeys(state),
nonActiveFilters: channelsSelectors.nonActiveFilters(state),
channelNodes: channelsSelectors.channelNodes(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
@ -172,20 +243,70 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
return {}
}
const networkTabProps = {
currentChannels: stateProps.currentChannels,
channels: stateProps.channels,
balance: stateProps.balance,
currentTicker: stateProps.currentTicker,
contactsform: stateProps.contactsform,
nodes: stateProps.network.nodes,
nonActiveFilters: stateProps.nonActiveFilters,
fetchChannels: dispatchProps.fetchChannels,
openContactsForm: dispatchProps.openContactsForm,
contactFormSelectors: dispatchProps.contactFormSelectors,
updateManualFormError: dispatchProps.updateManualFormErrors,
toggleFilterPulldown: dispatchProps.toggleFilterPulldown,
changeFilter: dispatchProps.changeFilter,
updateChannelSearchQuery: dispatchProps.updateChannelSearchQuery,
openContactModal: dispatchProps.openContactModal
}
const contactsFormProps = {
closeContactsForm: dispatchProps.closeContactsForm,
updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery,
updateManualFormSearchQuery: dispatchProps.updateManualFormSearchQuery,
updateContactCapacity: dispatchProps.updateContactCapacity,
openChannel: dispatchProps.openChannel,
updateManualFormErrors: dispatchProps.updateManualFormErrors,
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
}
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
}
return {
...stateProps,
...dispatchProps,
...ownProps,
// props for the network sidebar
networkTabProps,
// props for the contacts form
contactsFormProps,
// props for the contact modal
contactModalProps,
// Props to pass to the pay form
formProps: formProps(stateProps.form.formType),
// action to open the pay form
openPayForm: () => dispatchProps.setFormType('PAY_FORM'),
// action to open the request form
openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM'),
// action to close form
closeForm: () => dispatchProps.setFormType(null)
}
}

196
app/routes/contacts/components/Contacts.js

@ -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

172
app/routes/contacts/components/Contacts.scss

@ -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;
}
}

115
app/routes/contacts/containers/ContactsContainer.js

@ -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))

3
app/routes/contacts/index.js

@ -1,3 +0,0 @@
import ContactsContainer from './containers/ContactsContainer'
export default ContactsContainer

88
app/routes/help/components/Help.js

@ -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

90
app/routes/help/components/Help.scss

@ -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;
}
}
}

10
app/routes/help/containers/HelpContainer.js

@ -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))

3
app/routes/help/index.js

@ -1,3 +0,0 @@
import HelpContainer from './containers/HelpContainer'
export default HelpContainer

168
app/routes/network/components/Network.js

@ -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

99
app/routes/network/components/Network.scss

@ -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;
}
}
}

62
app/routes/network/containers/NetworkContainer.js

@ -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))

3
app/routes/network/index.js

@ -1,3 +0,0 @@
import NetworkContainer from './containers/NetworkContainer'
export default NetworkContainer

3
app/utils/btc.js

@ -9,7 +9,8 @@ export function btcToSatoshis(btc) {
export function satoshisToBtc(satoshis) {
if (satoshis === undefined || satoshis === null || satoshis === '') return null
return sb.toBitcoin(satoshis).toFixed(8)
const btcAmount = sb.toBitcoin(satoshis).toFixed(8)
return btcAmount > 0 ? btcAmount : btcAmount * -1
}
export function btcToUsd(btc, price) {

5
app/variables.scss

@ -8,12 +8,15 @@ $traditionalgrey: #cccccc;
$lightgrey: #F7F7F7;
$darkgrey: #EBEBEB;
$darkestgrey: #999999;
$bluegrey: #555459;
$bluegrey: #2A2D38;
$spacegrey: #222E2B;
$spaceblue: #252832;
$spaceborder: #404040;
$green: #0bb634;
$terminalgreen: #00FF00;
$red: #FF556A;
$blue: #007bb6;
$orange: #FF8A65;
$yellow: #FFF680;
$curve: cubic-bezier(0.650, 0.000, 0.450, 1.000);

BIN
resources/icon.icns

Binary file not shown.

BIN
resources/icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 263 KiB

30
test/components/Nav.spec.js

@ -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')
})
})

72
test/reducers/__snapshots__/channels.spec.js.snap

@ -19,29 +19,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -76,29 +76,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -136,29 +136,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -191,29 +191,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -248,29 +248,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -305,29 +305,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],

Loading…
Cancel
Save