Jack Mallers
7 years ago
22 changed files with 281 additions and 800 deletions
@ -0,0 +1,106 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
|
||||
|
import ReactModal from 'react-modal' |
||||
|
import { FaClose, FaCircle } from 'react-icons/lib/fa' |
||||
|
|
||||
|
import { btc } from 'utils' |
||||
|
|
||||
|
import styles from './ContactModal.scss' |
||||
|
|
||||
|
const ContactModal = ({ isOpen, channel, closeContactModal }) => { |
||||
|
console.log('channel: ', channel) |
||||
|
const customStyles = { |
||||
|
overlay: { |
||||
|
cursor: 'pointer', |
||||
|
overflowY: 'auto' |
||||
|
}, |
||||
|
content: { |
||||
|
top: 'auto', |
||||
|
left: '20%', |
||||
|
right: '0', |
||||
|
bottom: 'auto', |
||||
|
width: '40%', |
||||
|
margin: '50px auto', |
||||
|
borderRadius: 'none', |
||||
|
padding: '0' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<ReactModal |
||||
|
isOpen={isOpen} |
||||
|
contentLabel='No Overlay Click Modal' |
||||
|
ariaHideApp |
||||
|
shouldCloseOnOverlayClick |
||||
|
onRequestClose={closeContactModal} |
||||
|
parentSelector={() => document.body} |
||||
|
style={customStyles} |
||||
|
> |
||||
|
{ |
||||
|
channel && |
||||
|
<div className={styles.container}> |
||||
|
<header className={styles.header}> |
||||
|
<div className={`${styles.status} ${channel.active && styles.online}`}> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> |
||||
|
<span> |
||||
|
{ |
||||
|
channel.active ? |
||||
|
'Online' |
||||
|
: |
||||
|
'Offline' |
||||
|
} |
||||
|
</span> |
||||
|
</div> |
||||
|
<div className={styles.closeContainer}> |
||||
|
<span onClick={closeContactModal}> |
||||
|
<FaClose /> |
||||
|
</span> |
||||
|
</div> |
||||
|
</header> |
||||
|
|
||||
|
<section className={styles.title}> |
||||
|
<h2>{channel.remote_pubkey}</h2> |
||||
|
</section> |
||||
|
|
||||
|
<section className={styles.stats}> |
||||
|
<div className={styles.pay}> |
||||
|
<h4>Can Pay</h4> |
||||
|
<div className={styles.meter}> |
||||
|
<div className={styles.amount} style={{ width: `${(channel.local_balance / channel.capacity) * 100}%` }} /> |
||||
|
</div> |
||||
|
<span>{btc.satoshisToBtc(channel.local_balance)} BTC</span> |
||||
|
</div> |
||||
|
|
||||
|
<div className={styles.pay}> |
||||
|
<h4>Can Receive</h4> |
||||
|
<div className={styles.meter}> |
||||
|
<div className={styles.amount} style={{ width: `${(channel.remote_balance / channel.capacity) * 100}%` }} /> |
||||
|
</div> |
||||
|
<span>{btc.satoshisToBtc(channel.remote_balance)} BTC</span> |
||||
|
</div> |
||||
|
|
||||
|
<div className={styles.sent}> |
||||
|
<h4>Total Bitcoin Sent</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.total_satoshis_sent)} BTC</p> |
||||
|
</div> |
||||
|
<div className={styles.received}> |
||||
|
<h4>Total Bitcoin Received</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.total_satoshis_received)} BTC</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<footer> |
||||
|
<div>Remove</div> |
||||
|
</footer> |
||||
|
</div> |
||||
|
} |
||||
|
</ReactModal> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
ContactModal.propTypes = { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export default ContactModal |
@ -0,0 +1,100 @@ |
|||||
|
@import '../../variables.scss'; |
||||
|
|
||||
|
.header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: flex-end; |
||||
|
background: $lightgrey; |
||||
|
margin-bottom: 30px; |
||||
|
padding: 20px; |
||||
|
|
||||
|
.status { |
||||
|
font-size: 12px; |
||||
|
color: $darkestgrey; |
||||
|
|
||||
|
&.online { |
||||
|
color: $green; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.closeContainer { |
||||
|
background: $lightgrey; |
||||
|
line-height: 12px; |
||||
|
|
||||
|
span { |
||||
|
color: $darkestgrey; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.container section { |
||||
|
margin-bottom: 30px; |
||||
|
padding: 0 20px; |
||||
|
|
||||
|
.pay, .receive, .sent, .received { |
||||
|
margin: 40px 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.title { |
||||
|
|
||||
|
h2 { |
||||
|
color: $secondary; |
||||
|
font-weight: bold; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.stats { |
||||
|
|
||||
|
h4 { |
||||
|
color: $secondary; |
||||
|
font-weight: bold; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
margin-top: 10px; |
||||
|
color: $darkestgrey; |
||||
|
} |
||||
|
|
||||
|
.meter, .amount { |
||||
|
height: 10px; |
||||
|
border-radius: 10px; |
||||
|
} |
||||
|
|
||||
|
.meter { |
||||
|
background: $darkgrey; |
||||
|
width: 100%; |
||||
|
margin: 10px 0; |
||||
|
} |
||||
|
|
||||
|
.amount { |
||||
|
background: $darkestgrey; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.container footer { |
||||
|
padding: 20px; |
||||
|
text-align: center; |
||||
|
|
||||
|
div { |
||||
|
color: $red; |
||||
|
font-size: 18px; |
||||
|
|
||||
|
&:hover { |
||||
|
color: lighten($red, 10%); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,205 +0,0 @@ |
|||||
import React, { Component } from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
|
|
||||
import { FaAngleDown, FaRepeat } from 'react-icons/lib/fa' |
|
||||
import { MdSearch } from 'react-icons/lib/md' |
|
||||
|
|
||||
import OpenPendingChannel from 'components/Channels/OpenPendingChannel' |
|
||||
import ClosedPendingChannel from 'components/Channels/ClosedPendingChannel' |
|
||||
import Channel from 'components/Channels/Channel' |
|
||||
import ChannelForm from 'components/ChannelForm' |
|
||||
|
|
||||
import styles from './Channels.scss' |
|
||||
|
|
||||
class Channels extends Component { |
|
||||
constructor(props) { |
|
||||
super(props) |
|
||||
|
|
||||
this.state = { |
|
||||
refreshing: false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentWillMount() { |
|
||||
const { fetchChannels, fetchPeers } = this.props |
|
||||
|
|
||||
fetchChannels() |
|
||||
fetchPeers() |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { |
|
||||
fetchChannels, |
|
||||
closeChannel, |
|
||||
channels: { |
|
||||
searchQuery, |
|
||||
filterPulldown, |
|
||||
filter, |
|
||||
viewType |
|
||||
}, |
|
||||
|
|
||||
nonActiveFilters, |
|
||||
toggleFilterPulldown, |
|
||||
changeFilter, |
|
||||
|
|
||||
currentChannels, |
|
||||
updateChannelSearchQuery, |
|
||||
|
|
||||
openChannelForm, |
|
||||
|
|
||||
ticker, |
|
||||
currentTicker, |
|
||||
|
|
||||
channelFormProps |
|
||||
} = 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 peers
|
|
||||
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.container} ${viewType === 1 && styles.graphview}`}> |
|
||||
<ChannelForm {...channelFormProps} /> |
|
||||
|
|
||||
<header className={styles.header}> |
|
||||
<div className={styles.titleContainer}> |
|
||||
<div className={styles.left}> |
|
||||
<h1>Channels</h1> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className={styles.createChannelContainer}> |
|
||||
<div className={`buttonPrimary ${styles.newChannelButton}`} onClick={openChannelForm}> |
|
||||
Create new channel |
|
||||
</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 channels by funding transaction or remote public key' |
|
||||
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> |
|
||||
|
|
||||
<div className={`${styles.channels} ${filterPulldown && styles.fade}`}> |
|
||||
<ul className={viewType === 1 && styles.cardsContainer}> |
|
||||
{ |
|
||||
currentChannels.map((channel, index) => { |
|
||||
if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) { |
|
||||
return ( |
|
||||
<OpenPendingChannel |
|
||||
key={index} |
|
||||
channel={channel} |
|
||||
ticker={ticker} |
|
||||
currentTicker={currentTicker} |
|
||||
explorerLinkBase={'https://testnet.smartbit.com.au/'} |
|
||||
/> |
|
||||
) |
|
||||
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { |
|
||||
return ( |
|
||||
<ClosedPendingChannel |
|
||||
key={index} |
|
||||
channel={channel} |
|
||||
ticker={ticker} |
|
||||
currentTicker={currentTicker} |
|
||||
explorerLinkBase={'https://testnet.smartbit.com.au/'} |
|
||||
/> |
|
||||
) |
|
||||
} |
|
||||
return ( |
|
||||
<Channel |
|
||||
key={index} |
|
||||
ticker={ticker} |
|
||||
channel={channel} |
|
||||
closeChannel={closeChannel} |
|
||||
currentTicker={currentTicker} |
|
||||
/> |
|
||||
) |
|
||||
}) |
|
||||
} |
|
||||
</ul> |
|
||||
</div> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Channels.propTypes = { |
|
||||
fetchChannels: PropTypes.func.isRequired, |
|
||||
|
|
||||
channels: PropTypes.object.isRequired, |
|
||||
currentChannels: PropTypes.array.isRequired, |
|
||||
nonActiveFilters: PropTypes.array.isRequired, |
|
||||
|
|
||||
updateChannelSearchQuery: PropTypes.func.isRequired, |
|
||||
setCurrentChannel: PropTypes.func.isRequired, |
|
||||
openChannelForm: PropTypes.func.isRequired, |
|
||||
closeChannel: PropTypes.func.isRequired, |
|
||||
toggleFilterPulldown: PropTypes.func.isRequired, |
|
||||
changeFilter: PropTypes.func.isRequired, |
|
||||
fetchPeers: PropTypes.func.isRequired, |
|
||||
|
|
||||
ticker: PropTypes.object.isRequired, |
|
||||
currentTicker: PropTypes.object.isRequired, |
|
||||
|
|
||||
channelFormProps: PropTypes.object.isRequired |
|
||||
} |
|
||||
|
|
||||
export default Channels |
|
@ -1,178 +0,0 @@ |
|||||
@import '../../../variables.scss'; |
|
||||
|
|
||||
.container.graphview { |
|
||||
background: $black; |
|
||||
} |
|
||||
|
|
||||
.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; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.createChannelContainer { |
|
||||
padding: 20px 40px; |
|
||||
|
|
||||
.createChannelButton { |
|
||||
font-size: 14px; |
|
||||
margin-left: 10px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.filtersContainer { |
|
||||
position: relative; |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
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 { |
|
||||
text-decoration: underline; |
|
||||
|
|
||||
svg { |
|
||||
font-size: 12px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
.layoutsContainer { |
|
||||
padding: 40px; |
|
||||
|
|
||||
span { |
|
||||
font-size: 30px; |
|
||||
color: $grey; |
|
||||
cursor: pointer; |
|
||||
transition: all 0.25s; |
|
||||
|
|
||||
&:nth-child(1) { |
|
||||
margin-right: 20px; |
|
||||
} |
|
||||
|
|
||||
&:hover { |
|
||||
color: $darkestgrey; |
|
||||
} |
|
||||
|
|
||||
&.active { |
|
||||
color: $darkestgrey; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.createChannelContainer { |
|
||||
padding: 40px; |
|
||||
|
|
||||
.newChannelButton { |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.channels { |
|
||||
padding: 10px 40px 40px 40px; |
|
||||
transition: opacity 0.25s; |
|
||||
|
|
||||
&.fade { |
|
||||
opacity: 0.05; |
|
||||
} |
|
||||
|
|
||||
.cardsContainer { |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
flex-wrap: wrap; |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
} |
|
@ -1,104 +0,0 @@ |
|||||
import { withRouter } from 'react-router' |
|
||||
import { connect } from 'react-redux' |
|
||||
|
|
||||
import { |
|
||||
fetchChannels, |
|
||||
openChannel, |
|
||||
closeChannel, |
|
||||
updateChannelSearchQuery, |
|
||||
setViewType, |
|
||||
currentChannels, |
|
||||
|
|
||||
toggleFilterPulldown, |
|
||||
changeFilter, |
|
||||
|
|
||||
channelsSelectors |
|
||||
} from 'reducers/channels' |
|
||||
|
|
||||
import { |
|
||||
openChannelForm, |
|
||||
changeStep, |
|
||||
setNodeKey, |
|
||||
setLocalAmount, |
|
||||
setPushAmount, |
|
||||
closeChannelForm, |
|
||||
channelFormSelectors |
|
||||
} from 'reducers/channelform' |
|
||||
|
|
||||
import { fetchPeers } from 'reducers/peers' |
|
||||
|
|
||||
import { tickerSelectors } from 'reducers/ticker' |
|
||||
|
|
||||
import { fetchDescribeNetwork, setCurrentChannel } from '../../../reducers/network' |
|
||||
|
|
||||
import Channels from '../components/Channels' |
|
||||
|
|
||||
const mapDispatchToProps = { |
|
||||
fetchChannels, |
|
||||
openChannel, |
|
||||
closeChannel, |
|
||||
updateChannelSearchQuery, |
|
||||
setViewType, |
|
||||
toggleFilterPulldown, |
|
||||
changeFilter, |
|
||||
|
|
||||
openChannelForm, |
|
||||
closeChannelForm, |
|
||||
setNodeKey, |
|
||||
setLocalAmount, |
|
||||
setPushAmount, |
|
||||
changeStep, |
|
||||
|
|
||||
fetchPeers, |
|
||||
|
|
||||
fetchDescribeNetwork, |
|
||||
setCurrentChannel |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => ({ |
|
||||
channels: state.channels, |
|
||||
openChannels: state.channels.channels, |
|
||||
channelform: state.channelform, |
|
||||
peers: state.peers, |
|
||||
ticker: state.ticker, |
|
||||
network: state.network, |
|
||||
identity_pubkey: state.info.data.identity_pubkey, |
|
||||
|
|
||||
currentChannels: currentChannels(state), |
|
||||
activeChanIds: channelsSelectors.activeChanIds(state), |
|
||||
nonActiveFilters: channelsSelectors.nonActiveFilters(state), |
|
||||
activeChannels: channelsSelectors.activeChannels(state), |
|
||||
|
|
||||
currentTicker: tickerSelectors.currentTicker(state), |
|
||||
|
|
||||
channelFormHeader: channelFormSelectors.channelFormHeader(state), |
|
||||
channelFormProgress: channelFormSelectors.channelFormProgress(state), |
|
||||
stepTwoIsValid: channelFormSelectors.stepTwoIsValid(state) |
|
||||
}) |
|
||||
|
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
|
||||
const channelFormProps = { |
|
||||
openChannel: dispatchProps.openChannel, |
|
||||
closeChannelForm: dispatchProps.closeChannelForm, |
|
||||
changeStep: dispatchProps.changeStep, |
|
||||
setNodeKey: dispatchProps.setNodeKey, |
|
||||
setLocalAmount: dispatchProps.setLocalAmount, |
|
||||
setPushAmount: dispatchProps.setPushAmount, |
|
||||
|
|
||||
channelform: stateProps.channelform, |
|
||||
channelFormHeader: stateProps.channelFormHeader, |
|
||||
channelFormProgress: stateProps.channelFormProgress, |
|
||||
stepTwoIsValid: stateProps.stepTwoIsValid, |
|
||||
peers: stateProps.peers.peers |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
...stateProps, |
|
||||
...dispatchProps, |
|
||||
...ownProps, |
|
||||
|
|
||||
channelFormProps |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Channels)) |
|
@ -1,3 +0,0 @@ |
|||||
import ChannelsContainer from './containers/ChannelsContainer' |
|
||||
|
|
||||
export default ChannelsContainer |
|
@ -1,137 +0,0 @@ |
|||||
import React, { Component } from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
|
|
||||
import { FaRepeat } from 'react-icons/lib/fa' |
|
||||
import { MdSearch } from 'react-icons/lib/md' |
|
||||
|
|
||||
import PeerForm from 'components/Peers/PeerForm' |
|
||||
import PeerModal from 'components/Peers/PeerModal' |
|
||||
import Peer from 'components/Peers/Peer' |
|
||||
|
|
||||
import styles from './Peers.scss' |
|
||||
|
|
||||
class Peers extends Component { |
|
||||
constructor(props) { |
|
||||
super(props) |
|
||||
|
|
||||
this.state = { |
|
||||
refreshing: false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentWillMount() { |
|
||||
this.props.fetchPeers() |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { |
|
||||
fetchPeers, |
|
||||
peerFormProps, |
|
||||
setPeerForm, |
|
||||
setPeer, |
|
||||
updateSearchQuery, |
|
||||
disconnectRequest, |
|
||||
|
|
||||
peerModalOpen, |
|
||||
filteredPeers, |
|
||||
peers: { peer, searchQuery } |
|
||||
} = 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 peers
|
|
||||
fetchPeers() |
|
||||
|
|
||||
// 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> |
|
||||
<PeerForm {...peerFormProps} /> |
|
||||
|
|
||||
<PeerModal isOpen={peerModalOpen} resetPeer={setPeer} peer={peer} disconnect={disconnectRequest} /> |
|
||||
|
|
||||
<header className={styles.header}> |
|
||||
<div className={styles.titleContainer}> |
|
||||
<div className={styles.left}> |
|
||||
<h1>Peers</h1> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className={styles.addPeerContainer}> |
|
||||
<div className={`buttonPrimary ${styles.newPeerButton}`} onClick={() => setPeerForm({ isOpen: true })}> |
|
||||
Add new peer |
|
||||
</div> |
|
||||
</div> |
|
||||
</header> |
|
||||
|
|
||||
<div className={styles.search}> |
|
||||
<label className={`${styles.label} ${styles.input}`} htmlFor='channelSearch'> |
|
||||
<MdSearch /> |
|
||||
</label> |
|
||||
<input |
|
||||
value={searchQuery} |
|
||||
onChange={event => updateSearchQuery(event.target.value)} |
|
||||
className={`${styles.text} ${styles.input}`} |
|
||||
placeholder='Search peers by their node public key or IP address' |
|
||||
type='text' |
|
||||
id='peersSearch' |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<div className={styles.refreshContainer}> |
|
||||
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}> |
|
||||
{ |
|
||||
this.state.refreshing ? |
|
||||
<FaRepeat /> |
|
||||
: |
|
||||
'Refresh' |
|
||||
} |
|
||||
</span> |
|
||||
</div> |
|
||||
|
|
||||
<div className={styles.peers}> |
|
||||
{ |
|
||||
filteredPeers.map(filteredPeer => <Peer key={filteredPeer.peer_id} peer={filteredPeer} setPeer={setPeer} />) |
|
||||
} |
|
||||
</div> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Peers.propTypes = { |
|
||||
fetchPeers: PropTypes.func.isRequired, |
|
||||
peerFormProps: PropTypes.object.isRequired, |
|
||||
setPeerForm: PropTypes.func.isRequired, |
|
||||
setPeer: PropTypes.func.isRequired, |
|
||||
updateSearchQuery: PropTypes.func.isRequired, |
|
||||
disconnectRequest: PropTypes.func.isRequired, |
|
||||
|
|
||||
peerModalOpen: PropTypes.bool.isRequired, |
|
||||
filteredPeers: PropTypes.array.isRequired, |
|
||||
peers: PropTypes.shape({ |
|
||||
peer: PropTypes.object, |
|
||||
searchQuery: PropTypes.string |
|
||||
}).isRequired |
|
||||
} |
|
||||
|
|
||||
export default Peers |
|
@ -1,81 +0,0 @@ |
|||||
@import '../../../variables.scss'; |
|
||||
|
|
||||
.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; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.addPeerContainer { |
|
||||
padding: 20px 40px; |
|
||||
|
|
||||
.newPeerButton { |
|
||||
font-size: 14px; |
|
||||
margin-left: 10px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.refreshContainer { |
|
||||
padding: 20px 40px 0 40px; |
|
||||
text-align: right; |
|
||||
cursor: pointer; |
|
||||
|
|
||||
.refresh { |
|
||||
text-decoration: underline; |
|
||||
|
|
||||
svg { |
|
||||
font-size: 12px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.peers { |
|
||||
padding: 40px; |
|
||||
} |
|
@ -1,52 +0,0 @@ |
|||||
import { withRouter } from 'react-router' |
|
||||
import { connect } from 'react-redux' |
|
||||
|
|
||||
import { |
|
||||
fetchPeers, |
|
||||
setPeer, |
|
||||
setPeerForm, |
|
||||
connectRequest, |
|
||||
disconnectRequest, |
|
||||
updateSearchQuery, |
|
||||
|
|
||||
peersSelectors |
|
||||
} from 'reducers/peers' |
|
||||
|
|
||||
import Peers from '../components/Peers' |
|
||||
|
|
||||
const mapDispatchToProps = { |
|
||||
fetchPeers, |
|
||||
setPeer, |
|
||||
peersSelectors, |
|
||||
setPeerForm, |
|
||||
connectRequest, |
|
||||
disconnectRequest, |
|
||||
updateSearchQuery |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => ({ |
|
||||
peers: state.peers, |
|
||||
info: state.info, |
|
||||
|
|
||||
peerModalOpen: peersSelectors.peerModalOpen(state), |
|
||||
filteredPeers: peersSelectors.filteredPeers(state) |
|
||||
}) |
|
||||
|
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
|
||||
const peerFormProps = { |
|
||||
setForm: dispatchProps.setPeerForm, |
|
||||
connect: dispatchProps.connectRequest, |
|
||||
|
|
||||
form: stateProps.peers.peerForm |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
...stateProps, |
|
||||
...dispatchProps, |
|
||||
...ownProps, |
|
||||
|
|
||||
peerFormProps |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Peers)) |
|
@ -1,3 +0,0 @@ |
|||||
import PeersContainer from './containers/PeersContainer' |
|
||||
|
|
||||
export default PeersContainer |
|
Loading…
Reference in new issue