diff options
Diffstat (limited to 'packages/website/ts/components')
-rw-r--r-- | packages/website/ts/components/legacy_portal/legacy_portal.tsx (renamed from packages/website/ts/components/portal.tsx) | 67 | ||||
-rw-r--r-- | packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx (renamed from packages/website/ts/components/portal_menu.tsx) | 28 | ||||
-rw-r--r-- | packages/website/ts/components/portal/portal.tsx | 297 | ||||
-rw-r--r-- | packages/website/ts/components/relayer_index/relayer_grid_tile.tsx | 77 | ||||
-rw-r--r-- | packages/website/ts/components/relayer_index/relayer_index.tsx | 70 | ||||
-rw-r--r-- | packages/website/ts/components/relayer_index/relayer_top_tokens.tsx | 16 | ||||
-rw-r--r-- | packages/website/ts/components/top_bar/provider_display.tsx | 36 | ||||
-rw-r--r-- | packages/website/ts/components/top_bar/top_bar.tsx | 100 | ||||
-rw-r--r-- | packages/website/ts/components/ui/input_label.tsx | 2 | ||||
-rw-r--r-- | packages/website/ts/components/wallet/wallet.tsx | 123 |
10 files changed, 594 insertions, 222 deletions
diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/legacy_portal/legacy_portal.tsx index b79f5e288..c45b20365 100644 --- a/packages/website/ts/components/portal.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal.tsx @@ -14,7 +14,7 @@ import { WrappedEthSectionNoticeDialog } from 'ts/components/dialogs/wrapped_eth import { EthWrappers } from 'ts/components/eth_wrappers'; import { FillOrder } from 'ts/components/fill_order'; import { Footer } from 'ts/components/footer'; -import { PortalMenu } from 'ts/components/portal_menu'; +import { LegacyPortalMenu } from 'ts/components/legacy_portal/legacy_portal_menu'; import { RelayerIndex } from 'ts/components/relayer_index/relayer_index'; import { TokenBalances } from 'ts/components/token_balances'; import { TopBar } from 'ts/components/top_bar/top_bar'; @@ -43,9 +43,7 @@ import { utils } from 'ts/utils/utils'; const THROTTLE_TIMEOUT = 100; -export interface PortalPassedProps {} - -export interface PortalAllProps { +export interface LegacyPortalProps { blockchainErr: BlockchainErrs; blockchainIsLoaded: boolean; dispatcher: Dispatcher; @@ -67,7 +65,7 @@ export interface PortalAllProps { translate: Translate; } -interface PortalAllState { +interface LegacyPortalState { prevNetworkId: number; prevNodeVersion: string; prevUserAddress: string; @@ -77,7 +75,7 @@ interface PortalAllState { isLedgerDialogOpen: boolean; } -export class Portal extends React.Component<PortalAllProps, PortalAllState> { +export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPortalState> { private _blockchain: Blockchain; private _sharedOrderIfExists: Order; private _throttledScreenWidthUpdate: () => void; @@ -86,13 +84,13 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { const hasAlreadyDismissedWethNotice = !_.isUndefined(didDismissWethNotice) && !_.isEmpty(didDismissWethNotice); return hasAlreadyDismissedWethNotice; } - constructor(props: PortalAllProps) { + constructor(props: LegacyPortalProps) { super(props); this._sharedOrderIfExists = this._getSharedOrderIfExists(); this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); const isViewingBalances = _.includes(props.location.pathname, `${WebsitePaths.Portal}/balances`); - const hasAlreadyDismissedWethNotice = Portal.hasAlreadyDismissedWethNotice(); + const hasAlreadyDismissedWethNotice = LegacyPortal.hasAlreadyDismissedWethNotice(); const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER); const hasAcceptedDisclaimer = @@ -123,7 +121,7 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { // become disconnected from their backing Ethereum node, changes user accounts, etc...) this.props.dispatcher.resetState(); } - public componentWillReceiveProps(nextProps: PortalAllProps) { + public componentWillReceiveProps(nextProps: LegacyPortalProps) { if (nextProps.networkId !== this.state.prevNetworkId) { // tslint:disable-next-line:no-floating-promises this._blockchain.networkIdUpdatedFireAndForgetAsync(nextProps.networkId); @@ -145,7 +143,7 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { } if (nextProps.location.pathname !== this.state.prevPathname) { const isViewingBalances = _.includes(nextProps.location.pathname, `${WebsitePaths.Portal}/balances`); - const hasAlreadyDismissedWethNotice = Portal.hasAlreadyDismissedWethNotice(); + const hasAlreadyDismissedWethNotice = LegacyPortal.hasAlreadyDismissedWethNotice(); this.setState({ prevPathname: nextProps.location.pathname, isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances, @@ -156,7 +154,6 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind( this.props.dispatcher, ); - const isDevelopment = configs.ENVIRONMENT === Environments.DEVELOPMENT; const portalStyle: React.CSSProperties = { minHeight: '100vh', display: 'flex', @@ -200,24 +197,12 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { ) : ( <div className="mx-auto flex"> <div className="col col-2 pr2 pt1 sm-hide xs-hide" style={portalMenuContainerStyle}> - <PortalMenu menuItemStyle={{ color: colors.white }} /> + <LegacyPortalMenu menuItemStyle={{ color: colors.white }} /> </div> <div className="col col-12 lg-col-10 md-col-10 sm-col sm-col-12"> <div className="py2" style={{ backgroundColor: colors.grey50 }}> {this.props.blockchainIsLoaded ? ( <Switch> - {isDevelopment && ( - <Route - path={`${WebsitePaths.Portal}/wallet`} - render={this._renderWallet.bind(this)} - /> - )} - {isDevelopment && ( - <Route - path={`${WebsitePaths.Portal}/relayers`} - render={this._renderRelayers.bind(this)} - /> - )} <Route path={`${WebsitePaths.Portal}/weth`} render={this._renderEthWrapper.bind(this)} @@ -296,40 +281,6 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { isLedgerDialogOpen: !this.state.isLedgerDialogOpen, }); } - private _renderWallet() { - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); - return ( - <div className="flex flex-center"> - <div className="mx-auto"> - <Wallet - userAddress={this.props.userAddress} - networkId={this.props.networkId} - blockchain={this._blockchain} - blockchainIsLoaded={this.props.blockchainIsLoaded} - blockchainErr={this.props.blockchainErr} - dispatcher={this.props.dispatcher} - tokenByAddress={this.props.tokenByAddress} - trackedTokens={trackedTokens} - userEtherBalanceInWei={this.props.userEtherBalanceInWei} - lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} - injectedProviderName={this.props.injectedProviderName} - providerType={this.props.providerType} - onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)} - /> - </div> - </div> - ); - } - private _renderRelayers() { - return ( - <div className="flex flex-center"> - <div className="mx-auto" style={{ width: 800 }}> - <RelayerIndex networkId={this.props.networkId} /> - </div> - </div> - ); - } private _renderEthWrapper() { return ( <EthWrappers diff --git a/packages/website/ts/components/portal_menu.tsx b/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx index 2b4d7eea2..94113f066 100644 --- a/packages/website/ts/components/portal_menu.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx @@ -4,15 +4,15 @@ import { MenuItem } from 'ts/components/ui/menu_item'; import { Environments, WebsitePaths } from 'ts/types'; import { configs } from 'ts/utils/configs'; -export interface PortalMenuProps { +export interface LegacyPortalMenuProps { menuItemStyle: React.CSSProperties; onClick?: () => void; } -interface PortalMenuState {} +interface LegacyPortalMenuState {} -export class PortalMenu extends React.Component<PortalMenuProps, PortalMenuState> { - public static defaultProps: Partial<PortalMenuProps> = { +export class LegacyPortalMenu extends React.Component<LegacyPortalMenuProps, LegacyPortalMenuState> { + public static defaultProps: Partial<LegacyPortalMenuProps> = { onClick: _.noop, }; public render() { @@ -58,26 +58,6 @@ export class PortalMenu extends React.Component<PortalMenuProps, PortalMenuState > {this._renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')} </MenuItem> - {configs.ENVIRONMENT === Environments.DEVELOPMENT && ( - <div> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}/wallet`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Wallet', 'zmdi-balance-wallet')} - </MenuItem> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}/relayers`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Relayers', 'zmdi-input-antenna')} - </MenuItem> - </div> - )} </div> ); } diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx new file mode 100644 index 000000000..4cbc65ce4 --- /dev/null +++ b/packages/website/ts/components/portal/portal.tsx @@ -0,0 +1,297 @@ +import { colors, Styles } from '@0xproject/react-shared'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; +import * as DocumentTitle from 'react-document-title'; + +import { Blockchain } from 'ts/blockchain'; +import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog'; +import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog'; +import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog'; +import { AssetPicker } from 'ts/components/generate_order/asset_picker'; +import { RelayerIndex } from 'ts/components/relayer_index/relayer_index'; +import { TopBar, TopBarDisplayType } from 'ts/components/top_bar/top_bar'; +import { FlashMessage } from 'ts/components/ui/flash_message'; +import { Wallet } from 'ts/components/wallet/wallet'; +import { localStorage } from 'ts/local_storage/local_storage'; +import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, TokenVisibility } from 'ts/types'; +import { configs } from 'ts/utils/configs'; +import { constants } from 'ts/utils/constants'; +import { Translate } from 'ts/utils/translate'; +import { utils } from 'ts/utils/utils'; + +export interface PortalProps { + blockchainErr: BlockchainErrs; + blockchainIsLoaded: boolean; + dispatcher: Dispatcher; + hashData: HashData; + injectedProviderName: string; + networkId: number; + nodeVersion: string; + orderFillAmount: BigNumber; + providerType: ProviderType; + screenWidth: ScreenWidths; + tokenByAddress: TokenByAddress; + userEtherBalanceInWei: BigNumber; + userAddress: string; + shouldBlockchainErrDialogBeOpen: boolean; + userSuppliedOrderCache: Order; + location: Location; + flashMessage?: string | React.ReactNode; + lastForceTokenStateRefetch: number; + translate: Translate; +} + +interface PortalState { + prevNetworkId: number; + prevNodeVersion: string; + prevUserAddress: string; + prevPathname: string; + isDisclaimerDialogOpen: boolean; + isLedgerDialogOpen: boolean; + tokenManagementState: TokenManagementState; +} + +enum TokenManagementState { + Add = 'Add', + Remove = 'Remove', + None = 'None', +} + +const THROTTLE_TIMEOUT = 100; +const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded); + +const styles: Styles = { + root: { + width: '100%', + height: '100%', + backgroundColor: colors.lightestGrey, + }, + body: { + height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`, + }, + scrollContainer: { + height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`, + WebkitOverflowScrolling: 'touch', + overflow: 'auto', + }, + title: { + fontWeight: 'bold', + fontSize: 20, + }, +}; + +export class Portal extends React.Component<PortalProps, PortalState> { + private _blockchain: Blockchain; + private _throttledScreenWidthUpdate: () => void; + constructor(props: PortalProps) { + super(props); + this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); + const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER); + const hasAcceptedDisclaimer = + !_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer); + this.state = { + prevNetworkId: this.props.networkId, + prevNodeVersion: this.props.nodeVersion, + prevUserAddress: this.props.userAddress, + prevPathname: this.props.location.pathname, + isDisclaimerDialogOpen: !hasAcceptedDisclaimer, + tokenManagementState: TokenManagementState.None, + isLedgerDialogOpen: false, + }; + } + public componentDidMount() { + window.addEventListener('resize', this._throttledScreenWidthUpdate); + window.scrollTo(0, 0); + } + public componentWillMount() { + this._blockchain = new Blockchain(this.props.dispatcher); + } + public componentWillUnmount() { + this._blockchain.destroy(); + window.removeEventListener('resize', this._throttledScreenWidthUpdate); + // We re-set the entire redux state when the portal is unmounted so that when it is re-rendered + // the initialization process always occurs from the same base state. This helps avoid + // initialization inconsistencies (i.e While the portal was unrendered, the user might have + // become disconnected from their backing Ethereum node, changed user accounts, etc...) + this.props.dispatcher.resetState(); + } + public componentWillReceiveProps(nextProps: PortalProps) { + if (nextProps.networkId !== this.state.prevNetworkId) { + // tslint:disable-next-line:no-floating-promises + this._blockchain.networkIdUpdatedFireAndForgetAsync(nextProps.networkId); + this.setState({ + prevNetworkId: nextProps.networkId, + }); + } + if (nextProps.userAddress !== this.state.prevUserAddress) { + const newUserAddress = _.isEmpty(nextProps.userAddress) ? undefined : nextProps.userAddress; + // tslint:disable-next-line:no-floating-promises + this._blockchain.userAddressUpdatedFireAndForgetAsync(newUserAddress); + this.setState({ + prevUserAddress: nextProps.userAddress, + }); + } + if (nextProps.nodeVersion !== this.state.prevNodeVersion) { + // tslint:disable-next-line:no-floating-promises + this._blockchain.nodeVersionUpdatedFireAndForgetAsync(nextProps.nodeVersion); + } + if (nextProps.location.pathname !== this.state.prevPathname) { + this.setState({ + prevPathname: nextProps.location.pathname, + }); + } + } + public render() { + const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind( + this.props.dispatcher, + ); + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); + const isAssetPickerDialogOpen = this.state.tokenManagementState !== TokenManagementState.None; + const tokenVisibility = + this.state.tokenManagementState === TokenManagementState.Add + ? TokenVisibility.UNTRACKED + : TokenVisibility.TRACKED; + return ( + <div style={styles.root}> + <DocumentTitle title="0x Portal DApp" /> + <TopBar + userAddress={this.props.userAddress} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)} + dispatcher={this.props.dispatcher} + providerType={this.props.providerType} + blockchainIsLoaded={this.props.blockchainIsLoaded} + location={this.props.location} + blockchain={this._blockchain} + translate={this.props.translate} + displayType={TopBarDisplayType.Expanded} + style={{ backgroundColor: colors.lightestGrey }} + /> + <div id="portal" style={styles.body}> + <div className="sm-flex flex-center"> + <div className="flex-last px3"> + <div className="py3" style={styles.title}> + Your Account + </div> + <Wallet + userAddress={this.props.userAddress} + networkId={this.props.networkId} + blockchain={this._blockchain} + blockchainIsLoaded={this.props.blockchainIsLoaded} + blockchainErr={this.props.blockchainErr} + dispatcher={this.props.dispatcher} + tokenByAddress={this.props.tokenByAddress} + trackedTokens={trackedTokens} + userEtherBalanceInWei={this.props.userEtherBalanceInWei} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)} + onAddToken={this._onAddToken.bind(this)} + onRemoveToken={this._onRemoveToken.bind(this)} + /> + </div> + <div className="flex-auto px3" style={styles.scrollContainer}> + <div className="py3" style={styles.title}> + Explore 0x Ecosystem + </div> + <RelayerIndex networkId={this.props.networkId} /> + </div> + </div> + <BlockchainErrDialog + blockchain={this._blockchain} + blockchainErr={this.props.blockchainErr} + isOpen={this.props.shouldBlockchainErrDialogBeOpen} + userAddress={this.props.userAddress} + toggleDialogFn={updateShouldBlockchainErrDialogBeOpen} + networkId={this.props.networkId} + /> + <PortalDisclaimerDialog + isOpen={this.state.isDisclaimerDialogOpen} + onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)} + /> + <FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} /> + {this.props.blockchainIsLoaded && ( + <LedgerConfigDialog + providerType={this.props.providerType} + networkId={this.props.networkId} + blockchain={this._blockchain} + dispatcher={this.props.dispatcher} + toggleDialogFn={this._onToggleLedgerDialog.bind(this)} + isOpen={this.state.isLedgerDialogOpen} + /> + )} + <AssetPicker + userAddress={this.props.userAddress} + networkId={this.props.networkId} + blockchain={this._blockchain} + dispatcher={this.props.dispatcher} + isOpen={isAssetPickerDialogOpen} + currentTokenAddress={''} + onTokenChosen={this._onTokenChosen.bind(this)} + tokenByAddress={this.props.tokenByAddress} + tokenVisibility={tokenVisibility} + /> + </div> + </div> + ); + } + private _onTokenChosen(tokenAddress: string) { + if (_.isEmpty(tokenAddress)) { + this.setState({ + tokenManagementState: TokenManagementState.None, + }); + return; + } + const token = this.props.tokenByAddress[tokenAddress]; + const isDefaultTrackedToken = _.includes(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, token.symbol); + if (this.state.tokenManagementState === TokenManagementState.Remove && !isDefaultTrackedToken) { + if (token.isRegistered) { + // Remove the token from tracked tokens + const newToken = { + ...token, + isTracked: false, + }; + this.props.dispatcher.updateTokenByAddress([newToken]); + } else { + this.props.dispatcher.removeTokenToTokenByAddress(token); + } + trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress); + } else if (isDefaultTrackedToken) { + this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`); + } + this.setState({ + tokenManagementState: TokenManagementState.None, + }); + } + private _onToggleLedgerDialog() { + this.setState({ + isLedgerDialogOpen: !this.state.isLedgerDialogOpen, + }); + } + private _onAddToken() { + this.setState({ + tokenManagementState: TokenManagementState.Add, + }); + } + private _onRemoveToken() { + this.setState({ + tokenManagementState: TokenManagementState.Remove, + }); + } + private _onPortalDisclaimerAccepted() { + localStorage.setItem(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER, 'set'); + this.setState({ + isDisclaimerDialogOpen: false, + }); + } + private _updateScreenWidth() { + const newScreenWidth = utils.getScreenWidth(); + this.props.dispatcher.updateScreenWidth(newScreenWidth); + } +} diff --git a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx index 0c4b2841c..d88a59d15 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -12,38 +12,6 @@ export interface RelayerGridTileProps { networkId: number; } -// TODO: Get top tokens and headerurl from remote -const headerUrl = '/images/og_image.png'; -const topTokens = [ - { - address: '0x1dad4783cf3fe3085c1426157ab175a6119a04ba', - decimals: 18, - iconUrl: '/images/token_icons/makerdao.png', - isRegistered: true, - isTracked: true, - name: 'Maker DAO', - symbol: 'MKR', - }, - { - address: '0x323b5d4c32345ced77393b3530b1eed0f346429d', - decimals: 18, - iconUrl: '/images/token_icons/melon.png', - isRegistered: true, - isTracked: true, - name: 'Melon Token', - symbol: 'MLN', - }, - { - address: '0xb18845c260f680d5b9d84649638813e342e4f8c9', - decimals: 18, - iconUrl: '/images/token_icons/augur.png', - isRegistered: true, - isTracked: true, - name: 'Augur Reputation Token', - symbol: 'REP', - }, -]; - const styles: Styles = { root: { backgroundColor: colors.white, @@ -68,6 +36,9 @@ const styles: Styles = { borderBottomLeftRadius: 4, borderTopRightRadius: 4, borderTopLeftRadius: 4, + borderWidth: 1, + borderStyle: 'solid', + borderColor: colors.walletBorder, }, body: { paddingLeft: 6, @@ -91,11 +62,20 @@ const styles: Styles = { }, }; +const FALLBACK_IMG_SRC = '/images/landing/hero_chip_image.png'; + export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (props: RelayerGridTileProps) => { + const link = props.relayerInfo.appUrl || props.relayerInfo.url; return ( <GridTile style={styles.root}> <div style={styles.innerDiv}> - <img src={headerUrl} style={styles.header} /> + <a href={link} target="_blank" style={{ textDecoration: 'none' }}> + <ImgWithFallback + src={props.relayerInfo.headerImgUrl} + fallbackSrc={FALLBACK_IMG_SRC} + style={styles.header} + /> + </a> <div style={styles.body}> <div className="py1" style={styles.relayerNameLabel}> {props.relayerInfo.name} @@ -104,7 +84,7 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = ( <div className="py1" style={styles.subLabel}> Daily Trade Volume </div> - <TopTokens tokens={topTokens} networkId={props.networkId} /> + <TopTokens tokens={props.relayerInfo.topTokens} networkId={props.networkId} /> <div className="py1" style={styles.subLabel}> Top tokens </div> @@ -113,3 +93,32 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = ( </GridTile> ); }; + +interface ImgWithFallbackProps { + src?: string; + fallbackSrc: string; + style: React.CSSProperties; +} +interface ImgWithFallbackState { + imageLoadFailed: boolean; +} +class ImgWithFallback extends React.Component<ImgWithFallbackProps, ImgWithFallbackState> { + constructor(props: ImgWithFallbackProps) { + super(props); + this.state = { + imageLoadFailed: false, + }; + } + public render() { + if (this.state.imageLoadFailed || _.isUndefined(this.props.src)) { + return <img src={this.props.fallbackSrc} style={this.props.style} />; + } else { + return <img src={this.props.src} onError={this._onError.bind(this)} style={this.props.style} />; + } + } + private _onError() { + this.setState({ + imageLoadFailed: true, + }); + } +} diff --git a/packages/website/ts/components/relayer_index/relayer_index.tsx b/packages/website/ts/components/relayer_index/relayer_index.tsx index 50760c32d..dffd0f83f 100644 --- a/packages/website/ts/components/relayer_index/relayer_index.tsx +++ b/packages/website/ts/components/relayer_index/relayer_index.tsx @@ -1,5 +1,7 @@ import { colors, Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; +import CircularProgress from 'material-ui/CircularProgress'; +import FlatButton from 'material-ui/FlatButton'; import { GridList } from 'material-ui/GridList'; import * as React from 'react'; @@ -32,9 +34,9 @@ const styles: Styles = { }, }; -const CELL_HEIGHT = 260; +const CELL_HEIGHT = 290; const NUMBER_OF_COLUMNS = 4; -const GRID_PADDING = 16; +const GRID_PADDING = 20; export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerIndexState> { private _isUnmounted: boolean; @@ -55,7 +57,24 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde } public render() { const readyToRender = _.isUndefined(this.state.error) && !_.isUndefined(this.state.relayerInfos); - if (readyToRender) { + if (!readyToRender) { + return ( + <div className="col col-12" style={{ ...styles.root, height: '100%' }}> + <div + className="relative sm-px2 sm-pt2 sm-m1" + style={{ height: 122, top: '33%', transform: 'translateY(-50%)' }} + > + <div className="center pb2"> + {_.isUndefined(this.state.error) ? ( + <CircularProgress size={40} thickness={5} /> + ) : ( + <Retry onRetry={this._fetchRelayerInfosAsync.bind(this)} /> + )} + </div> + </div> + </div> + ); + } else { return ( <div style={styles.root}> <GridList @@ -64,23 +83,22 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde padding={GRID_PADDING} style={styles.gridList} > - {this.state.relayerInfos.map((relayerInfo: WebsiteBackendRelayerInfo) => ( - <RelayerGridTile - key={relayerInfo.id} - relayerInfo={relayerInfo} - networkId={this.props.networkId} - /> + {this.state.relayerInfos.map((relayerInfo: WebsiteBackendRelayerInfo, index) => ( + <RelayerGridTile key={index} relayerInfo={relayerInfo} networkId={this.props.networkId} /> ))} </GridList> </div> ); - } else { - // TODO: loading and error states with a scrolling container - return null; } } private async _fetchRelayerInfosAsync(): Promise<void> { try { + if (!this._isUnmounted) { + this.setState({ + relayerInfos: undefined, + error: undefined, + }); + } const relayerInfos = await backendClient.getRelayerInfosAsync(); if (!this._isUnmounted) { this.setState({ @@ -96,3 +114,31 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde } } } + +interface RetryProps { + onRetry: () => void; +} +const Retry = (props: RetryProps) => ( + <div className="clearfix center" style={{ color: colors.black }}> + <div className="mx-auto inline-block align-middle" style={{ lineHeight: '44px', textAlign: 'center' }}> + <div className="h2" style={{ fontFamily: 'Roboto Mono' }}> + Something went wrong. + </div> + <div className="py3"> + <FlatButton + label={'reload'} + backgroundColor={colors.black} + labelStyle={{ + fontSize: 18, + fontFamily: 'Roboto Mono', + fontWeight: 'lighter', + color: colors.white, + textTransform: 'lowercase', + }} + style={{ width: 280, height: 62, borderRadius: 5 }} + onClick={props.onRetry} + /> + </div> + </div> + </div> +); diff --git a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx index 233590b78..db4d3a211 100644 --- a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx +++ b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx @@ -3,10 +3,10 @@ import * as _ from 'lodash'; import * as React from 'react'; import { TokenIcon } from 'ts/components/ui/token_icon'; -import { Token } from 'ts/types'; +import { WebsiteBackendTokenInfo } from 'ts/types'; export interface TopTokensProps { - tokens: Token[]; + tokens: WebsiteBackendTokenInfo[]; networkId: number; } @@ -23,17 +23,17 @@ const styles: Styles = { export const TopTokens: React.StatelessComponent<TopTokensProps> = (props: TopTokensProps) => { return ( <div className="flex"> - {_.map(props.tokens, (token: Token, index: number) => { + {_.map(props.tokens, (tokenInfo: WebsiteBackendTokenInfo, index: number) => { const firstItemStyle = { ...styles.tokenLabel, ...styles.followingTokenLabel }; const style = index !== 0 ? firstItemStyle : styles.tokenLabel; return ( <a - key={token.address} - href={tokenLinkFromToken(token, props.networkId)} + key={tokenInfo.address} + href={tokenLinkFromToken(tokenInfo, props.networkId)} target="_blank" style={style} > - {token.symbol} + {tokenInfo.symbol} </a> ); })} @@ -41,6 +41,6 @@ export const TopTokens: React.StatelessComponent<TopTokensProps> = (props: TopTo ); }; -function tokenLinkFromToken(token: Token, networkId: number) { - return sharedUtils.getEtherScanLinkIfExists(token.address, networkId, EtherscanLinkSuffixes.Address); +function tokenLinkFromToken(tokenInfo: WebsiteBackendTokenInfo, networkId: number) { + return sharedUtils.getEtherScanLinkIfExists(tokenInfo.address, networkId, EtherscanLinkSuffixes.Address); } diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index 79e7c3e2d..dc6ade841 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -1,4 +1,4 @@ -import { colors } from '@0xproject/react-shared'; +import { colors, Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import RaisedButton from 'material-ui/RaisedButton'; import * as React from 'react'; @@ -11,9 +11,9 @@ import { ProviderType } from 'ts/types'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; -const IDENTICON_DIAMETER = 32; +const ROOT_HEIGHT = 24; -interface ProviderDisplayProps { +export interface ProviderDisplayProps { dispatcher: Dispatcher; userAddress: string; networkId: number; @@ -25,6 +25,15 @@ interface ProviderDisplayProps { interface ProviderDisplayState {} +const styles: Styles = { + root: { + height: ROOT_HEIGHT, + backgroundColor: colors.white, + borderRadius: ROOT_HEIGHT, + boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`, + }, +}; + export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> { public render() { const isAddressAvailable = !_.isEmpty(this.props.userAddress); @@ -42,21 +51,20 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi : 'Connect a wallet'; const providerTitle = this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S'; + const isProviderMetamask = providerTitle === constants.PROVIDER_NAME_METAMASK; const hoverActiveNode = ( - <div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}> + <div className="flex right lg-pr0 md-pr2 sm-pr2 p1" style={styles.root}> <div> - <Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} /> - </div> - <div style={{ marginLeft: 12, paddingTop: 1 }}> - <div style={{ fontSize: 12, color: colors.amber800 }}>{providerTitle}</div> - <div style={{ fontSize: 14 }}>{displayAddress}</div> + <Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} /> </div> - <div - style={{ borderLeft: `1px solid ${colors.grey300}`, marginLeft: 17, paddingTop: 1 }} - className="px2" - > - <i style={{ fontSize: 30, color: colors.grey300 }} className="zmdi zmdi zmdi-chevron-down" /> + <div style={{ marginLeft: 12, paddingTop: 3 }}> + <div style={{ fontSize: 16, color: colors.darkGrey }}>{displayAddress}</div> </div> + {isProviderMetamask && ( + <div style={{ marginLeft: 16 }}> + <img src="/images/metamask_icon.png" style={{ width: ROOT_HEIGHT, height: ROOT_HEIGHT }} /> + </div> + )} </div> ); const hasInjectedProvider = diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 13351dcdc..5a1b50310 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import ReactTooltip = require('react-tooltip'); import { Blockchain } from 'ts/blockchain'; -import { PortalMenu } from 'ts/components/portal_menu'; +import { LegacyPortalMenu } from 'ts/components/legacy_portal/legacy_portal_menu'; import { SidebarHeader } from 'ts/components/sidebar_header'; import { ProviderDisplay } from 'ts/components/top_bar/provider_display'; import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item'; @@ -19,7 +19,12 @@ import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/ty import { constants } from 'ts/utils/constants'; import { Translate } from 'ts/utils/translate'; -interface TopBarProps { +export enum TopBarDisplayType { + Default, + Expanded, +} + +export interface TopBarProps { userAddress?: string; networkId?: number; injectedProviderName?: string; @@ -34,7 +39,7 @@ interface TopBarProps { availableDocVersions?: string[]; menu?: DocsMenu; menuSubsectionsBySection?: MenuSubsectionsBySection; - shouldFullWidth?: boolean; + displayType?: TopBarDisplayType; docsInfo?: DocsInfo; style?: React.CSSProperties; isNightVersion?: boolean; @@ -47,17 +52,8 @@ interface TopBarState { } const styles: Styles = { - address: { - marginRight: 12, - overflow: 'hidden', - paddingTop: 4, - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - width: 70, - }, topBar: { - backgroundcolor: colors.white, - height: 59, + backgroundColor: colors.white, width: '100%', position: 'relative', top: 0, @@ -78,12 +74,19 @@ const styles: Styles = { }, }; +const DEFAULT_HEIGHT = 59; +const EXPANDED_HEIGHT = 75; + export class TopBar extends React.Component<TopBarProps, TopBarState> { public static defaultProps: Partial<TopBarProps> = { - shouldFullWidth: false, + displayType: TopBarDisplayType.Default, style: {}, isNightVersion: false, }; + public static heightForDisplayType(displayType: TopBarDisplayType) { + const result = displayType === TopBarDisplayType.Expanded ? EXPANDED_HEIGHT : DEFAULT_HEIGHT; + return result + 1; + } constructor(props: TopBarProps) { super(props); this.state = { @@ -92,8 +95,9 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { } public render() { const isNightVersion = this.props.isNightVersion; - const isFullWidthPage = this.props.shouldFullWidth; - const parentClassNames = `flex mx-auto ${isFullWidthPage ? 'pl2' : 'max-width-4'}`; + const isExpandedDisplayType = this.props.displayType === TopBarDisplayType.Expanded; + const parentClassNames = `flex mx-auto ${isExpandedDisplayType ? 'pl3 py1' : 'max-width-4'}`; + const height = isExpandedDisplayType ? EXPANDED_HEIGHT : DEFAULT_HEIGHT; const developerSectionMenuItems = [ <Link key="subMenuItem-zeroEx" to={WebsitePaths.ZeroExJs} className="text-decoration-none"> <MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="0x.js" /> @@ -139,10 +143,16 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { primaryText={this.props.translate.get(Key.Web3Wrapper, Deco.CapWords)} /> </Link>, - <Link key="subMenuItem-deployer" to={WebsitePaths.Deployer} className="text-decoration-none"> + <Link key="subMenuItem-order-utils" to={WebsitePaths.OrderUtils} className="text-decoration-none"> <MenuItem style={{ fontSize: styles.menuItem.fontSize }} - primaryText={this.props.translate.get(Key.Deployer, Deco.CapWords)} + primaryText={this.props.translate.get(Key.OrderUtils, Deco.CapWords)} + /> + </Link>, + <Link key="subMenuItem-sol-compiler" to={WebsitePaths.SolCompiler} className="text-decoration-none"> + <MenuItem + style={{ fontSize: styles.menuItem.fontSize }} + primaryText={this.props.translate.get(Key.SolCompiler, Deco.CapWords)} /> </Link>, <Link key="subMenuItem-sol-cov" to={WebsitePaths.SolCov} className="text-decoration-none"> @@ -172,9 +182,11 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </a>, ]; const bottomBorderStyle = this._shouldDisplayBottomBar() ? styles.bottomBar : {}; - const fullWidthClasses = isFullWidthPage ? 'pr4' : ''; + const fullWidthClasses = isExpandedDisplayType ? 'pr4' : ''; const logoUrl = isNightVersion ? '/images/protocol_logo_white.png' : '/images/protocol_logo_black.png'; - const menuClasses = `col col-${isFullWidthPage ? '4' : '5'} ${fullWidthClasses} lg-pr0 md-pr2 sm-hide xs-hide`; + const menuClasses = `col col-${ + isExpandedDisplayType ? '4' : '5' + } ${fullWidthClasses} lg-pr0 md-pr2 sm-hide xs-hide`; const menuIconStyle = { fontSize: 25, color: isNightVersion ? 'white' : 'black', @@ -191,15 +203,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { ); const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>; return ( - <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1"> + <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style, ...{ height } }} className="pb1"> <div className={parentClassNames}> - <div className="col col-2 sm-pl2 md-pl2 lg-pl0" style={{ paddingTop: 15 }}> + <div className="col col-2 sm-pl1 md-pl2 lg-pl0" style={{ paddingTop: 15 }}> <Link to={`${WebsitePaths.Home}`} className="text-decoration-none"> <img src={logoUrl} height="30" /> </Link> </div> - <div className={`col col-${isFullWidthPage ? '8' : '9'} lg-hide md-hide`} /> - <div className={`col col-${isFullWidthPage ? '6' : '5'} sm-hide xs-hide`} /> + <div className={`col col-${isExpandedDisplayType ? '8' : '9'} lg-hide md-hide`} /> + <div className={`col col-${isExpandedDisplayType ? '6' : '5'} sm-hide xs-hide`} /> {!this._isViewingPortal() && ( <div className={menuClasses}> <div className="flex justify-between"> @@ -236,7 +248,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { path={`${WebsitePaths.Portal}`} isPrimary={true} style={styles.menuItem} - className={`${isFullWidthPage && 'md-hide'}`} + className={`${isExpandedDisplayType && 'md-hide'}`} isNightVersion={isNightVersion} isExternal={false} /> @@ -244,7 +256,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> )} {this.props.blockchainIsLoaded && ( - <div className="sm-hide xs-hide col col-5"> + <div className="sm-hide xs-hide col col-5" style={{ paddingTop: 8, marginRight: 36 }}> <ProviderDisplay dispatcher={this.props.dispatcher} userAddress={this.props.userAddress} @@ -256,7 +268,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { /> </div> )} - <div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}> + <div className={`col ${isExpandedDisplayType ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}> <div style={menuIconStyle}> <i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} /> </div> @@ -316,10 +328,10 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </MenuItem> </Link> )} - {!this._isViewingDeployerDocs() && ( - <Link to={WebsitePaths.Deployer} className="text-decoration-none"> + {!this._isViewingSolCompilerDocs() && ( + <Link to={WebsitePaths.SolCompiler} className="text-decoration-none"> <MenuItem className="py2"> - {this.props.translate.get(Key.Deployer, Deco.Cap)}{' '} + {this.props.translate.get(Key.SolCompiler, Deco.Cap)}{' '} {this.props.translate.get(Key.Docs, Deco.Cap)} </MenuItem> </Link> @@ -378,7 +390,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { (!this._isViewing0xjsDocs() && !this._isViewingSmartContractsDocs() && !this._isViewingWeb3WrapperDocs() && - !this._isViewingDeployerDocs() && + !this._isViewingSolCompilerDocs() && !this._isViewingJsonSchemasDocs() && !this._isViewingSolCovDocs() && !this._isViewingSubprovidersDocs() && @@ -431,22 +443,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { <div className="pl1 py1" style={{ backgroundColor: colors.lightGrey }}> {this.props.translate.get(Key.PortalDApp, Deco.CapWords)} </div> - <PortalMenu menuItemStyle={{ color: 'black' }} onClick={this._onMenuButtonClick.bind(this)} /> - </div> - ); - } - private _renderUser() { - const userAddress = this.props.userAddress; - const identiconDiameter = 26; - return ( - <div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}> - <div style={styles.address} data-tip={true} data-for="userAddressTooltip"> - {!_.isEmpty(userAddress) ? userAddress : ''} - </div> - <ReactTooltip id="userAddressTooltip">{userAddress}</ReactTooltip> - <div> - <Identicon address={userAddress} diameter={identiconDiameter} /> - </div> + <LegacyPortalMenu menuItemStyle={{ color: 'black' }} onClick={this._onMenuButtonClick.bind(this)} /> </div> ); } @@ -479,8 +476,8 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { _.includes(this.props.location.pathname, WebsiteLegacyPaths.Web3Wrapper) ); } - private _isViewingDeployerDocs() { - return _.includes(this.props.location.pathname, WebsitePaths.Deployer); + private _isViewingSolCompilerDocs() { + return _.includes(this.props.location.pathname, WebsitePaths.SolCompiler); } private _isViewingJsonSchemasDocs() { return _.includes(this.props.location.pathname, WebsitePaths.JSONSchemas); @@ -501,11 +498,12 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { this._isViewingFAQ() || this._isViewingSmartContractsDocs() || this._isViewingWeb3WrapperDocs() || - this._isViewingDeployerDocs() || + this._isViewingSolCompilerDocs() || this._isViewingJsonSchemasDocs() || this._isViewingSolCovDocs() || this._isViewingSubprovidersDocs() || - this._isViewingConnectDocs() + this._isViewingConnectDocs() || + this._isViewingPortal() ); } } // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/ui/input_label.tsx b/packages/website/ts/components/ui/input_label.tsx index 2506db423..8eda45a5d 100644 --- a/packages/website/ts/components/ui/input_label.tsx +++ b/packages/website/ts/components/ui/input_label.tsx @@ -17,7 +17,7 @@ const styles: Styles = { userSelect: 'none', width: 240, zIndex: 1, - }, + } as React.CSSProperties, }; export const InputLabel = (props: InputLabelProps) => { diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index d1ae38550..079c0e3b3 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -9,8 +9,11 @@ import { import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import FlatButton from 'material-ui/FlatButton'; +import FloatingActionButton from 'material-ui/FloatingActionButton'; import { List, ListItem } from 'material-ui/List'; import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; +import ContentAdd from 'material-ui/svg-icons/content/add'; +import ContentRemove from 'material-ui/svg-icons/content/remove'; import NavigationArrowDownward from 'material-ui/svg-icons/navigation/arrow-downward'; import NavigationArrowUpward from 'material-ui/svg-icons/navigation/arrow-upward'; import Close from 'material-ui/svg-icons/navigation/close'; @@ -55,11 +58,14 @@ export interface WalletProps { injectedProviderName: string; providerType: ProviderType; onToggleLedgerDialog: () => void; + onAddToken: () => void; + onRemoveToken: () => void; } interface WalletState { trackedTokenStateByAddress: TokenStateByAddress; wrappedEtherDirection?: Side; + isHoveringSidebar: boolean; } interface AllowanceToggleConfig { @@ -94,6 +100,9 @@ const styles: Styles = { }, footerItemInnerDiv: { paddingLeft: 24, + borderTopColor: colors.walletBorder, + borderTopStyle: 'solid', + borderWidth: 1, }, borderedItem: { borderBottomColor: colors.walletBorder, @@ -114,7 +123,17 @@ const styles: Styles = { paddingTop: 8, paddingBottom: 8, }, - accessoryItemsContainer: { width: 150, right: 8 }, + accessoryItemsContainer: { + width: 150, + right: 8, + }, + bodyInnerDiv: { + padding: 0, + // TODO: make this completely responsive + maxHeight: 475, + overflow: 'auto', + WebkitOverflowScrolling: 'touch', + }, }; const ETHER_ICON_PATH = '/images/ether.png'; @@ -123,6 +142,7 @@ const ZRX_TOKEN_SYMBOL = 'ZRX'; const ETHER_SYMBOL = 'ETH'; const ICON_DIMENSION = 24; const TOKEN_AMOUNT_DISPLAY_PRECISION = 3; +const BODY_ITEM_KEY = 'BODY'; const HEADER_ITEM_KEY = 'HEADER'; const FOOTER_ITEM_KEY = 'FOOTER'; const DISCONNECTED_ITEM_KEY = 'DISCONNECTED'; @@ -139,6 +159,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { this.state = { trackedTokenStateByAddress: initialTrackedTokenStateByAddress, wrappedEtherDirection: undefined, + isHoveringSidebar: false, }; } public componentWillMount() { @@ -184,12 +205,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { return ( <List style={styles.list}> {isAddressAvailable - ? _.concat( - this._renderConnectedHeaderRows(), - this._renderEthRows(), - this._renderTokenRows(), - this._renderFooterRows(), - ) + ? _.concat(this._renderConnectedHeaderRows(), this._renderBody(), this._renderFooterRows()) : _.concat(this._renderDisconnectedHeaderRows(), this._renderDisconnectedRows())} </List> ); @@ -230,9 +246,61 @@ export class Wallet extends React.Component<WalletProps, WalletState> { /> ); } + private _renderBody() { + const bodyStyle: React.CSSProperties = { + ...styles.bodyInnerDiv, + overflow: this.state.isHoveringSidebar ? 'auto' : 'hidden', + }; + return ( + <ListItem + key={BODY_ITEM_KEY} + innerDivStyle={bodyStyle} + onMouseEnter={this._onSidebarHover.bind(this)} + onMouseLeave={this._onSidebarHoverOff.bind(this)} + > + {this._renderEthRows()} + {this._renderTokenRows()} + </ListItem> + ); + } + private _onSidebarHover(event: React.FormEvent<HTMLInputElement>) { + this.setState({ + isHoveringSidebar: true, + }); + } + private _onSidebarHoverOff() { + this.setState({ + isHoveringSidebar: false, + }); + } private _renderFooterRows() { - const primaryText = '+ other tokens'; - return <ListItem key={FOOTER_ITEM_KEY} primaryText={primaryText} innerDivStyle={styles.footerItemInnerDiv} />; + return ( + <ListItem + key={FOOTER_ITEM_KEY} + primaryText={ + <div className="flex"> + <FloatingActionButton mini={true} zDepth={0} onClick={this.props.onAddToken}> + <ContentAdd /> + </FloatingActionButton> + <FloatingActionButton mini={true} zDepth={0} className="px1" onClick={this.props.onRemoveToken}> + <ContentRemove /> + </FloatingActionButton> + <div + style={{ + paddingLeft: 10, + position: 'relative', + top: '50%', + transform: 'translateY(33%)', + }} + > + add/remove tokens + </div> + </div> + } + disabled={true} + innerDivStyle={styles.footerItemInnerDiv} + /> + ); } private _renderEthRows() { const primaryText = this._renderAmount( @@ -293,7 +361,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); } - private _renderTokenRow(token: Token) { + private _renderTokenRow(token: Token, index: number) { const tokenState = this.state.trackedTokenStateByAddress[token.address]; const tokenLink = sharedUtils.getEtherScanLinkIfExists( token.address, @@ -310,12 +378,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> { tokenState, }, }; + // if this is the last item in the list, do not render the border, it is rendered by the footer + const borderedStyle = index !== this.props.trackedTokens.length - 1 ? styles.borderedItem : {}; const shouldShowWrapEtherItem = !_.isUndefined(this.state.wrappedEtherDirection) && this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection; const style = shouldShowWrapEtherItem ? { ...walletItemStyles.focusedItem, ...styles.paddedItem } - : { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem }; + : { ...styles.tokenItem, ...borderedStyle, ...styles.paddedItem }; const etherToken = this._getEthToken(); return ( <div key={token.address}> @@ -461,16 +531,16 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); balanceAndAllowanceTupleByAddress[tokenAddress] = balanceAndAllowanceTuple; } - const pricesByAddress = await this._getPricesByAddressAsync(tokenAddresses); + const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses); const trackedTokenStateByAddress = _.reduce( tokenAddresses, (acc, address) => { const [balance, allowance] = balanceAndAllowanceTupleByAddress[address]; - const price = pricesByAddress[address]; + const priceIfExists = _.get(priceByAddress, address); acc[address] = { balance, allowance, - price, + price: priceIfExists, isLoaded: true, }; return acc; @@ -487,16 +557,29 @@ export class Wallet extends React.Component<WalletProps, WalletState> { private async _refetchTokenStateAsync(tokenAddress: string) { await this._fetchBalancesAndAllowancesAsync([tokenAddress]); } - private async _getPricesByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> { + private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> { if (_.isEmpty(tokenAddresses)) { return {}; } + // for each input token address, search for the corresponding symbol in this.props.tokenByAddress, if it exists + // create a mapping from existing symbols -> address + const tokenAddressBySymbol: { [symbol: string]: string } = {}; + _.each(tokenAddresses, address => { + const tokenIfExists = _.get(this.props.tokenByAddress, address); + if (!_.isUndefined(tokenIfExists)) { + const symbol = tokenIfExists.symbol; + tokenAddressBySymbol[symbol] = address; + } + }); + const tokenSymbols = _.keys(tokenAddressBySymbol); try { - const websiteBackendPriceInfos = await backendClient.getPriceInfosAsync(tokenAddresses); - const addresses = _.map(websiteBackendPriceInfos, info => info.address); - const prices = _.map(websiteBackendPriceInfos, info => new BigNumber(info.price)); - const pricesByAddress = _.zipObject(addresses, prices); - return pricesByAddress; + const priceBySymbol = await backendClient.getPriceInfoAsync(tokenSymbols); + const priceByAddress = _.mapKeys(priceBySymbol, (value, symbol) => _.get(tokenAddressBySymbol, symbol)); + const result = _.mapValues(priceByAddress, price => { + const priceBigNumber = new BigNumber(price); + return priceBigNumber; + }); + return result; } catch (err) { return {}; } |