Browse Source

feature(contacts): contact modal

renovate/lint-staged-8.x
Jack Mallers 7 years ago
parent
commit
8a32354b1e
  1. 9
      app/components/Contacts/Contact.scss
  2. 106
      app/components/Contacts/ContactModal.js
  3. 100
      app/components/Contacts/ContactModal.scss
  4. 11
      app/components/Contacts/ContactsForm.js
  5. 6
      app/components/Contacts/ContactsForm.scss
  6. 4
      app/components/Contacts/OfflineContact.js
  7. 4
      app/components/Contacts/OnlineContact.js
  8. 2
      app/components/ModalRoot/ModalRoot.js
  9. 13
      app/components/Nav/Nav.js
  10. 28
      app/reducers/channels.js
  11. 4
      app/routes.js
  12. 205
      app/routes/channels/components/Channels.js
  13. 178
      app/routes/channels/components/Channels.scss
  14. 104
      app/routes/channels/containers/ChannelsContainer.js
  15. 3
      app/routes/channels/index.js
  16. 17
      app/routes/contacts/components/Contacts.js
  17. 2
      app/routes/contacts/components/Contacts.scss
  18. 12
      app/routes/contacts/containers/ContactsContainer.js
  19. 137
      app/routes/peers/components/Peers.js
  20. 81
      app/routes/peers/components/Peers.scss
  21. 52
      app/routes/peers/containers/PeersContainer.js
  22. 3
      app/routes/peers/index.js

9
app/components/Contacts/Contact.scss

@ -4,8 +4,9 @@
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 30px 0;
border-bottom: 1px solid $traditionalgrey;
padding: 30px 60px 60px 60px;
cursor: pointer;
transition: all 0.25s;
&.loading {
.info {
@ -13,6 +14,10 @@
}
}
&:hover {
background: $lightgrey;
}
.limits {
display: flex;
flex-direction: row;

106
app/components/Contacts/ContactModal.js

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

100
app/components/Contacts/ContactModal.scss

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

11
app/components/Contacts/ContactsForm.js

@ -2,7 +2,7 @@ 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 { FaCircle, FaQuestionCircle } from 'react-icons/lib/fa'
import styles from './ContactsForm.scss'
class ContactsForm extends React.Component {
@ -183,6 +183,9 @@ class ContactsForm extends React.Component {
<footer className={styles.footer}>
<div>
<span>
Use
</span>
<span className={styles.amount}>
<input
type='text'
@ -196,6 +199,12 @@ class ContactsForm extends React.Component {
</span>
<span className={styles.caption}>
BTC per contact
<i
data-hint="You aren't spending anything, just moving money onto the Lightning Network"
className='hint--top'
>
<FaQuestionCircle style={{ verticalAlign: 'top' }} />
</i>
</span>
</div>
</footer>

6
app/components/Contacts/ContactsForm.scss

@ -186,6 +186,12 @@
&:nth-child(2) {
margin-left: 2px;
}
}
.caption svg {
font-size: 10px;
color: $darkestgrey;
}
}

4
app/components/Contacts/OfflineContact.js

@ -4,8 +4,8 @@ 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}>
const OfflineContact = ({ channel, openContactModal }) => (
<li className={styles.friend} key={channel.chan_id} onClick={() => openContactModal(channel)}>
<section className={styles.info}>
<p>
<FaCircle style={{ verticalAlign: 'top' }} />

4
app/components/Contacts/OnlineContact.js

@ -4,8 +4,8 @@ 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}>
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' }} />

2
app/components/ModalRoot/ModalRoot.js

@ -37,7 +37,7 @@ const ModalRoot = ({ modalType, modalProps, hideModal, currentTicker, currency }
ModalRoot.propTypes = {
modalType: PropTypes.string,
modalProps: PropTypes.object.isRequired,
modalProps: PropTypes.object,
hideModal: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired,
currency: PropTypes.string.isRequired

13
app/components/Nav/Nav.js

@ -5,7 +5,6 @@ import Isvg from 'react-inlinesvg'
import walletIcon from 'icons/wallet.svg'
import peersIcon from 'icons/peers.svg'
import channelsIcon from 'icons/channels.svg'
import networkIcon from 'icons/globe.svg'
import styles from './Nav.scss'
@ -29,18 +28,6 @@ const Nav = ({ openPayForm, openRequestForm }) => (
<span>Contacts</span>
</li>
</NavLink>
<NavLink exact to='/peers' activeClassName={styles.active} className={styles.link}>
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={peersIcon} />
<span>Peers</span>
</li>
</NavLink>
<NavLink exact to='/channels' activeClassName={styles.active} className={styles.link}>
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={channelsIcon} />
<span>Channels</span>
</li>
</NavLink>
<NavLink exact to='/network' activeClassName={styles.active} className={styles.link}>
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={networkIcon} />

28
app/reducers/channels.js

@ -32,6 +32,9 @@ export const CHANGE_CHANNEL_FILTER = 'CHANGE_CHANNEL_FILTER'
export const ADD_LOADING_PUBKEY = 'ADD_LOADING_PUBKEY'
export const REMOVE_LOADING_PUBKEY = 'REMOVE_LOADING_PUBKEY'
export const OPEN_CONTACT_MODAL = 'OPEN_CONTACT_MODAL'
export const CLOSE_CONTACT_MODAL = 'CLOSE_CONTACT_MODAL'
// ------------------------------------
// Actions
// ------------------------------------
@ -108,6 +111,19 @@ export function removeLoadingPubkey(pubkey) {
}
}
export function openContactModal(channel) {
return {
type: OPEN_CONTACT_MODAL,
channel
}
}
export function closeContactModal() {
return {
type: CLOSE_CONTACT_MODAL
}
}
// Send IPC event for peers
export const fetchChannels = () => async (dispatch) => {
dispatch(getChannels())
@ -297,7 +313,10 @@ const ACTION_HANDLERS = {
[CHANGE_CHANNEL_FILTER]: (state, { filter }) => ({ ...state, filterPulldown: false, filter }),
[ADD_LOADING_PUBKEY]: (state, { pubkey }) => ({ ...state, loadingChannelPubkeys: [pubkey, ...state.loadingChannelPubkeys] }),
[REMOVE_LOADING_PUBKEY]: (state, { pubkey }) => ({ ...state, loadingChannelPubkeys: state.loadingChannelPubkeys.filter(loadingPubkey => loadingPubkey !== pubkey) })
[REMOVE_LOADING_PUBKEY]: (state, { pubkey }) => ({ ...state, loadingChannelPubkeys: state.loadingChannelPubkeys.filter(loadingPubkey => loadingPubkey !== pubkey) }),
[OPEN_CONTACT_MODAL]: (state, { channel }) => ({ ...state, contactModal: { isOpen: true, channel } }),
[CLOSE_CONTACT_MODAL]: state => ({ ...state, contactModal: { isOpen: false, channel: null } })
}
const channelsSelectors = {}
@ -457,7 +476,12 @@ const initialState = {
{ key: 'CLOSING_PENDING_CHANNELS', name: 'Closing Contacts' }
],
loadingChannelPubkeys: []
loadingChannelPubkeys: [],
contactModal: {
isOpen: false,
channel: null
}
}
export default function channelsReducer(state = initialState, action) {

4
app/routes.js

@ -4,15 +4,11 @@ import { Switch, Route } from 'react-router'
import App from './routes/app'
import Activity from './routes/activity'
import Contacts from './routes/contacts'
import Peers from './routes/peers'
import Channels from './routes/channels'
import Network from './routes/network'
export default () => (
<App>
<Switch>
<Route path='/peers' component={Peers} />
<Route path='/channels' component={Channels} />
<Route path='/contacts' component={Contacts} />
<Route path='/network' component={Network} />
<Route path='/' component={Activity} />

205
app/routes/channels/components/Channels.js

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

178
app/routes/channels/components/Channels.scss

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

104
app/routes/channels/containers/ChannelsContainer.js

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

3
app/routes/channels/index.js

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

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

@ -7,6 +7,7 @@ import { FaAngleDown, FaRepeat } from 'react-icons/lib/fa'
import { btc } from 'utils'
import ContactModal from 'components/Contacts/ContactModal'
import ContactsForm from 'components/Contacts/ContactsForm'
import OnlineContact from 'components/Contacts/OnlineContact'
import PendingContact from 'components/Contacts/PendingContact'
@ -57,7 +58,10 @@ class Contacts extends Component {
nonActiveFilters,
openContactsForm,
openContactModal,
closeContactModal,
contactModalProps,
contactsFormProps,
peers
@ -92,6 +96,7 @@ class Contacts extends Component {
return (
<div className={styles.friendsContainer}>
<ContactModal {...contactModalProps} />
<ContactsForm {...contactsFormProps} />
<header className={styles.header}>
@ -151,13 +156,7 @@ class Contacts extends Component {
<ul className={`${styles.friends} ${filterPulldown && styles.fade}`}>
{
loadingChannelPubkeys.map(pubkey => {
console.log('pubkey: ', pubkey)
return (
<LoadingContact pubkey={pubkey} />
)
})
loadingChannelPubkeys.map(pubkey => <LoadingContact pubkey={pubkey} />)
}
{
@ -167,9 +166,9 @@ class Contacts extends Component {
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
return <ClosingContact channel={channel} key={index} />
} else if (channel.active) {
return <OnlineContact channel={channel} key={index} />
return <OnlineContact channel={channel} key={index} openContactModal={openContactModal} />
} else if (!channel.active) {
return <OfflineContact channel={channel} key={index} />
return <OfflineContact channel={channel} key={index} openContactModal={openContactModal} />
}
})
}

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

@ -162,7 +162,7 @@
}
.friends {
padding: 10px 60px 60px 60px;
padding: 10px 0 60px 0;
opacity: 1;
transition: all 0.25s;

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

@ -8,6 +8,8 @@ import {
updateChannelSearchQuery,
toggleFilterPulldown,
changeFilter,
openContactModal,
closeContactModal,
currentChannels,
channelsSelectors
@ -30,6 +32,8 @@ import Contacts from '../components/Contacts'
const mapDispatchToProps = {
openContactsForm,
closeContactsForm,
openContactModal,
closeContactModal,
updateContactFormSearchQuery,
updateContactCapacity,
openChannel,
@ -63,6 +67,13 @@ const mapStateToProps = state => ({
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const contactModalProps = {
closeContactModal: dispatchProps.closeContactModal,
isOpen: stateProps.channels.contactModal.isOpen,
channel: stateProps.channels.contactModal.channel
}
const contactsFormProps = {
closeContactsForm: dispatchProps.closeContactsForm,
updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery,
@ -84,6 +95,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
...dispatchProps,
...ownProps,
contactModalProps,
contactsFormProps
}
}

137
app/routes/peers/components/Peers.js

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

81
app/routes/peers/components/Peers.scss

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

52
app/routes/peers/containers/PeersContainer.js

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

3
app/routes/peers/index.js

@ -1,3 +0,0 @@
import PeersContainer from './containers/PeersContainer'
export default PeersContainer
Loading…
Cancel
Save