Jack Mallers
7 years ago
20 changed files with 890 additions and 95 deletions
@ -0,0 +1,38 @@ |
|||
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 ClosingContact = ({ channel }) => ( |
|||
<li className={styles.friend} key={index}> |
|||
<section className={styles.info}> |
|||
<p className={styles.closing}> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> |
|||
<span> |
|||
Removing |
|||
<i onClick={() => shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${channel.closing_txid}`)}> |
|||
(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 = { |
|||
|
|||
} |
|||
|
|||
export default ClosingContact |
@ -0,0 +1,102 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.friend { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
padding: 30px 0; |
|||
border-bottom: 1px solid $traditionalgrey; |
|||
|
|||
.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; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,31 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import styles from './Donut.scss' |
|||
|
|||
const Donut = ({ value, size, strokewidth }) => { |
|||
console.log('value: ', value) |
|||
console.log('size: ', size) |
|||
console.log('strokewidth: ', strokewidth) |
|||
|
|||
const halfsize = (size * 0.5); |
|||
const radius = halfsize - (strokewidth * 0.5); |
|||
const circumference = 2 * Math.PI * radius; |
|||
const strokeval = ((value * circumference) / 100); |
|||
const dashval = (strokeval + ' ' + circumference); |
|||
|
|||
const trackstyle = {strokeWidth: 5}; |
|||
const indicatorstyle = {strokeWidth: strokewidth, strokeDasharray: dashval} |
|||
const rotateval = 'rotate(-90 '+37.5+','+37.5+')'; |
|||
|
|||
return ( |
|||
<svg width={75} height={75} className={styles.donutchart}> |
|||
<circle r={30} cx={37.5} cy={37.5} transform={rotateval} style={trackstyle} className={styles.donutchartTrack} /> |
|||
<circle r={30} cx={37.5} cy={37.5} transform={rotateval} style={indicatorstyle} className={styles.donutchartIndicator} /> |
|||
</svg> |
|||
) |
|||
} |
|||
|
|||
Donut.propTypes = { |
|||
} |
|||
|
|||
export default Donut |
@ -0,0 +1,20 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.donutchartTrack{ |
|||
fill: transparent; |
|||
stroke: $lightgrey; |
|||
stroke-width: 26; |
|||
} |
|||
.donutchartIndicator { |
|||
fill: transparent; |
|||
stroke: $main; |
|||
stroke-width: 26; |
|||
stroke-dasharray: 0 10000; |
|||
transition: stroke-dasharray .3s ease; |
|||
} |
|||
|
|||
.donutchart { |
|||
margin: 0 auto; |
|||
border-radius: 50%; |
|||
display: block; |
|||
} |
@ -0,0 +1,146 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import ReactModal from 'react-modal' |
|||
import { MdClose } from 'react-icons/lib/md' |
|||
import { FaCircle } from 'react-icons/lib/fa' |
|||
import styles from './FriendsForm.scss' |
|||
|
|||
const FriendsForm = ({ |
|||
friendsform, |
|||
closeFriendsForm, |
|||
updateFriendFormSearchQuery, |
|||
openChannel, |
|||
|
|||
activeChannelPubkeys, |
|||
nonActiveChannelPubkeys, |
|||
pendingOpenChannelPubkeys, |
|||
filteredNetworkNodes |
|||
}) => { |
|||
console.log('pendingOpenChannelPubkeys: ', pendingOpenChannelPubkeys) |
|||
const renderRightSide = (node) => { |
|||
if (node.addresses.length > 1) { |
|||
console.log('node: ', node) |
|||
} |
|||
|
|||
if (activeChannelPubkeys.includes(node.pub_key)) { |
|||
return ( |
|||
<span className={`${styles.online} ${styles.inactive}`}> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Online</span> |
|||
</span> |
|||
) |
|||
} |
|||
|
|||
if (nonActiveChannelPubkeys.includes(node.pub_key)) { |
|||
return ( |
|||
<span className={`${styles.offline} ${styles.inactive}`}> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Offline</span> |
|||
</span> |
|||
) |
|||
} |
|||
|
|||
if (pendingOpenChannelPubkeys.includes(node.pub_key)) { |
|||
return ( |
|||
<span className={`${styles.pending} ${styles.inactive}`}> |
|||
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Pending</span> |
|||
</span> |
|||
) |
|||
} |
|||
|
|||
if (!node.addresses.length) { |
|||
return ( |
|||
<span className={`${styles.private} ${styles.inactive}`}> |
|||
Private |
|||
</span> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<span |
|||
className={`${styles.connect} hint--left`} |
|||
data-hint='Connect with 0.1 BTC' |
|||
onClick={() => openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: 0.1 })} |
|||
> |
|||
Connect |
|||
</span> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
<ReactModal |
|||
isOpen={friendsform.isOpen} |
|||
contentLabel='No Overlay Click Modal' |
|||
ariaHideApp |
|||
shouldCloseOnOverlayClick |
|||
onRequestClose={() => closeFriendsForm} |
|||
parentSelector={() => document.body} |
|||
className={styles.modal} |
|||
> |
|||
<header> |
|||
<div> |
|||
<h1>Add Contact</h1> |
|||
</div> |
|||
<div onClick={closeFriendsForm} className={styles.modalClose}> |
|||
<MdClose /> |
|||
</div> |
|||
</header> |
|||
|
|||
<div className={styles.form} onKeyPress={event => event.charCode === 13 && console.log('gaaaang')}> |
|||
<div className={styles.search}> |
|||
<input |
|||
type='text' |
|||
placeholder='Find friend by alias or pubkey' |
|||
className={styles.searchInput} |
|||
value={friendsform.searchQuery} |
|||
onChange={event => updateFriendFormSearchQuery(event.target.value)} |
|||
autoFocus |
|||
/> |
|||
</div> |
|||
|
|||
<ul className={styles.networkResults}> |
|||
{ |
|||
friendsform.searchQuery.length > 0 && filteredNetworkNodes.map(node => { |
|||
return ( |
|||
<li key={node.pub_key}> |
|||
<section> |
|||
{ |
|||
node.alias.length > 0 ? |
|||
<h2> |
|||
<span>{node.alias.trim()}</span> |
|||
<span>({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})</span> |
|||
</h2> |
|||
: |
|||
<h2> |
|||
<span>{node.pub_key}</span> |
|||
</h2> |
|||
} |
|||
</section> |
|||
<section> |
|||
{renderRightSide(node)} |
|||
</section> |
|||
</li> |
|||
) |
|||
}) |
|||
} |
|||
</ul> |
|||
</div> |
|||
<footer className={styles.footer}> |
|||
<div> |
|||
<span className={styles.amount}> |
|||
0.1 |
|||
</span> |
|||
<span className={styles.caption}> |
|||
BTC per contact |
|||
</span> |
|||
</div> |
|||
</footer> |
|||
</ReactModal> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
FriendsForm.propTypes = { |
|||
|
|||
} |
|||
|
|||
export default FriendsForm |
@ -0,0 +1,135 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.modal { |
|||
position: relative; |
|||
width: 50%; |
|||
margin: 50px auto; |
|||
position: absolute; |
|||
top: auto; |
|||
left: 20%; |
|||
right: 0; |
|||
bottom: auto; |
|||
background: $white; |
|||
outline: none; |
|||
z-index: -2; |
|||
border: 1px solid $darkgrey; |
|||
|
|||
header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
padding: 15px; |
|||
border-bottom: 1px solid $darkgrey; |
|||
|
|||
h1, svg { |
|||
font-size: 22px; |
|||
} |
|||
|
|||
svg { |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.form { |
|||
padding: 30px 15px; |
|||
|
|||
.search { |
|||
.searchInput { |
|||
width: calc(100% - 30px); |
|||
padding: 10px 15px; |
|||
outline: 0; |
|||
border: 0; |
|||
background: $lightgrey; |
|||
color: $darkestgrey; |
|||
border-radius: 5px; |
|||
font-size: 16px; |
|||
} |
|||
} |
|||
|
|||
.networkResults { |
|||
overflow-y: scroll; |
|||
height: 400px; |
|||
margin-top: 30px; |
|||
padding: 20px 0; |
|||
|
|||
li { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
padding: 10px 0; |
|||
|
|||
h2 { |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
letter-spacing: 1.3px; |
|||
|
|||
span { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
|
|||
&:nth-child(1) { |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
letter-spacing: 1.3px; |
|||
} |
|||
|
|||
&:nth-child(2) { |
|||
color: $darkestgrey; |
|||
font-size: 12px; |
|||
line-height: 16px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.connect { |
|||
cursor: pointer; |
|||
color: $darkestgrey; |
|||
transition: all 0.25s; |
|||
font-size: 12px; |
|||
|
|||
&:hover { |
|||
color: $main; |
|||
} |
|||
} |
|||
|
|||
.inactive { |
|||
font-size: 12px; |
|||
|
|||
display: inline-block; |
|||
vertical-align: top; |
|||
|
|||
&.online { |
|||
color: $green; |
|||
} |
|||
|
|||
&.offline { |
|||
color: $darkestgrey; |
|||
} |
|||
|
|||
&.pending { |
|||
color: $orange; |
|||
} |
|||
|
|||
&.private { |
|||
color: darken($darkestgrey, 50%); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.footer { |
|||
padding: 10px 15px; |
|||
border-top: 1px solid $darkgrey; |
|||
|
|||
span { |
|||
&:nth-child(1) { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
&:nth-child(2) { |
|||
margin-left: 2px; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,33 @@ |
|||
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 }) => ( |
|||
<li className={styles.friend} key={channel.chan_id}> |
|||
<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 = { |
|||
|
|||
} |
|||
|
|||
export default OfflineContact |
@ -0,0 +1,33 @@ |
|||
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 }) => ( |
|||
<li className={styles.friend} key={channel.chan_id}> |
|||
<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 = { |
|||
|
|||
} |
|||
|
|||
export default OnlineContact |
@ -0,0 +1,38 @@ |
|||
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 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={() => shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${channel.channel.channel_point.split(':')[0]}`)}> |
|||
(~{channel.blocks_till_open * 10} minutes) |
|||
</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 = { |
|||
|
|||
} |
|||
|
|||
export default PendingContact |
@ -0,0 +1,74 @@ |
|||
import { createSelector } from 'reselect' |
|||
|
|||
import filter from 'lodash/filter' |
|||
|
|||
// Initial State
|
|||
const initialState = { |
|||
isOpen: false, |
|||
searchQuery: '', |
|||
friend: '' |
|||
} |
|||
|
|||
// Constants
|
|||
// ------------------------------------
|
|||
export const OPEN_FRIENDS_FORM = 'OPEN_FRIENDS_FORM' |
|||
export const CLOSE_FRIENDS_FORM = 'CLOSE_FRIENDS_FORM' |
|||
|
|||
export const UPDATE_FRIEND_FORM_SEARCH_QUERY = 'UPDATE_FRIEND_FORM_SEARCH_QUERY' |
|||
|
|||
// ------------------------------------
|
|||
// Actions
|
|||
// ------------------------------------
|
|||
export function openFriendsForm() { |
|||
return { |
|||
type: OPEN_FRIENDS_FORM |
|||
} |
|||
} |
|||
|
|||
export function closeFriendsForm() { |
|||
return { |
|||
type: CLOSE_FRIENDS_FORM |
|||
} |
|||
} |
|||
|
|||
export function updateFriendFormSearchQuery(searchQuery) { |
|||
return { |
|||
type: UPDATE_FRIEND_FORM_SEARCH_QUERY, |
|||
searchQuery |
|||
} |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Action Handlers
|
|||
// ------------------------------------
|
|||
const ACTION_HANDLERS = { |
|||
[OPEN_FRIENDS_FORM]: state => ({ ...state, isOpen: true }), |
|||
[CLOSE_FRIENDS_FORM]: state => ({ ...state, isOpen: false }), |
|||
|
|||
[UPDATE_FRIEND_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }) |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Selector
|
|||
// ------------------------------------
|
|||
const friendFormSelectors = {} |
|||
const networkNodesSelector = state => state.network.nodes |
|||
const searchQuerySelector = state => state.friendsform.searchQuery |
|||
|
|||
|
|||
friendFormSelectors.filteredNetworkNodes = createSelector( |
|||
networkNodesSelector, |
|||
searchQuerySelector, |
|||
(nodes, searchQuery) => filter(nodes, node => node.alias.includes(searchQuery) || node.pub_key.includes(searchQuery)) |
|||
) |
|||
|
|||
export { friendFormSelectors } |
|||
|
|||
// ------------------------------------
|
|||
// Reducer
|
|||
// ------------------------------------
|
|||
export default function friendFormReducer(state = initialState, action) { |
|||
const handler = ACTION_HANDLERS[action.type] |
|||
|
|||
return handler ? handler(state, action) : state |
|||
} |
@ -1,24 +1,76 @@ |
|||
import { withRouter } from 'react-router' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { fetchChannels, channelsSelectors } from 'reducers/channels' |
|||
import { |
|||
fetchChannels, |
|||
openChannel, |
|||
currentChannels, |
|||
channelsSelectors |
|||
} from 'reducers/channels' |
|||
|
|||
import { fetchPeers } from 'reducers/peers' |
|||
|
|||
import { fetchDescribeNetwork } from 'reducers/network' |
|||
|
|||
import { |
|||
openFriendsForm, |
|||
closeFriendsForm, |
|||
updateFriendFormSearchQuery, |
|||
friendFormSelectors |
|||
} from 'reducers/friendsform' |
|||
|
|||
import Friends from '../components/Friends' |
|||
|
|||
const mapDispatchToProps = { |
|||
openFriendsForm, |
|||
closeFriendsForm, |
|||
updateFriendFormSearchQuery, |
|||
openChannel, |
|||
|
|||
fetchChannels, |
|||
fetchPeers |
|||
fetchPeers, |
|||
fetchDescribeNetwork |
|||
} |
|||
|
|||
const mapStateToProps = state => ({ |
|||
channels: state.channels, |
|||
peers: state.peers, |
|||
network: state.network, |
|||
friendsform: state.friendsform, |
|||
|
|||
currentChannels: currentChannels(state), |
|||
activeChannels: channelsSelectors.activeChannels(state), |
|||
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state), |
|||
nonActiveChannels: channelsSelectors.nonActiveChannels(state), |
|||
pendingOpenChannels: channelsSelectors.pendingOpenChannels(state) |
|||
nonActiveChannelPubkeys: channelsSelectors.nonActiveChannelPubkeys(state), |
|||
pendingOpenChannels: channelsSelectors.pendingOpenChannels(state), |
|||
pendingOpenChannelPubkeys: channelsSelectors.pendingOpenChannelPubkeys(state), |
|||
closingPendingChannels: channelsSelectors.closingPendingChannels(state), |
|||
|
|||
filteredNetworkNodes: friendFormSelectors.filteredNetworkNodes(state) |
|||
}) |
|||
|
|||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Friends)) |
|||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
|||
const friendsFormProps = { |
|||
closeFriendsForm: dispatchProps.closeFriendsForm, |
|||
updateFriendFormSearchQuery: dispatchProps.updateFriendFormSearchQuery, |
|||
openChannel: dispatchProps.openChannel, |
|||
|
|||
friendsform: stateProps.friendsform, |
|||
filteredNetworkNodes: stateProps.filteredNetworkNodes, |
|||
|
|||
activeChannelPubkeys: stateProps.activeChannelPubkeys, |
|||
nonActiveChannelPubkeys: stateProps.nonActiveChannelPubkeys, |
|||
pendingOpenChannelPubkeys: stateProps.pendingOpenChannelPubkeys |
|||
} |
|||
|
|||
return { |
|||
...stateProps, |
|||
...dispatchProps, |
|||
...ownProps, |
|||
|
|||
friendsFormProps |
|||
} |
|||
} |
|||
|
|||
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Friends)) |
|||
|
Loading…
Reference in new issue