Equivalent Values for Tokens (#366)

* Add select with tokens.

* Reformat state shape to allow multiple currency rates. Show those rates when selected.

* Add loader
This commit is contained in:
William O'Beirne 2017-11-14 22:51:09 -07:00 committed by Daniel Ternyak
parent 151b2d762f
commit 8bc1a348c7
8 changed files with 167 additions and 67 deletions

View File

@ -3,9 +3,9 @@ import { TypeKeys } from './constants';
import { fetchRates } from './actionPayloads';
export type TFetchCCRates = typeof fetchCCRates;
export function fetchCCRates(): interfaces.FetchCCRates {
export function fetchCCRates(symbol: string): interfaces.FetchCCRates {
return {
type: TypeKeys.RATES_FETCH_CC,
payload: fetchRates()
payload: fetchRates(symbol)
};
}

View File

@ -1,22 +1,31 @@
import { handleJSONResponse } from 'api/utils';
export const symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP'];
const symbolsURL = symbols.join(',');
export const rateSymbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH'];
const rateSymbolsArg = rateSymbols.join(',');
// TODO - internationalize
const ERROR_MESSAGE = 'Could not fetch rate data.';
const CCApi = 'https://min-api.cryptocompare.com';
const CCRates = CCSymbols => `${CCApi}/data/price?fsym=ETH&tsyms=${CCSymbols}`;
const CCRates = (symbol: string) => {
return `${CCApi}/data/price?fsym=${symbol}&tsyms=${rateSymbolsArg}`;
};
export interface CCResponse {
BTC: number;
EUR: number;
GBP: number;
CHF: number;
REP: number;
symbol: string;
rates: {
BTC: number;
EUR: number;
GBP: number;
CHF: number;
REP: number;
ETH: number;
};
}
export const fetchRates = (): Promise<CCResponse> =>
fetch(CCRates(symbolsURL)).then(response =>
handleJSONResponse(response, ERROR_MESSAGE)
);
export const fetchRates = (symbol: string): Promise<CCResponse> =>
fetch(CCRates(symbol))
.then(response => handleJSONResponse(response, ERROR_MESSAGE))
.then(rates => ({
symbol,
rates
}));

View File

@ -1,4 +1,3 @@
import { TFetchCCRates } from 'actions/rates';
import { Identicon, UnitDisplay } from 'components/ui';
import { NetworkConfig } from 'config/data';
import { IWallet, Balance } from 'libs/wallet';
@ -11,7 +10,6 @@ interface Props {
balance: Balance;
wallet: IWallet;
network: NetworkConfig;
fetchCCRates: TFetchCCRates;
}
interface State {
@ -32,7 +30,6 @@ export default class AccountInfo extends React.Component<Props, State> {
}
public componentDidMount() {
this.props.fetchCCRates();
this.setAddressFromWallet();
}

View File

@ -34,5 +34,10 @@
@include mono;
}
}
&-loader {
padding: 25px 0;
text-align: center;
}
}
}

View File

@ -1,56 +1,139 @@
import React from 'react';
import * as React from 'react';
import translate from 'translations';
import './EquivalentValues.scss';
import { State } from 'reducers/rates';
import { symbols } from 'actions/rates';
import { UnitDisplay } from 'components/ui';
import { rateSymbols, TFetchCCRates } from 'actions/rates';
import { TokenBalance } from 'selectors/wallet';
import { Balance } from 'libs/wallet';
import Spinner from 'components/ui/Spinner';
import UnitDisplay from 'components/ui/UnitDisplay';
import './EquivalentValues.scss';
interface Props {
balance: Balance;
rates?: State['rates'];
balance?: Balance;
tokenBalances?: TokenBalance[];
rates: State['rates'];
ratesError?: State['ratesError'];
fetchCCRates: TFetchCCRates;
}
export default class EquivalentValues extends React.Component<Props, {}> {
interface CmpState {
currency: string;
}
export default class EquivalentValues extends React.Component<Props, CmpState> {
public state = {
currency: 'ETH'
};
private balanceLookup = {};
public constructor(props) {
super(props);
this.makeBalanceLookup(props);
}
public componentDidMount() {
this.props.fetchCCRates(this.state.currency);
}
public componentWillReceiveProps(nextProps) {
const { balance, tokenBalances } = this.props;
if (
nextProps.balance !== balance ||
nextProps.tokenBalances !== tokenBalances
) {
this.makeBalanceLookup(nextProps);
}
}
public render() {
const { balance, rates, ratesError } = this.props;
const { tokenBalances, rates, ratesError } = this.props;
const { currency } = this.state;
const balance = this.balanceLookup[currency];
let values;
if (balance && rates && rates[currency]) {
values = rateSymbols.map(key => {
if (!rates[currency][key] || key === currency) {
return null;
}
return (
<li className="EquivalentValues-values-currency" key={key}>
<span className="EquivalentValues-values-currency-label">
{key}:
</span>{' '}
<span className="EquivalentValues-values-currency-value">
{balance.isPending ? (
<Spinner />
) : (
<UnitDisplay
unit={'ether'}
value={balance ? balance.muln(rates[currency][key]) : null}
displayShortBalance={2}
/>
)}
</span>
</li>
);
});
} else if (ratesError) {
values = <h5>{ratesError}</h5>;
} else {
values = (
<div className="EquivalentValues-values-loader">
<Spinner size="x3" />
</div>
);
}
return (
<div className="EquivalentValues">
<h5 className="EquivalentValues-title">{translate('sidebar_Equiv')}</h5>
<ul className="EquivalentValues-values">
{rates
? symbols.map(key => {
if (!rates[key]) {
return null;
<h5 className="EquivalentValues-title">
{translate('sidebar_Equiv')} for{' '}
<select
className="EquivalentValues-title-symbol"
onChange={this.changeCurrency}
value={currency}
>
<option value="ETH">ETH</option>
{tokenBalances &&
tokenBalances.map(tk => {
if (!tk.balance || tk.balance.isZero()) {
return;
}
const sym = tk.symbol;
return (
<li className="EquivalentValues-values-currency" key={key}>
<span className="EquivalentValues-values-currency-label">
{key + ': '}
</span>
<span className="EquivalentValues-values-currency-value">
{balance.isPending ? (
<Spinner />
) : (
<UnitDisplay
unit={'ether'}
value={
balance.wei ? balance.wei.muln(rates[key]) : null
}
displayShortBalance={2}
/>
)}
</span>
</li>
<option key={sym} value={sym}>
{sym}
</option>
);
})
: ratesError && <h5>{ratesError}</h5>}
</ul>
})}
</select>
</h5>
<ul className="EquivalentValues-values">{values}</ul>
</div>
);
}
private changeCurrency = (ev: React.FormEvent<HTMLSelectElement>) => {
const currency = ev.currentTarget.value;
this.setState({ currency });
if (!this.props.rates || !this.props.rates[currency]) {
this.props.fetchCCRates(currency);
}
};
private makeBalanceLookup(props: Props) {
const tokenBalances = props.tokenBalances || [];
this.balanceLookup = tokenBalances.reduce(
(prev, tk) => {
return {
...prev,
[tk.symbol]: tk.balance
};
},
{ ETH: props.balance && props.balance.wei }
);
}
}

View File

@ -66,12 +66,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
{
name: 'Account Info',
content: (
<AccountInfo
wallet={wallet}
balance={balance}
network={network}
fetchCCRates={fetchCCRates}
/>
<AccountInfo wallet={wallet} balance={balance} network={network} />
)
},
{
@ -94,8 +89,10 @@ export class BalanceSidebar extends React.Component<Props, {}> {
content: (
<EquivalentValues
balance={balance}
tokenBalances={tokenBalances}
rates={rates}
ratesError={ratesError}
fetchCCRates={fetchCCRates}
/>
)
}

View File

@ -66,7 +66,9 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
{/*Amount to send*/}
{!this.isExpanded() && (
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">{` ${originAmount} ${originKind}`}</h3>
<h3 className="SwapInfo-details-block-value">{` ${originAmount} ${
originKind
}`}</h3>
<p className="SwapInfo-details-block-label">
{translate('SEND_amount')}
</p>
@ -109,9 +111,9 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">
{`${computedOriginDestinationRatio &&
toFixedIfLarger(
computedOriginDestinationRatio
)} ${destinationKind}/${originKind}`}
toFixedIfLarger(computedOriginDestinationRatio)} ${
destinationKind
}/${originKind}`}
</h3>
<p className="SwapInfo-details-block-label">
{translate('SWAP_your_rate')}

View File

@ -1,20 +1,27 @@
import { FetchCCRatesSucceeded, RatesAction, CCResponse } from 'actions/rates';
import { TypeKeys } from 'actions/rates/constants';
import { Optional } from 'utils/types';
// SYMBOL -> PRICE TO BUY 1 ETH
export interface State {
rates?: Optional<CCResponse>;
rates: { [symbol: string]: CCResponse['rates'] };
ratesError?: string | null;
}
export const INITIAL_STATE: State = {};
export const INITIAL_STATE: State = {
rates: {}
};
function fetchCCRatesSucceeded(
state: State,
action: FetchCCRatesSucceeded
): State {
return { ...state, rates: action.payload };
return {
...state,
rates: {
...state.rates,
[action.payload.symbol]: action.payload.rates
}
};
}
function fetchCCRatesFailed(state: State): State {