Sidebar refactor / style update (#173)

* Convert bootstrap to sass instead of checked in and less

* Darken body, adjust header.

* First pass at tab styles, each tab will need a lot of individual love tho.

* Update footer to main site content, improve responsiveness.

* Missing key added.

* Fix dropdowns.

* Convert GenerateWallet HTML over, still needs styling.

* Send form.

* Current rates styled.

* CurrencySwap form styles.

* SwapInfoHeader styled.

* Finish up swap restyling, minor usability improvements for mobile.

* Fix up notifications / alert customizations

* Import v3 variables.

* Fix notification spacing.

* Align input height base with buttons.

* Revert height base, add additional bootstrap overrides.

* Grid overrides.

* Move overrides to their own folder. Adjust naming.

* Fix inconsistencies.

* Style generate wallet pt 1.

* Style generate wallet pt 2

* Style generate wallet pt 3

* Fix swap

* Added some missing overries, fixed the fallout.

* Remove header text, indicate alpha version.

* Fix radio / checkbox weights.

* Bind => arrow

* Convert simpledropdown to proper form select, instead of weirdly implemented nonfuncitoning dropdown.

* Fix token balances buttons, footr icons.

* Break out files, style up account info.

* Style up token balances.

* Equivalent values styling.

* Sidebar promos.

* Fix up delete button and add custom form.

* Even spacing.

* Unlog

* Convert Big types to Ether types

* Fix test to expect Ether instead of Big
This commit is contained in:
William O'Beirne 2017-09-08 15:26:51 -04:00 committed by Daniel Ternyak
parent 38dd22953a
commit 8854d42fd9
26 changed files with 838 additions and 443 deletions

View File

@ -1,6 +1,7 @@
// @flow // @flow
import BaseWallet from 'libs/wallet/base'; import BaseWallet from 'libs/wallet/base';
import Big from 'bignumber.js'; import Big from 'bignumber.js';
import { Wei } from 'libs/units';
/*** Unlock Private Key ***/ /*** Unlock Private Key ***/
export type PrivateKeyUnlockParams = { export type PrivateKeyUnlockParams = {
@ -58,10 +59,10 @@ export function setWallet(value: BaseWallet): SetWalletAction {
/*** Set Balance ***/ /*** Set Balance ***/
export type SetBalanceAction = { export type SetBalanceAction = {
type: 'WALLET_SET_BALANCE', type: 'WALLET_SET_BALANCE',
payload: Big payload: Wei
}; };
export function setBalance(value: Big): SetBalanceAction { export function setBalance(value: Wei): SetBalanceAction {
return { return {
type: 'WALLET_SET_BALANCE', type: 'WALLET_SET_BALANCE',
payload: value payload: value

View File

@ -0,0 +1 @@
<svg width="579" height="126" viewBox="0 0 579 126" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" fill-rule="evenodd"><path d="M37.752 125.873c-18.928 0-37.383-13.566-37.383-44.324 0-30.759 18.455-44.167 37.383-44.167 9.307 0 16.563 2.367 21.768 5.837L53.841 55.68c-3.47-2.524-8.675-4.101-13.88-4.101-11.357 0-21.768 8.991-21.768 29.812s10.726 29.97 21.768 29.97c5.205 0 10.41-1.578 13.88-4.101l5.679 12.776c-5.363 3.628-12.461 5.837-21.768 5.837M102.898 125.873c-24.133 0-37.383-19.087-37.383-44.324 0-25.238 13.25-44.167 37.383-44.167 24.134 0 37.384 18.929 37.384 44.167 0 25.237-13.25 44.324-37.384 44.324zm0-74.768c-13.407 0-20.032 11.988-20.032 30.286 0 18.297 6.625 30.443 20.032 30.443 13.408 0 20.033-12.146 20.033-30.443 0-18.298-6.625-30.286-20.033-30.286zM163.468 23.659c-5.678 0-10.253-4.416-10.253-9.779 0-5.363 4.575-9.78 10.253-9.78s10.253 4.417 10.253 9.78c0 5.363-4.575 9.779-10.253 9.779zm-8.675 15.459h17.351v85.02h-17.351v-85.02zM240.443 124.137V67.352c0-9.937-5.994-16.089-17.824-16.089-6.309 0-12.146 1.104-15.616 2.524v70.35H189.81V43.376c8.518-3.47 19.402-5.994 32.651-5.994 23.819 0 35.333 10.411 35.333 28.393v58.362h-17.351M303.536 125.873c-11.042 0-21.925-2.682-28.55-5.994V.314h17.193v41.012c4.101-1.893 10.726-3.47 16.562-3.47 21.926 0 36.753 15.773 36.753 41.8 0 32.02-16.563 46.217-41.958 46.217zm2.208-74.61c-4.732 0-10.253 1.104-13.565 2.84v55.838c2.524 1.104 7.414 2.208 12.303 2.208 13.723 0 23.819-9.464 23.819-31.231 0-18.613-8.834-29.655-22.557-29.655zM392.341 125.873c-24.449 0-36.752-9.938-36.752-26.658 0-23.66 25.237-27.919 50.948-29.339v-5.363c0-10.726-7.098-14.512-17.982-14.512-8.044 0-17.824 2.524-23.502 5.206l-4.417-11.831c6.783-2.997 18.297-5.994 29.654-5.994 20.348 0 32.652 7.887 32.652 28.866v53.631c-6.152 3.312-18.613 5.994-30.601 5.994zm14.196-44.482c-17.351.946-34.702 2.366-34.702 17.509 0 8.99 6.941 14.511 20.033 14.511 5.521 0 11.988-.946 14.669-2.208V81.391zM461.743 125.873c-9.937 0-20.348-2.682-26.499-5.994l5.836-13.25c4.416 2.681 13.723 5.52 20.19 5.52 9.306 0 15.458-4.574 15.458-11.672 0-7.729-6.467-10.726-15.142-13.881-11.358-4.259-24.134-9.464-24.134-25.395 0-14.039 10.884-23.819 29.812-23.819 10.253 0 18.771 2.524 24.765 5.994l-5.364 11.988c-3.785-2.366-11.356-5.047-17.508-5.047-8.991 0-14.039 4.732-14.039 10.884 0 7.729 6.31 10.41 14.67 13.565 11.83 4.417 24.922 9.306 24.922 25.869 0 15.3-11.672 25.238-32.967 25.238M578.625 81.233l-56.47 7.887c1.735 15.3 11.673 23.029 26.027 23.029 8.517 0 17.666-2.05 23.502-5.205l5.048 12.935c-6.625 3.47-17.982 5.994-29.654 5.994-26.816 0-41.801-17.194-41.801-44.324 0-26.027 14.512-44.167 38.33-44.167 22.083 0 35.175 14.512 35.175 37.384 0 2.05 0 4.259-.157 6.467zm-35.333-31.232c-13.25 0-21.925 10.096-22.241 27.762l41.169-5.679c-.158-14.827-7.571-22.083-18.928-22.083z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1916.3 516.8" width="2500" height="674"><style>.st0{fill:#fff}</style><g id="squares_1_"><path class="st0" d="M578.2 392.7V24.3h25.6v344.1h175.3v24.3H578.2zm327.5 5.1c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5H804.6c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.3 2.6-28.9 3.9-43.5 3.8zM898 135.6c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm238-21.8c19.2 0 37.1 3.8 51.2 10.2 14.1 7.7 26.9 19.2 38.4 37.1h1.3c-1.3-21.7-1.3-42.2-1.3-62.7V0h24.3v392.7h-16.6l-6.4-42.2c-20.5 30.7-51.2 47.3-89.6 47.3s-66.5-11.5-87-35.8c-20.5-23-29.4-57.6-29.4-102.3 0-47.3 10.2-83.2 29.4-108.7 19.2-25.6 48.6-37.2 85.7-37.2zm0 21.8c-29.4 0-52.4 10.2-67.8 32-15.3 20.5-23 51.2-23 92.1 0 78 30.7 116.4 90.8 116.4 30.7 0 53.7-9 67.8-26.9 14.1-17.9 21.7-47.3 21.7-89.6v-3.8c0-42.2-7.7-72.9-21.7-90.8-12.8-20.5-35.8-29.4-67.8-29.4zm379.9-16.6v17.9l-56.3 3.8c15.3 19.2 23 39.7 23 61.4 0 26.9-9 47.3-26.9 64-17.9 16.6-40.9 24.3-70.4 24.3-12.8 0-21.7 0-25.6-1.3-10.2 5.1-17.9 11.5-23 17.9-5.1 7.7-7.7 14.1-7.7 23s3.8 15.3 10.2 19.2c6.4 3.8 17.9 6.4 33.3 6.4h47.3c29.4 0 52.4 6.4 67.8 17.9s24.3 29.4 24.3 53.7c0 29.4-11.5 51.2-34.5 66.5-23 15.3-56.3 23-99.8 23-34.5 0-61.4-6.4-80.6-20.5-19.2-12.8-28.1-32-28.1-55 0-19.2 6.4-34.5 17.9-47.3s28.1-20.5 47.3-25.6c-7.7-3.8-15.3-9-19.2-15.3-5-6.2-7.7-13.8-7.7-21.7 0-17.9 11.5-34.5 34.5-48.6-15.3-6.4-28.1-16.6-37.1-30.7-9-14.1-12.8-30.7-12.8-48.6 0-26.9 9-49.9 25.6-66.5 17.9-16.6 40.9-24.3 70.4-24.3 17.9 0 32 1.3 42.2 5.1h85.7v1.3h.2zm-222.6 319.8c0 37.1 28.1 56.3 84.4 56.3 71.6 0 107.5-23 107.5-69.1 0-16.6-5.1-28.1-16.6-35.8-11.5-7.7-29.4-11.5-55-11.5h-44.8c-49.9 1.2-75.5 20.4-75.5 60.1zm21.8-235.4c0 21.7 6.4 37.1 19.2 49.9 12.8 11.5 29.4 17.9 51.2 17.9 23 0 40.9-6.4 52.4-17.9 12.8-11.5 17.9-28.1 17.9-49.9 0-23-6.4-40.9-19.2-52.4-12.8-11.5-29.4-17.9-52.4-17.9-21.7 0-39.7 6.4-51.2 19.2-12.8 11.4-17.9 29.3-17.9 51.1z"/><path class="st0" d="M1640 397.8c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5h-197c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.1 2.6-28.2 3.8-44.8 3.8zm-6.4-262.2c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm245.6-21.8c11.5 0 24.3 1.3 37.1 3.8l-5.1 24.3c-11.8-2.6-23.8-3.9-35.8-3.8-23 0-42.2 10.2-57.6 29.4-15.3 20.5-23 44.8-23 75.5v149.7h-25.6V119h21.7l2.6 49.9h1.3c11.5-20.5 23-34.5 35.8-42.2 15.4-9 30.7-12.9 48.6-12.9zM333.9 12.8h-183v245.6h245.6V76.7c.1-34.5-28.1-63.9-62.6-63.9zm-239.2 0H64c-34.5 0-64 28.1-64 64v30.7h94.7V12.8zM0 165h94.7v94.7H0V165zm301.9 245.6h30.7c34.5 0 64-28.1 64-64V316h-94.7v94.6zm-151-94.6h94.7v94.7h-94.7V316zM0 316v30.7c0 34.5 28.1 64 64 64h30.7V316H0z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1 @@
<svg width="2568" height="723" viewBox="0 0 2568 723" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="#FFF"><path d="M249 0C149.9 0 69.7 80.2 69.7 179.3v67.2C34.9 252.8 0 261.2 0 272.1v350.7s0 9.7 10.9 14.3c39.5 16 194.9 71 230.6 83.6 4.6 1.7 5.9 1.7 7.1 1.7 1.7 0 2.5 0 7.1-1.7 35.7-12.6 191.5-67.6 231-83.6 10.1-4.2 10.5-13.9 10.5-13.9V272.1c0-10.9-34.4-19.7-69.3-25.6v-67.2C428.4 80.2 347.7 0 249 0zm0 85.7c58.4 0 93.7 35.3 93.7 93.7v58.4c-65.5-4.6-121.4-4.6-187.3 0v-58.4c0-58.5 35.3-93.7 93.6-93.7zm-.4 238.1c81.5 0 149.9 6.3 149.9 17.6v218.8c0 3.4-.4 3.8-3.4 5-2.9 1.3-139 50.4-139 50.4s-5.5 1.7-7.1 1.7c-1.7 0-7.1-2.1-7.1-2.1s-136.1-49.1-139-50.4c-2.9-1.3-3.4-1.7-3.4-5V341c-.8-11.3 67.6-17.2 149.1-17.2zM728.547 562.528V322.922H641V237h272.962v85.922h-86.686v239.606zM1134.394 562.528l-44.92-102.36h-35.745v102.36H955V237h173.755c76.27 0 117.175 50.56 117.175 111.536 0 56.198-32.495 85.922-58.587 98.729l58.97 115.168h-111.919v.095zm11.66-213.992c0-17.681-15.674-25.327-32.113-25.327h-60.212v51.419h60.212c16.44-.382 32.113-8.028 32.113-26.092zM1298 562.528V237h246.87v85.922h-148.523v32.113h144.891v85.922h-144.891v35.745h148.523v85.826zM1596 563.528v-78.275l124.056-161.331H1596V238h254.038v77.511L1725.6 477.702h128.07v85.922l-257.67-.096zM1878 400.594C1878 300.623 1955.511 232 2056.247 232c100.354 0 178.248 68.24 178.248 168.594 0 99.972-77.512 168.212-178.248 168.212-100.736 0-178.247-68.24-178.247-168.212zm256.141 0c0-45.398-30.87-81.525-78.276-81.525-47.405 0-78.276 36.127-78.276 81.525s30.87 81.526 78.276 81.526c47.788 0 78.276-36.128 78.276-81.526zM2455.394 563.528l-44.92-102.36h-35.745v102.36H2276V238h173.755c76.27 0 117.175 50.56 117.175 111.536 0 56.198-32.495 85.922-58.587 98.729l58.97 115.168h-111.919v.095zm12.043-214.374c0-17.682-15.675-25.328-32.113-25.328h-60.213v51.42h60.213c16.534-.383 32.113-8.029 32.113-26.092z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -209,38 +209,6 @@ textarea {
} }
} }
.account-info {
.clearfix;
padding-left: 1em;
margin: 0;
li {
margin-bottom: 0;
list-style-type: none;
word-break: break-all;
}
table& {
font-weight: 200;
border-bottom: 0;
min-width: 200px;
td {
padding: 4px 5px;
line-height: 1;
}
td:first-child {
max-width: 115px;
word-wrap: break-word;
padding-left: 1em;
}
tr:nth-child(even) {
background-color: @gray-lightest;
}
tr:nth-last-child(2),
tr:last-child {
background-color: white !important;
}
}
}
input[type="text"] + .eye { input[type="text"] + .eye {
cursor: pointer; cursor: pointer;
&:before { &:before {
@ -322,10 +290,6 @@ input[type="password"] + .eye {
text-align: right; text-align: right;
} }
.token-balances {
margin-bottom: 15px;
}
h2 a.isActive { h2 a.isActive {
color: #333; color: #333;
cursor: default; cursor: default;

View File

@ -0,0 +1,105 @@
// @flow
import './AccountInfo.scss';
import React from 'react';
import translate from 'translations';
import { Identicon } from 'components/ui';
import { formatNumber } from 'utils/formatters';
import type Big from 'bignumber.js';
import type { BaseWallet } from 'libs/wallet';
import type { NetworkConfig } from 'config/data';
import { Ether } from 'libs/units';
type Props = {
balance: Ether,
wallet: BaseWallet,
network: NetworkConfig
};
export default class AccountInfo extends React.Component {
props: Props;
state = {
showLongBalance: false,
address: ''
};
componentDidMount() {
this.props.wallet.getAddress().then(addr => {
this.setState({ address: addr });
});
}
toggleShowLongBalance = (e: SyntheticMouseEvent) => {
e.preventDefault();
this.setState(state => {
return {
showLongBalance: !state.showLongBalance
};
});
};
render() {
const { network, balance } = this.props;
const { blockExplorer, tokenExplorer } = network;
const { address } = this.state;
return (
<div className="AccountInfo">
<div className="AccountInfo-section">
<h5 className="AccountInfo-section-header">
{translate('sidebar_AccountAddr')}
</h5>
<div className="AccountInfo-address">
<div className="AccountInfo-address-icon">
<Identicon address={address} size="100%" />
</div>
<div className="AccountInfo-address-addr">
{address}
</div>
</div>
</div>
<div className="AccountInfo-section">
<h5 className="AccountInfo-section-header">
{translate('sidebar_AccountBal')}
</h5>
<ul className="AccountInfo-list">
<li className="AccountInfo-list-item">
<span
className="AccountInfo-list-item-clickable mono wrap"
onClick={this.toggleShowLongBalance}
title={`${balance.toString()}`}
>
{this.state.showLongBalance
? balance.toString()
: formatNumber(balance.amount)}
</span>
{` ${network.name}`}
</li>
</ul>
</div>
{(!!blockExplorer || !!tokenExplorer) &&
<div className="AccountInfo-section">
<h5 className="AccountInfo-section-header">
{translate('sidebar_TransHistory')}
</h5>
<ul className="AccountInfo-list">
{!!blockExplorer &&
<li className="AccountInfo-list-item">
<a href={blockExplorer.address(address)} target="_blank">
{`${network.name} (${blockExplorer.name})`}
</a>
</li>}
{!!tokenExplorer &&
<li className="AccountInfo-list-item">
<a href={tokenExplorer.address(address)} target="_blank">
{`Tokens (${tokenExplorer.name})`}
</a>
</li>}
</ul>
</div>}
</div>
);
}
}

View File

@ -0,0 +1,76 @@
@import "common/sass/variables";
@import "common/sass/mixins";
.AccountInfo {
&-section {
margin-top: $space * 1.5;
&:first-child {
margin-top: 0;
}
&-header {
margin-top: 0;
}
}
&-address,
&-list {
padding-left: $space;
}
&-address {
@include clearfix;
&-icon {
float: left;
width: 44px;
height: 44px;
margin-right: $space-md;
}
&-addr {
width: 100%;
word-wrap: break-word;
@include mono;
}
}
&-list {
&-item {
margin-bottom: 0;
list-style-type: none;
word-break: break-all;
&-clickable:hover {
cursor: pointer;
text-decoration: underline;
}
}
}
}
.account-info {
padding-left: 1em;
margin: 0;
li {
}
table {
font-weight: 200;
border-bottom: 0;
min-width: 200px;
td {
padding: 4px 5px;
line-height: 1;
}
td:first-child {
max-width: 115px;
word-wrap: break-word;
padding-left: 1em;
}
tr:nth-last-child(2),
tr:last-child {
background-color: white !important;
}
}
}

View File

@ -1,105 +0,0 @@
// @flow
import React from 'react';
import { isValidETHAddress, isPositiveIntegerOrZero } from 'libs/validators';
import translate from 'translations';
export default class AddCustomTokenForm extends React.Component {
props: {
onSave: ({ address: string, symbol: string, decimal: number }) => void
};
state = {
address: '',
symbol: '',
decimal: ''
};
render() {
return (
<div className="custom-token-fields">
<label>
{translate('TOKEN_Addr')}
</label>
<input
className={
'form-control input-sm ' +
(isValidETHAddress(this.state.address) ? 'is-valid' : 'is-invalid')
}
type="text"
name="address"
value={this.state.address}
onChange={this.onFieldChange}
/>
<label>
{translate('TOKEN_Symbol')}
</label>
<input
className={
'form-control input-sm ' +
(this.state.symbol !== '' ? 'is-valid' : 'is-invalid')
}
type="text"
name="symbol"
value={this.state.symbol}
onChange={this.onFieldChange}
/>
<label>
{translate('TOKEN_Dec')}
</label>
<input
className={
'form-control input-sm ' +
(isPositiveIntegerOrZero(parseInt(this.state.decimal))
? 'is-valid'
: 'is-invalid')
}
type="text"
name="decimal"
value={this.state.decimal}
onChange={this.onFieldChange}
/>
<div
className={`btn btn-primary btn-sm ${this.isValid()
? ''
: 'disabled'}`}
onClick={this.onSave}
>
{translate('x_Save')}
</div>
</div>
);
}
isValid() {
const { address, symbol, decimal } = this.state;
if (!isPositiveIntegerOrZero(parseInt(decimal))) {
return false;
}
if (!isValidETHAddress(address)) {
return false;
}
if (symbol === '') {
return false;
}
return true;
}
onFieldChange = (e: SyntheticInputEvent) => {
var name = e.target.name;
var value = e.target.value;
this.setState(state => {
var newState = Object.assign({}, state);
newState[name] = value;
return newState;
});
};
onSave = () => {
if (!this.isValid()) {
return;
}
const { address, symbol, decimal } = this.state;
this.props.onSave({ address, symbol, decimal: parseInt(decimal) });
};
}

View File

@ -1,192 +0,0 @@
// @flow
import React from 'react';
import Big from 'bignumber.js';
import { BaseWallet } from 'libs/wallet';
import type { NetworkConfig } from 'config/data';
import type { State } from 'reducers';
import { connect } from 'react-redux';
import { getWalletInst, getTokenBalances } from 'selectors/wallet';
import type { TokenBalance } from 'selectors/wallet';
import { getNetworkConfig } from 'selectors/config';
import { Link } from 'react-router';
import TokenBalances from './TokenBalances';
import { formatNumber } from 'utils/formatters';
import { Identicon } from 'components/ui';
import translate from 'translations';
import * as customTokenActions from 'actions/customTokens';
import { showNotification } from 'actions/notifications';
type Props = {
wallet: BaseWallet,
balance: Big,
network: NetworkConfig,
tokenBalances: TokenBalance[],
rates: { [string]: number },
showNotification: Function,
addCustomToken: typeof customTokenActions.addCustomToken,
removeCustomToken: typeof customTokenActions.removeCustomToken
};
export class BalanceSidebar extends React.Component {
props: Props;
state = {
showLongBalance: false,
address: ''
};
componentDidMount() {
this.props.wallet
.getAddress()
.then(addr => {
this.setState({ address: addr });
})
.catch(err => {
this.props.showNotification('danger', err);
});
}
render() {
const { wallet, balance, network, tokenBalances, rates } = this.props;
const { blockExplorer, tokenExplorer } = network;
const { address } = this.state;
if (!wallet) {
return null;
}
return (
<aside>
<h5>
{translate('sidebar_AccountAddr')}
</h5>
<ul className="account-info">
<Identicon address={address} />
<span className="mono wrap">
{address}
</span>
</ul>
<hr />
<h5>
{translate('sidebar_AccountBal')}
</h5>
<ul
className="account-info point"
onDoubleClick={this.toggleShowLongBalance}
title={`${balance.toString()} (Double-Click)`}
>
<li>
<span className="mono wrap">
{this.state.showLongBalance
? balance.toString()
: formatNumber(balance)}
</span>
{` ${network.name}`}
</li>
</ul>
<TokenBalances
tokens={tokenBalances}
onAddCustomToken={this.props.addCustomToken}
onRemoveCustomToken={this.props.removeCustomToken}
/>
<hr />
{(!!blockExplorer || !!tokenExplorer) &&
<div>
<h5>
{translate('sidebar_TransHistory')}
</h5>
<ul className="account-info">
{!!blockExplorer &&
<li>
<a href={blockExplorer.address(address)} target="_blank">
{`${network.name} (${blockExplorer.name})`}
</a>
</li>}
{!!tokenExplorer &&
<li>
<a href={tokenExplorer.address(address)} target="_blank">
{`Tokens (${tokenExplorer.name})`}
</a>
</li>}
</ul>
</div>}
<hr />
{!!Object.keys(rates).length &&
<section>
<h5>
{translate('sidebar_Equiv')}
</h5>
<ul className="account-info">
{rates['BTC'] &&
<li>
<span className="mono wrap">
{formatNumber(balance.times(rates['BTC']))}
</span>{' '}
BTC
</li>}
{rates['REP'] &&
<li>
<span className="mono wrap">
{formatNumber(balance.times(rates['REP']), 2)}
</span>{' '}
REP
</li>}
{rates['EUR'] &&
<li>
<span className="mono wrap">
{formatNumber(balance.times(rates['EUR']), 2)}
</span>
{' EUR'}
</li>}
{rates['USD'] &&
<li>
<span className="mono wrap">
${formatNumber(balance.times(rates['USD']), 2)}
</span>
{' USD'}
</li>}
{rates['GBP'] &&
<li>
<span className="mono wrap">
£{formatNumber(balance.times(rates['GBP']), 2)}
</span>
{' GBP'}
</li>}
{rates['CHF'] &&
<li>
<span className="mono wrap">
{formatNumber(balance.times(rates['CHF']), 2)}
</span>{' '}
CHF
</li>}
</ul>
<Link to={'swap'} className="btn btn-primary btn-sm">
Swap via bity
</Link>
</section>}
</aside>
);
}
toggleShowLongBalance = (e: SyntheticMouseEvent) => {
e.preventDefault();
this.setState(state => {
return {
showLongBalance: !state.showLongBalance
};
});
};
}
function mapStateToProps(state: State) {
return {
wallet: getWalletInst(state),
balance: state.wallet.balance,
tokenBalances: getTokenBalances(state),
network: getNetworkConfig(state),
rates: state.rates
};
}
export default connect(mapStateToProps, {
...customTokenActions,
showNotification
})(BalanceSidebar);

View File

@ -0,0 +1,47 @@
// @flow
import './EquivalentValues.scss';
import React from 'react';
import translate from 'translations';
import { Link } from 'react-router';
import { formatNumber } from 'utils/formatters';
import type Big from 'bignumber.js';
import { Ether } from 'libs/units';
const ratesKeys = ['BTC', 'REP', 'EUR', 'USD', 'GBP', 'CHF'];
type Props = {
balance: Ether,
rates: { [string]: number }
};
export default class EquivalentValues extends React.Component {
props: Props;
render() {
const { balance, rates } = this.props;
return (
<div className="EquivalentValues">
<h5 className="EquivalentValues-title">
{translate('sidebar_Equiv')}
</h5>
<ul className="EquivalentValues-values">
{ratesKeys.map(key => {
if (!rates[key]) return null;
return (
<li className="EquivalentValues-values-currency" key={key}>
<span className="EquivalentValues-values-currency-label">
{key}:
</span>
<span className="EquivalentValues-values-currency-value">
{' '}{formatNumber(balance.amount.times(rates[key]))}
</span>
</li>
);
})}
</ul>
</div>
);
}
}

View File

@ -0,0 +1,37 @@
@import "common/sass/variables";
@import "common/sass/mixins";
.EquivalentValues {
&-title {
margin-top: 0;
margin-bottom: $space;
}
&-values {
list-style: none;
padding: 0;
@include clearfix;
&-currency {
float: left;
width: 50%;
margin-bottom: $space-xs;
&:nth-child(odd) {
padding-right: $space-sm;
}
&:nth-child(even) {
padding-left: $space-sm;
}
&-label {
display: inline-block;
min-width: 36px;
}
&-value {
font-weight: 600;
@include mono;
}
}
}
}

View File

@ -0,0 +1,104 @@
// @flow
import './Promos.scss';
import React from 'react';
import { Link } from 'react-router';
const promos = [
{
color: '#6e9a3e',
href:
'https://myetherwallet.groovehq.com/knowledge_base/topics/protecting-yourself-and-your-funds',
isExternal: true,
texts: [<h6 key="1">Learn more about protecting your funds.</h6>],
images: [
require('assets/images/logo-ledger.svg'),
require('assets/images/logo-trezor.svg')
]
},
{
color: '#2b71b1',
href:
'https://buy.coinbase.com?code=a6e1bd98-6464-5552-84dd-b27f0388ac7d&address=0xA7DeFf12461661212734dB35AdE9aE7d987D648c&crypto_currency=ETH&currency=USD',
isExternal: true,
texts: [
<p key="1">Its now easier to get more ETH</p>,
<h5 key="2">Buy ETH with USD</h5>
],
images: [require('assets/images/logo-coinbase.svg')]
},
{
color: '#006e79',
href: '/swap',
texts: [
<p key="1">Its now easier to get more ETH</p>,
<h5 key="2">Swap BTC &lt;-&gt; ETH</h5>
],
images: [require('assets/images/logo-bity-white.svg')]
}
];
export default class Promos extends React.Component {
state: { activePromo: number };
state = {
activePromo: parseInt(Math.random() * promos.length)
};
_navigateToPromo = (idx: number) => {
this.setState({ activePromo: Math.max(0, Math.min(promos.length, idx)) });
};
render() {
const { activePromo } = this.state;
const promo = promos[activePromo];
const promoContent = (
<div className="Promos-promo-inner">
<div className="Promos-promo-text">
{promo.texts}
</div>
<div className="Promos-promo-images">
{promo.images.map((img, idx) => <img src={img} key={idx} />)}
</div>
</div>
);
const promoEl = promo.isExternal
? <a
className="Promos-promo"
key={promo.href}
href={promo.href}
style={{ backgroundColor: promo.color }}
>
{promoContent}
</a>
: <Link
className="Promos-promo"
key={promo.href}
to={promo.href}
style={{ backgroundColor: promo.color }}
>
<div className="Promos-promo-inner">
{promoContent}
</div>
</Link>;
return (
<div className="Promos">
{promoEl}
<div className="Promos-nav">
{promos.map((promo, idx) => {
return (
<button
className={`Promos-nav-btn ${idx === activePromo
? 'is-active'
: ''}`}
key={idx}
onClick={() => this._navigateToPromo(idx)}
/>
);
})}
</div>
</div>
);
}
}

View File

@ -0,0 +1,90 @@
@import "common/sass/variables";
@import "common/sass/mixins";
.Promos {
&-promo {
position: relative;
height: 6rem;
display: block;
color: #fff;
text-decoration: none;
text-align: center;
transition-duration: 200ms;
@include clearfix;
&:hover,
&:focus,
&:active {
color: #fff;
opacity: 0.85;
}
&-inner {
position: absolute;
display: flex;
align-items: center;
top: 50%;
left: 0;
width: 100%;
transform: translateY(-50%);
}
&-text,
&-images {
padding: 0 $space-sm;
}
&-text {
flex: 1;
p,
h4,
h5,
h6 {
margin: .15rem 0;
}
p {
font-size: 0.8rem;
}
h5 {
font-size: 1.3rem;
}
}
&-images {
padding: 0 $space * 1.5;
img {
display: block;
margin: 0 auto;
width: 100%;
max-width: 96px;
height: auto;
padding: $space-xs;
}
}
}
&-nav {
text-align: center;
&-btn {
@include reset-button;
display: inline-block;
margin: 0 $space-xs;
background: $gray-dark;
width: 12px;
height: 12px;
border: 3px solid $gray-lightest;
border-radius: 100%;
outline: none;
opacity: 0.6;
&.is-active {
opacity: 1;
}
}
}
}

View File

@ -1,87 +0,0 @@
// @flow
import React from 'react';
import translate from 'translations';
import TokenRow from './TokenRow';
import AddCustomTokenForm from './AddCustomTokenForm';
import type { TokenBalance } from 'selectors/wallet';
import type { Token } from 'config/data';
type Props = {
tokens: TokenBalance[],
onAddCustomToken: (token: Token) => any,
onRemoveCustomToken: (symbol: string) => any
};
export default class TokenBalances extends React.Component {
props: Props;
state = {
showAllTokens: false,
showCustomTokenForm: false
};
render() {
const { tokens } = this.props;
return (
<section className="token-balances">
<h5>{translate('sidebar_TokenBal')}</h5>
<table className="account-info">
<tbody>
{tokens
.filter(
token =>
!token.balance.eq(0) ||
token.custom ||
this.state.showAllTokens
)
.map(token =>
<TokenRow
key={token.symbol}
balance={token.balance}
symbol={token.symbol}
custom={token.custom}
onRemove={this.props.onRemoveCustomToken}
/>
)}
</tbody>
</table>
<a
className="btn btn-default btn-xs"
onClick={this.toggleShowAllTokens}
>
{!this.state.showAllTokens ? 'Show All Tokens' : 'Hide Tokens'}
</a>{' '}
<a
className="btn btn-default btn-xs"
onClick={this.toggleShowCustomTokenForm}
>
<span>
{translate('SEND_custom')}
</span>
</a>
{this.state.showCustomTokenForm &&
<AddCustomTokenForm onSave={this.addCustomToken} />}
</section>
);
}
toggleShowAllTokens = () => {
this.setState(state => {
return {
showAllTokens: !state.showAllTokens
};
});
};
toggleShowCustomTokenForm = () => {
this.setState(state => {
return {
showCustomTokenForm: !state.showCustomTokenForm
};
});
};
addCustomToken = (token: Token) => {
this.props.onAddCustomToken(token);
this.setState({ showCustomTokenForm: false });
};
}

View File

@ -0,0 +1,108 @@
// @flow
import React from 'react';
import classnames from 'classnames';
import { isValidETHAddress, isPositiveIntegerOrZero } from 'libs/validators';
import translate from 'translations';
export default class AddCustomTokenForm extends React.Component {
props: {
onSave: ({ address: string, symbol: string, decimal: number }) => void
};
state = {
address: '',
symbol: '',
decimal: ''
};
render() {
const { address, symbol, decimal } = this.state;
const inputClasses = 'AddCustom-field-input form-control input-sm';
const errors = this.getErrors();
const fields = [
{
name: 'address',
value: address,
label: translate('TOKEN_Addr')
},
{
name: 'symbol',
value: symbol,
label: translate('TOKEN_Symbol')
},
{
name: 'decimal',
value: decimal,
label: translate('TOKEN_Dec')
}
];
return (
<form className="AddCustom" onSubmit={this.onSave}>
{fields.map(field => {
return (
<label className="AddCustom-field form-group" key={field.name}>
<span className="AddCustom-field-label">
{field.label}
</span>
<input
className={classnames(
inputClasses,
errors[field.name] ? 'is-invalid' : 'is-valid'
)}
type="text"
name={field.name}
value={field.value}
onChange={this.onFieldChange}
/>
</label>
);
})}
<button
className="btn btn-primary btn-sm btn-block"
disabled={!this.isValid()}
>
{translate('x_Save')}
</button>
</form>
);
}
getErrors() {
const { address, symbol, decimal } = this.state;
const errors = {};
if (!isPositiveIntegerOrZero(parseInt(decimal, 10))) {
errors.decimal = true;
}
if (!isValidETHAddress(address)) {
errors.address = true;
}
if (!symbol) {
errors.symbol = true;
}
return errors;
}
isValid() {
return !Object.keys(this.getErrors()).length;
}
onFieldChange = (e: SyntheticInputEvent) => {
var name = e.target.name;
var value = e.target.value;
this.setState({ [name]: value });
};
onSave = (ev: SyntheticInputEvent) => {
ev.preventDefault();
if (!this.isValid()) {
return;
}
const { address, symbol, decimal } = this.state;
this.props.onSave({ address, symbol, decimal: parseInt(decimal, 10) });
};
}

View File

@ -1,4 +1,5 @@
// @flow // @flow
import './TokenRow.scss';
import React from 'react'; import React from 'react';
import Big from 'bignumber.js'; import Big from 'bignumber.js';
import { formatNumber } from 'utils/formatters'; import { formatNumber } from 'utils/formatters';
@ -19,24 +20,25 @@ export default class TokenRow extends React.Component {
const { balance, symbol, custom } = this.props; const { balance, symbol, custom } = this.props;
const { showLongBalance } = this.state; const { showLongBalance } = this.state;
return ( return (
<tr> <tr className="TokenRow">
<td <td
className="mono wrap point" className="TokenRow-balance"
title={`${balance.toString()} (Double-Click)`} title={`${balance.toString()} (Double-Click)`}
onDoubleClick={this.toggleShowLongBalance} onDoubleClick={this.toggleShowLongBalance}
> >
{!!custom && {!!custom &&
<img <img
src={removeIcon} src={removeIcon}
className="token-remove" className="TokenRow-balance-remove"
title="Remove Token" title="Remove Token"
onClick={this.onRemove} onClick={this.onRemove}
tabIndex="0"
/>} />}
<span> <span>
{showLongBalance ? balance.toString() : formatNumber(balance)} {showLongBalance ? balance.toString() : formatNumber(balance)}
</span> </span>
</td> </td>
<td> <td className="TokenRow-symbol">
{symbol} {symbol}
</td> </td>
</tr> </tr>

View File

@ -0,0 +1,31 @@
@import "common/sass/variables";
@import "common/sass/mixins";
.TokenRow {
border-bottom: 1px solid $gray-lighter;
&-balance,
&-symbol {
padding: $space-xs 0 $space-xs $space-md;
}
&-balance {
@include mono;
&-remove {
margin-left: -32px;
margin-right: 20px;
height: 12px;
cursor: pointer;
opacity: 0.4;
&:hover {
opacity: 1;
}
}
}
&-symbol {
font-weight: 300;
}
}

View File

@ -0,0 +1,93 @@
// @flow
import './index.scss';
import React from 'react';
import translate from 'translations';
import TokenRow from './TokenRow';
import AddCustomTokenForm from './AddCustomTokenForm';
import type { TokenBalance } from 'selectors/wallet';
import type { Token } from 'config/data';
type Props = {
tokens: TokenBalance[],
onAddCustomToken: (token: Token) => any,
onRemoveCustomToken: (symbol: string) => any
};
export default class TokenBalances extends React.Component {
props: Props;
state = {
showAllTokens: false,
showCustomTokenForm: false
};
render() {
const { tokens } = this.props;
const shownTokens = tokens.filter(
token => !token.balance.eq(0) || token.custom || this.state.showAllTokens
);
return (
<section className="TokenBalances">
<h5 className="TokenBalances-title">
{translate('sidebar_TokenBal')}
</h5>
<table className="TokenBalances-rows">
<tbody>
{shownTokens.map(token =>
<TokenRow
key={token.symbol}
balance={token.balance}
symbol={token.symbol}
custom={token.custom}
onRemove={this.props.onRemoveCustomToken}
/>
)}
</tbody>
</table>
<div className="TokenBalances-buttons">
<button
className="btn btn-default btn-xs"
onClick={this.toggleShowAllTokens}
>
{!this.state.showAllTokens ? 'Show All Tokens' : 'Hide Tokens'}
</button>{' '}
<button
className="btn btn-default btn-xs"
onClick={this.toggleShowCustomTokenForm}
>
<span>
{translate('SEND_custom')}
</span>
</button>
</div>
{this.state.showCustomTokenForm &&
<div className="TokenBalances-form">
<AddCustomTokenForm onSave={this.addCustomToken} />
</div>}
</section>
);
}
toggleShowAllTokens = () => {
this.setState(state => {
return {
showAllTokens: !state.showAllTokens
};
});
};
toggleShowCustomTokenForm = () => {
this.setState(state => {
return {
showCustomTokenForm: !state.showCustomTokenForm
};
});
};
addCustomToken = (token: Token) => {
this.props.onAddCustomToken(token);
this.setState({ showCustomTokenForm: false });
};
}

View File

@ -0,0 +1,18 @@
@import "common/sass/variables";
.TokenBalances {
&-title {
margin-top: 0;
}
&-rows {
width: 100%;
margin-bottom: $space;
}
&-form {
margin-top: $space * 2;
padding-top: $space;
border-top: 1px solid $gray-lighter;
}
}

View File

@ -1,3 +0,0 @@
// @flow
export { default } from './BalanceSidebar';

View File

@ -0,0 +1,96 @@
// @flow
import React from 'react';
import Big from 'bignumber.js';
import { BaseWallet } from 'libs/wallet';
import type { NetworkConfig } from 'config/data';
import type { State } from 'reducers';
import { connect } from 'react-redux';
import { getWalletInst, getTokenBalances } from 'selectors/wallet';
import type { TokenBalance } from 'selectors/wallet';
import { getNetworkConfig } from 'selectors/config';
import * as customTokenActions from 'actions/customTokens';
import { showNotification } from 'actions/notifications';
import AccountInfo from './AccountInfo';
import Promos from './Promos';
import TokenBalances from './TokenBalances';
import EquivalentValues from './EquivalentValues';
import { Ether } from 'libs/units';
type Props = {
wallet: BaseWallet,
balance: Ether,
network: NetworkConfig,
tokenBalances: TokenBalance[],
rates: { [string]: number },
showNotification: Function,
addCustomToken: typeof customTokenActions.addCustomToken,
removeCustomToken: typeof customTokenActions.removeCustomToken
};
export class BalanceSidebar extends React.Component {
props: Props;
render() {
const { wallet, balance, network, tokenBalances, rates } = this.props;
if (!wallet) {
return null;
}
const blocks = [
{
name: 'Account Info',
content: (
<AccountInfo wallet={wallet} balance={balance} network={network} />
)
},
{
name: 'Promos',
isFullWidth: true,
content: <Promos />
},
{
name: 'Token Balances',
content: (
<TokenBalances
tokens={tokenBalances}
onAddCustomToken={this.props.addCustomToken}
onRemoveCustomToken={this.props.removeCustomToken}
/>
)
},
{
name: 'Equivalent Values',
content: <EquivalentValues balance={balance} rates={rates} />
}
];
return (
<aside>
{blocks.map(block =>
<section
className={`Block ${block.isFullWidth ? 'is-full-width' : ''}`}
key={block.name}
>
{block.content}
</section>
)}
</aside>
);
}
}
function mapStateToProps(state: State) {
return {
wallet: getWalletInst(state),
balance: state.wallet.balance,
tokenBalances: getTokenBalances(state),
network: getNetworkConfig(state),
rates: state.rates
};
}
export default connect(mapStateToProps, {
...customTokenActions,
showNotification
})(BalanceSidebar);

View File

@ -285,13 +285,7 @@ export class SendTransaction extends React.Component {
{/* Sidebar */} {/* Sidebar */}
{unlocked && {unlocked &&
<section className="col-sm-4"> <section className="col-sm-4">
<div className="Tab-content-pane">
<div>
<BalanceSidebar /> <BalanceSidebar />
<hr />
<Donate onDonate={this.onNewTx} />
</div>
</div>
</section>} </section>}
</div> </div>
@ -512,7 +506,7 @@ export class SendTransaction extends React.Component {
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState) {
return { return {
wallet: state.wallet.inst, wallet: state.wallet.inst,
balance: new Ether(state.wallet.balance), balance: state.wallet.balance,
tokenBalances: getTokenBalances(state), tokenBalances: getTokenBalances(state),
node: getNodeConfig(state), node: getNodeConfig(state),
nodeLib: getNodeLib(state), nodeLib: getNodeLib(state),

View File

@ -10,10 +10,12 @@ import { toUnit } from 'libs/units';
import Big from 'bignumber.js'; import Big from 'bignumber.js';
import { getTxFromBroadcastTransactionStatus } from 'selectors/wallet'; import { getTxFromBroadcastTransactionStatus } from 'selectors/wallet';
import type { BroadcastTransactionStatus } from 'libs/transaction'; import type { BroadcastTransactionStatus } from 'libs/transaction';
import { Ether } from 'libs/units';
export type State = { export type State = {
inst: ?BaseWallet, inst: ?BaseWallet,
// in ETH // in ETH
balance: Big, balance: Ether,
tokens: { tokens: {
[string]: Big [string]: Big
}, },
@ -22,18 +24,18 @@ export type State = {
export const INITIAL_STATE: State = { export const INITIAL_STATE: State = {
inst: null, inst: null,
balance: new Big(0), balance: new Ether(0),
tokens: {}, tokens: {},
isBroadcasting: false, isBroadcasting: false,
transactions: [] transactions: []
}; };
function setWallet(state: State, action: SetWalletAction): State { function setWallet(state: State, action: SetWalletAction): State {
return { ...state, inst: action.payload, balance: new Big(0), tokens: {} }; return { ...state, inst: action.payload, balance: new Ether(0), tokens: {} };
} }
function setBalance(state: State, action: SetBalanceAction): State { function setBalance(state: State, action: SetBalanceAction): State {
const ethBalance = toUnit(action.payload, 'wei', 'ether'); const ethBalance = action.payload.toEther();
return { ...state, balance: ethBalance }; return { ...state, balance: ethBalance };
} }

View File

@ -39,7 +39,7 @@ function* updateAccountBalance(): Generator<Yield, Return, Next> {
const address = yield wallet.getAddress(); const address = yield wallet.getAddress();
// network request // network request
let balance: Wei = yield apply(node, node.getBalance, [address]); let balance: Wei = yield apply(node, node.getBalance, [address]);
yield put(setBalance(balance.amount)); yield put(setBalance(balance));
} catch (error) { } catch (error) {
yield put({ type: 'updateAccountBalance_error', error }); yield put({ type: 'updateAccountBalance_error', error });
} }

View File

@ -10,6 +10,16 @@
min-height: 1.5rem; min-height: 1.5rem;
padding: 1.5rem 2rem; padding: 1.5rem 2rem;
margin: 0 auto 1rem; margin: 0 auto 1rem;
&.is-full-width {
background: none;
box-shadow: none;
padding: 0;
} }
} }
} }
}
.Block {
@extend .Tab-content-pane;
}

View File

@ -1,6 +1,7 @@
import { wallet, INITIAL_STATE } from 'reducers/wallet'; import { wallet, INITIAL_STATE } from 'reducers/wallet';
import * as walletActions from 'actions/wallet'; import * as walletActions from 'actions/wallet';
import Big from 'bignumber.js'; import Big from 'bignumber.js';
import { Ether } from 'libs/units';
describe('wallet reducer', () => { describe('wallet reducer', () => {
it('should return the initial state', () => { it('should return the initial state', () => {
@ -12,7 +13,7 @@ describe('wallet reducer', () => {
expect(wallet(undefined, walletActions.setWallet(walletInstance))).toEqual({ expect(wallet(undefined, walletActions.setWallet(walletInstance))).toEqual({
...INITIAL_STATE, ...INITIAL_STATE,
inst: walletInstance, inst: walletInstance,
balance: new Big(0), balance: new Ether(0),
tokens: {} tokens: {}
}); });
}); });