diff options
Diffstat (limited to 'packages/website')
19 files changed, 324 insertions, 247 deletions
diff --git a/packages/website/public/index.html b/packages/website/public/index.html index 4c0985c71..060f2c3c2 100644 --- a/packages/website/public/index.html +++ b/packages/website/public/index.html @@ -70,7 +70,18 @@ })(document, 'script', 'twitter-wjs'); </script> <!-- End Twitter SDK --> - + <!-- Hotjar Tracking Code for https://0xproject.com/ --> + <script> + (function (h, o, t, j, a, r) { + h.hj = h.hj || function () { (h.hj.q = h.hj.q || []).push(arguments) }; + h._hjSettings = { hjid: 935597, hjsv: 6 }; + a = o.getElementsByTagName('head')[0]; + r = o.createElement('script'); r.async = 1; + r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv; + a.appendChild(r); + })(window, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv='); + </script> + <!-- End Hotjar Tracking Code --> <!-- Main --> <script type="text/javascript" crossorigin="anonymous" src="/bundle.js" charset="utf-8"></script> </body> diff --git a/packages/website/ts/blockchain_watcher.ts b/packages/website/ts/blockchain_watcher.ts index df5f73fd1..4b23aa98a 100644 --- a/packages/website/ts/blockchain_watcher.ts +++ b/packages/website/ts/blockchain_watcher.ts @@ -10,6 +10,7 @@ export class BlockchainWatcher { private _watchBalanceIntervalId: NodeJS.Timer; private _prevUserEtherBalanceInWei?: BigNumber; private _prevUserAddressIfExists: string; + private _prevNodeVersionIfExists: string; constructor(dispatcher: Dispatcher, web3Wrapper: Web3Wrapper, shouldPollUserAddress: boolean) { this._dispatcher = dispatcher; this._shouldPollUserAddress = shouldPollUserAddress; @@ -43,11 +44,9 @@ export class BlockchainWatcher { ); } private async _updateBalanceAsync(): Promise<void> { - let prevNodeVersion: string; - // Check for node version changes const currentNodeVersion = await this._web3Wrapper.getNodeVersionAsync(); - if (currentNodeVersion !== prevNodeVersion) { - prevNodeVersion = currentNodeVersion; + if (this._prevNodeVersionIfExists !== currentNodeVersion) { + this._prevNodeVersionIfExists = currentNodeVersion; this._dispatcher.updateNodeVersion(currentNodeVersion); } diff --git a/packages/website/ts/components/onboarding/onboarding_card.tsx b/packages/website/ts/components/onboarding/onboarding_card.tsx index 48e8ab022..ba5b3d6ea 100644 --- a/packages/website/ts/components/onboarding/onboarding_card.tsx +++ b/packages/website/ts/components/onboarding/onboarding_card.tsx @@ -39,7 +39,7 @@ export const OnboardingCard: React.StatelessComponent<OnboardingCardProps> = ({ borderRadius, }) => ( <Island borderRadius={borderRadius}> - <Container paddingRight="30px" paddingLeft="30px" maxWidth={350} paddingTop="15px" paddingBottom="15px"> + <Container paddingRight="30px" paddingLeft="30px" paddingTop="15px" paddingBottom="15px"> <div className="flex flex-column"> <div className="flex justify-between"> <Title>{title}</Title> diff --git a/packages/website/ts/components/onboarding/onboarding_flow.tsx b/packages/website/ts/components/onboarding/onboarding_flow.tsx index 1f4c6df82..c2b4a4ca7 100644 --- a/packages/website/ts/components/onboarding/onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/onboarding_flow.tsx @@ -6,13 +6,29 @@ import { ContinueButtonDisplay, OnboardingTooltip } from 'ts/components/onboardi import { Animation } from 'ts/components/ui/animation'; import { Container } from 'ts/components/ui/container'; import { Overlay } from 'ts/components/ui/overlay'; +import { PointerDirection } from 'ts/components/ui/pointer'; import { zIndex } from 'ts/style/z_index'; -export interface Step { +export interface FixedPositionSettings { + type: 'fixed'; + top?: string; + bottom?: string; + left?: string; + right?: string; + pointerDirection?: PointerDirection; +} + +export interface TargetPositionSettings { + type: 'target'; target: string; + placement: Placement; +} + +export interface Step { + // Provide either a CSS selector, or fixed position settings. Only applies to desktop. + position: TargetPositionSettings | FixedPositionSettings; title?: string; content: React.ReactNode; - placement?: Placement; shouldHideBackButton?: boolean; shouldHideNextButton?: boolean; continueButtonDisplay?: ContinueButtonDisplay; @@ -40,18 +56,30 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { return null; } let onboardingElement = null; + const currentStep = this._getCurrentStep(); if (this.props.isMobile) { - onboardingElement = <Animation type="easeUpFromBottom">{this._renderOnboardignCard()}</Animation>; - } else { + onboardingElement = <Animation type="easeUpFromBottom">{this._renderOnboardingCard()}</Animation>; + } else if (currentStep.position.type === 'target') { + const { placement, target } = currentStep.position; onboardingElement = ( - <Popper - referenceElement={this._getElementForStep()} - placement={this._getCurrentStep().placement} - positionFixed={true} - > + <Popper referenceElement={document.querySelector(target)} placement={placement} positionFixed={true}> {this._renderPopperChildren.bind(this)} </Popper> ); + } else if (currentStep.position.type === 'fixed') { + const { top, right, bottom, left, pointerDirection } = currentStep.position; + onboardingElement = ( + <Container + position="fixed" + zIndex={zIndex.aboveOverlay} + top={top} + right={right} + bottom={bottom} + left={left} + > + {this._renderToolTip(pointerDirection)} + </Container> + ); } if (this.props.disableOverlay) { return onboardingElement; @@ -63,9 +91,6 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { </div> ); } - private _getElementForStep(): Element { - return document.querySelector(this._getCurrentStep().target); - } private _renderPopperChildren(props: PopperChildrenProps): React.ReactNode { const customStyles = { zIndex: zIndex.aboveOverlay }; // On re-render, we want to re-center the popper. @@ -76,7 +101,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { </div> ); } - private _renderToolTip(): React.ReactNode { + private _renderToolTip(pointerDirection?: PointerDirection): React.ReactNode { const { steps, stepIndex } = this.props; const step = steps[stepIndex]; const isLastStep = steps.length - 1 === stepIndex; @@ -94,12 +119,13 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { continueButtonDisplay={step.continueButtonDisplay} continueButtonText={step.continueButtonText} onContinueButtonClick={step.onContinueButtonClick} + pointerDirection={pointerDirection} /> </Container> ); } - private _renderOnboardignCard(): React.ReactNode { + private _renderOnboardingCard(): React.ReactNode { const { steps, stepIndex } = this.props; const step = steps[stepIndex]; const isLastStep = steps.length - 1 === stepIndex; diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index d0955c2dc..e01da68a7 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -9,7 +9,12 @@ import { AddEthOnboardingStep } from 'ts/components/onboarding/add_eth_onboardin import { CongratsOnboardingStep } from 'ts/components/onboarding/congrats_onboarding_step'; import { InstallWalletOnboardingStep } from 'ts/components/onboarding/install_wallet_onboarding_step'; import { IntroOnboardingStep } from 'ts/components/onboarding/intro_onboarding_step'; -import { OnboardingFlow, Step } from 'ts/components/onboarding/onboarding_flow'; +import { + FixedPositionSettings, + OnboardingFlow, + Step, + TargetPositionSettings, +} from 'ts/components/onboarding/onboarding_flow'; import { SetAllowancesOnboardingStep } from 'ts/components/onboarding/set_allowances_onboarding_step'; import { UnlockWalletOnboardingStep } from 'ts/components/onboarding/unlock_wallet_onboarding_step'; import { @@ -45,8 +50,6 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp private _unlisten: () => void; public componentDidMount(): void { this._adjustStepIfShould(); - // Wait until the step is adjusted to decide whether we should show onboarding. - setTimeout(this._autoStartOnboardingIfShould.bind(this), 1000); // If there is a route change, just close onboarding. this._unlisten = this.props.history.listen(() => this.props.updateIsRunning(false)); } @@ -61,6 +64,9 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp document.querySelector('.wallet').scrollIntoView(); } } + if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) { + this._autoStartOnboardingIfShould(); + } } public render(): React.ReactNode { return ( @@ -76,56 +82,61 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp ); } private _getSteps(): Step[] { + const nextToWalletPosition: TargetPositionSettings = { + type: 'target', + target: '.wallet', + placement: 'right', + }; + const underMetamaskExtension: FixedPositionSettings = { + type: 'fixed', + top: '30px', + right: '10px', + pointerDirection: 'top', + }; const steps: Step[] = [ { - target: '.wallet', + position: nextToWalletPosition, title: '0x Ecosystem Setup', content: <InstallWalletOnboardingStep />, - placement: 'right', shouldHideBackButton: true, shouldHideNextButton: true, }, { - target: '.wallet', + position: underMetamaskExtension, title: '0x Ecosystem Setup', content: <UnlockWalletOnboardingStep />, - placement: 'right', shouldHideBackButton: true, shouldHideNextButton: true, }, { - target: '.wallet', + position: nextToWalletPosition, title: '0x Ecosystem Account Setup', content: <IntroOnboardingStep />, - placement: 'right', shouldHideBackButton: true, continueButtonDisplay: 'enabled', }, { - target: '.wallet', + position: nextToWalletPosition, title: 'Step 1: Add ETH', content: ( <AddEthOnboardingStep userEthBalanceInWei={this.props.userEtherBalanceInWei || new BigNumber(0)} /> ), - placement: 'right', continueButtonDisplay: this._userHasVisibleEth() ? 'enabled' : 'disabled', }, { - target: '.wallet', + position: nextToWalletPosition, title: 'Step 2: Wrap ETH', content: <WrapEthOnboardingStep1 />, - placement: 'right', continueButtonDisplay: 'enabled', }, { - target: '.wallet', + position: nextToWalletPosition, title: 'Step 2: Wrap ETH', content: <WrapEthOnboardingStep2 />, - placement: 'right', continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled', }, { - target: '.wallet', + position: nextToWalletPosition, title: 'Step 2: Wrap ETH', content: ( <WrapEthOnboardingStep3 @@ -134,11 +145,10 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp } /> ), - placement: 'right', continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled', }, { - target: '.wallet', + position: nextToWalletPosition, title: 'Step 3: Unlock Tokens', content: ( <SetAllowancesOnboardingStep @@ -147,14 +157,12 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp doesUserHaveAllowancesForWethAndZrx={this._doesUserHaveAllowancesForWethAndZrx()} /> ), - placement: 'right', continueButtonDisplay: this._doesUserHaveAllowancesForWethAndZrx() ? 'enabled' : 'disabled', }, { - target: '.wallet', + position: nextToWalletPosition, title: '🎉 The Ecosystem Awaits', content: <CongratsOnboardingStep />, - placement: 'right', continueButtonDisplay: 'enabled', shouldHideNextButton: true, continueButtonText: 'Enter the 0x Ecosystem', @@ -221,7 +229,7 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp } private _autoStartOnboardingIfShould(): void { if ( - (this.props.stepIndex === 0 && !this.props.isRunning) || + (this.props.stepIndex === 0 && !this.props.isRunning && this.props.blockchainIsLoaded) || (!this.props.isRunning && !this.props.hasBeenClosed && this.props.blockchainIsLoaded) ) { const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; diff --git a/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx b/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx index 0039aa545..4ed7137d4 100644 --- a/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx @@ -10,7 +10,7 @@ export const UnlockWalletOnboardingStep: React.StatelessComponent<UnlockWalletOn <Container marginTop="15px" marginBottom="15px"> <img src="/images/metamask_icon.png" height="50px" width="50px" /> </Container> - <Text center={true}>Unlock your metamask extension to get started.</Text> + <Text center={true}>Unlock your MetaMask extension to get started.</Text> </div> </div> ); 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 80ff8c7ff..02bc1b014 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -9,6 +9,7 @@ import { Container } from 'ts/components/ui/container'; import { Image } from 'ts/components/ui/image'; import { Island } from 'ts/components/ui/island'; import { colors } from 'ts/style/colors'; +import { media } from 'ts/style/media'; import { styled } from 'ts/style/theme'; import { WebsiteBackendRelayerInfo } from 'ts/types'; import { utils } from 'ts/utils/utils'; @@ -111,6 +112,9 @@ const GridTile = styled(PlainGridTile)` &:hover { transform: translate(0px, -3px); } + ${media.small` + transform: none; + `}; `; interface SectionProps { diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index ae3be6436..806eaeea5 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -1,23 +1,26 @@ import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import CircularProgress from 'material-ui/CircularProgress'; -import RaisedButton from 'material-ui/RaisedButton'; import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; -import { ProviderPicker } from 'ts/components/top_bar/provider_picker'; import { AccountConnection } from 'ts/components/ui/account_connection'; import { Container } from 'ts/components/ui/container'; import { DropDown } from 'ts/components/ui/drop_down'; import { Identicon } from 'ts/components/ui/identicon'; import { Image } from 'ts/components/ui/image'; import { Island } from 'ts/components/ui/island'; +import { + CopyAddressSimpleMenuItem, + DifferentWalletSimpleMenuItem, + GoToAccountManagementSimpleMenuItem, + SimpleMenu, +} from 'ts/components/ui/simple_menu'; import { Text } from 'ts/components/ui/text'; import { Dispatcher } from 'ts/redux/dispatcher'; import { colors } from 'ts/style/colors'; import { AccountState, ProviderType } from 'ts/types'; -import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; const ROOT_HEIGHT = 24; @@ -44,11 +47,7 @@ const styles: Styles = { export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> { public render(): React.ReactNode { - const isExternallyInjectedProvider = utils.isExternallyInjected( - this.props.providerType, - this.props.injectedProviderName, - ); - const hoverActiveNode = ( + const activeNode = ( <Island className="flex items-center py1 px2" style={styles.root}> {this._renderIcon()} <Container marginLeft="12px" marginRight="12px"> @@ -57,93 +56,34 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi {this._renderInjectedProvider()} </Island> ); - const hasLedgerProvider = this.props.providerType === ProviderType.Ledger; - const horizontalPosition = isExternallyInjectedProvider || hasLedgerProvider ? 'left' : 'middle'; return ( <div style={{ width: 'fit-content', height: 48, float: 'right' }}> <DropDown - hoverActiveNode={hoverActiveNode} - popoverContent={this.renderPopoverContent(isExternallyInjectedProvider, hasLedgerProvider)} - anchorOrigin={{ horizontal: horizontalPosition, vertical: 'bottom' }} - targetOrigin={{ horizontal: horizontalPosition, vertical: 'top' }} + activeNode={activeNode} + popoverContent={this._renderPopoverContent()} + anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }} + targetOrigin={{ horizontal: 'middle', vertical: 'top' }} zDepth={1} /> </div> ); } - public renderPopoverContent(hasInjectedProvider: boolean, hasLedgerProvider: boolean): React.ReactNode { - if (!this._isBlockchainReady()) { - return null; - } else if (hasInjectedProvider || hasLedgerProvider) { - return ( - <ProviderPicker - dispatcher={this.props.dispatcher} - networkId={this.props.networkId} - injectedProviderName={this.props.injectedProviderName} - providerType={this.props.providerType} - onToggleLedgerDialog={this.props.onToggleLedgerDialog} - blockchain={this.props.blockchain} - /> - ); - } else { - // Nothing to connect to, show install/info popover - return ( - <div className="px2" style={{ maxWidth: 420 }}> - <div className="center h4 py2" style={{ color: colors.grey700 }}> - Choose a wallet: - </div> - <div className="flex pb3"> - <div className="center px2"> - <div style={{ color: colors.darkGrey }}>Install a browser wallet</div> - <div className="py2"> - <img src="/images/metamask_or_parity.png" width="135" /> - </div> - <div> - Use{' '} - <a - href={constants.URL_METAMASK_CHROME_STORE} - target="_blank" - style={{ color: colors.lightBlueA700 }} - > - Metamask - </a>{' '} - or{' '} - <a - href={constants.URL_PARITY_CHROME_STORE} - target="_blank" - style={{ color: colors.lightBlueA700 }} - > - Parity Signer - </a> - </div> - </div> - <div> - <div - className="pl1 ml1" - style={{ borderLeft: `1px solid ${colors.grey300}`, height: 65 }} - /> - <div className="py1">or</div> - <div - className="pl1 ml1" - style={{ borderLeft: `1px solid ${colors.grey300}`, height: 68 }} - /> - </div> - <div className="px2 center"> - <div style={{ color: colors.darkGrey }}>Connect to a ledger hardware wallet</div> - <div style={{ paddingTop: 21, paddingBottom: 29 }}> - <img src="/images/ledger_icon.png" style={{ width: 80 }} /> - </div> - <div> - <RaisedButton - style={{ width: '100%' }} - label="Use Ledger" - onClick={this.props.onToggleLedgerDialog} - /> - </div> - </div> - </div> - </div> - ); + private _renderPopoverContent(): React.ReactNode { + const accountState = this._getAccountState(); + switch (accountState) { + case AccountState.Ready: + return ( + <SimpleMenu> + <CopyAddressSimpleMenuItem userAddress={this.props.userAddress} /> + <DifferentWalletSimpleMenuItem onClick={this.props.onToggleLedgerDialog} /> + <GoToAccountManagementSimpleMenuItem /> + </SimpleMenu> + ); + case AccountState.Disconnected: + case AccountState.Locked: + case AccountState.Loading: + default: + return null; } } private _renderIcon(): React.ReactNode { diff --git a/packages/website/ts/components/top_bar/provider_picker.tsx b/packages/website/ts/components/top_bar/provider_picker.tsx deleted file mode 100644 index 7937f2e9d..000000000 --- a/packages/website/ts/components/top_bar/provider_picker.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { colors, constants as sharedConstants } from '@0xproject/react-shared'; -import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; -import * as React from 'react'; -import { Blockchain } from 'ts/blockchain'; -import { Dispatcher } from 'ts/redux/dispatcher'; -import { ProviderType } from 'ts/types'; - -interface ProviderPickerProps { - networkId: number; - injectedProviderName: string; - providerType: ProviderType; - onToggleLedgerDialog: () => void; - dispatcher: Dispatcher; - blockchain: Blockchain; -} - -interface ProviderPickerState {} - -export class ProviderPicker extends React.Component<ProviderPickerProps, ProviderPickerState> { - public render(): React.ReactNode { - const isLedgerSelected = this.props.providerType === ProviderType.Ledger; - const menuStyle = { - padding: 10, - paddingTop: 15, - paddingBottom: 15, - }; - // Show dropdown with two options - return ( - <div style={{ width: 225, overflow: 'hidden' }}> - <RadioButtonGroup name="provider" defaultSelected={this.props.providerType}> - <RadioButton - onClick={this._onProviderRadioChanged.bind(this, ProviderType.Injected)} - style={{ ...menuStyle, backgroundColor: !isLedgerSelected && colors.grey50 }} - value={ProviderType.Injected} - label={this._renderLabel(this.props.injectedProviderName, !isLedgerSelected)} - /> - <RadioButton - onClick={this._onProviderRadioChanged.bind(this, ProviderType.Ledger)} - style={{ ...menuStyle, backgroundColor: isLedgerSelected && colors.grey50 }} - value={ProviderType.Ledger} - label={this._renderLabel('Ledger Nano S', isLedgerSelected)} - /> - </RadioButtonGroup> - </div> - ); - } - private _renderLabel(title: string, shouldShowNetwork: boolean): React.ReactNode { - const label = ( - <div className="flex"> - <div style={{ fontSize: 14 }}>{title}</div> - {shouldShowNetwork && this._renderNetwork()} - </div> - ); - return label; - } - private _renderNetwork(): React.ReactNode { - const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; - return ( - <div className="flex" style={{ marginTop: 1 }}> - <div className="relative" style={{ width: 14, paddingLeft: 14 }}> - <img - src={`/images/network_icons/${networkName.toLowerCase()}.png`} - className="absolute" - style={{ top: 6, width: 10 }} - /> - </div> - <div style={{ color: colors.lightGrey, fontSize: 11 }}>{networkName}</div> - </div> - ); - } - private _onProviderRadioChanged(value: string): void { - if (value === ProviderType.Ledger) { - this.props.onToggleLedgerDialog(); - } else { - // tslint:disable-next-line:no-floating-promises - this.props.blockchain.updateProviderToInjectedAsync(); - } - } -} diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index fac6c131f..778536663 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -199,7 +199,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { cursor: 'pointer', paddingTop: 16, }; - const hoverActiveNode = ( + const activeNode = ( <div className="flex relative" style={{ color: menuIconStyle.color }}> <div style={{ paddingRight: 10 }}>{this.props.translate.get(Key.Developers, Deco.Cap)}</div> <div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}> @@ -224,7 +224,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { <div className={menuClasses}> <div className="flex justify-between"> <DropDown - hoverActiveNode={hoverActiveNode} + activeNode={activeNode} popoverContent={popoverContent} anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }} targetOrigin={{ horizontal: 'middle', vertical: 'top' }} diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index fb718d731..edbf8814b 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -17,6 +17,7 @@ export interface ContainerProps { maxHeight?: StringOrNum; width?: StringOrNum; height?: StringOrNum; + minWidth?: StringOrNum; minHeight?: StringOrNum; isHidden?: boolean; className?: string; diff --git a/packages/website/ts/components/ui/drop_down.tsx b/packages/website/ts/components/ui/drop_down.tsx index 22cb942f8..4d5caef08 100644 --- a/packages/website/ts/components/ui/drop_down.tsx +++ b/packages/website/ts/components/ui/drop_down.tsx @@ -1,4 +1,4 @@ -import Popover, { PopoverAnimationVertical } from 'material-ui/Popover'; +import Popover from 'material-ui/Popover'; import * as React from 'react'; import { MaterialUIPosition } from 'ts/types'; @@ -7,13 +7,20 @@ const DEFAULT_STYLE = { fontSize: 14, }; -interface DropDownProps { - hoverActiveNode: React.ReactNode; +export enum DropdownMouseEvent { + Hover = 'hover', + Click = 'click', +} + +export interface DropDownProps { + activeNode: React.ReactNode; popoverContent: React.ReactNode; anchorOrigin: MaterialUIPosition; targetOrigin: MaterialUIPosition; style?: React.CSSProperties; zDepth?: number; + activateEvent?: DropdownMouseEvent; + closeEvent?: DropdownMouseEvent; } interface DropDownState { @@ -25,6 +32,8 @@ export class DropDown extends React.Component<DropDownProps, DropDownState> { public static defaultProps: Partial<DropDownProps> = { style: DEFAULT_STYLE, zDepth: 1, + activateEvent: DropdownMouseEvent.Hover, + closeEvent: DropdownMouseEvent.Hover, }; private _isHovering: boolean; private _popoverCloseCheckIntervalId: number; @@ -58,46 +67,61 @@ export class DropDown extends React.Component<DropDownProps, DropDownState> { onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)} > - {this.props.hoverActiveNode} + <div onClick={this._onActiveNodeClick.bind(this)}>{this.props.activeNode}</div> <Popover open={this.state.isDropDownOpen} anchorEl={this.state.anchorEl} anchorOrigin={this.props.anchorOrigin} targetOrigin={this.props.targetOrigin} onRequestClose={this._closePopover.bind(this)} - useLayerForClickAway={false} - animation={PopoverAnimationVertical} + useLayerForClickAway={this.props.closeEvent === DropdownMouseEvent.Click} + animated={false} zDepth={this.props.zDepth} > - <div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}> + <div + onMouseEnter={this._onHover.bind(this)} + onMouseLeave={this._onHoverOff.bind(this)} + onClick={this._closePopover.bind(this)} + > {this.props.popoverContent} </div> </Popover> </div> ); } + private _onActiveNodeClick(event: React.FormEvent<HTMLInputElement>): void { + if (this.props.activateEvent === DropdownMouseEvent.Click) { + this.setState({ + isDropDownOpen: true, + anchorEl: event.currentTarget, + }); + } + } private _onHover(event: React.FormEvent<HTMLInputElement>): void { this._isHovering = true; - this._checkIfShouldOpenPopover(event); + if (this.props.activateEvent === DropdownMouseEvent.Hover) { + this._checkIfShouldOpenPopover(event); + } + } + private _onHoverOff(): void { + this._isHovering = false; } private _checkIfShouldOpenPopover(event: React.FormEvent<HTMLInputElement>): void { if (this.state.isDropDownOpen) { return; // noop } - this.setState({ isDropDownOpen: true, anchorEl: event.currentTarget, }); } - private _onHoverOff(): void { - this._isHovering = false; - } private _checkIfShouldClosePopover(): void { - if (!this.state.isDropDownOpen || this._isHovering) { + if (!this.state.isDropDownOpen) { return; // noop } - this._closePopover(); + if (this.props.closeEvent === DropdownMouseEvent.Hover && !this._isHovering) { + this._closePopover(); + } } private _closePopover(): void { this.setState({ diff --git a/packages/website/ts/components/ui/simple_menu.tsx b/packages/website/ts/components/ui/simple_menu.tsx new file mode 100644 index 000000000..74b8ef6ae --- /dev/null +++ b/packages/website/ts/components/ui/simple_menu.tsx @@ -0,0 +1,88 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as CopyToClipboard from 'react-copy-to-clipboard'; +import { Link } from 'react-router-dom'; + +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; +import { colors } from 'ts/style/colors'; +import { WebsitePaths } from 'ts/types'; + +export interface SimpleMenuProps { + minWidth?: number | string; +} + +export const SimpleMenu: React.StatelessComponent<SimpleMenuProps> = ({ children, minWidth }) => { + return ( + <Container + marginLeft="16px" + marginRight="16px" + marginBottom="16px" + minWidth={minWidth} + className="flex flex-column" + > + {children} + </Container> + ); +}; + +SimpleMenu.defaultProps = { + minWidth: '220px', +}; + +export interface SimpleMenuItemProps { + displayText: string; + onClick?: () => void; +} +export const SimpleMenuItem: React.StatelessComponent<SimpleMenuItemProps> = ({ displayText, onClick }) => { + // Falling back to _.noop for onclick retains the hovering effect + return ( + <Container marginTop="16px" className="flex flex-column"> + <Text + fontSize="14px" + fontColor={colors.darkGrey} + onClick={onClick || _.noop} + hoverColor={colors.mediumBlue} + > + {displayText} + </Text> + </Container> + ); +}; + +export interface CopyAddressSimpleMenuItemProps { + userAddress: string; + onClick?: () => void; +} +export const CopyAddressSimpleMenuItem: React.StatelessComponent<CopyAddressSimpleMenuItemProps> = ({ + userAddress, + onClick, +}) => { + return ( + <CopyToClipboard text={userAddress}> + <SimpleMenuItem displayText="Copy Address to Clipboard" onClick={onClick} /> + </CopyToClipboard> + ); +}; + +export interface GoToAccountManagementSimpleMenuItemProps { + onClick?: () => void; +} +export const GoToAccountManagementSimpleMenuItem: React.StatelessComponent< + GoToAccountManagementSimpleMenuItemProps +> = ({ onClick }) => { + return ( + <Link to={`${WebsitePaths.Portal}/account`} style={{ textDecoration: 'none' }}> + <SimpleMenuItem displayText="Manage Account..." onClick={onClick} /> + </Link> + ); +}; + +export interface DifferentWalletSimpleMenuItemProps { + onClick?: () => void; +} +export const DifferentWalletSimpleMenuItem: React.StatelessComponent<DifferentWalletSimpleMenuItemProps> = ({ + onClick, +}) => { + return <SimpleMenuItem displayText="Use a Different Wallet..." onClick={onClick} />; +}; diff --git a/packages/website/ts/components/ui/text.tsx b/packages/website/ts/components/ui/text.tsx index c1cb2ade4..315f72854 100644 --- a/packages/website/ts/components/ui/text.tsx +++ b/packages/website/ts/components/ui/text.tsx @@ -3,7 +3,7 @@ import { darken } from 'polished'; import * as React from 'react'; import { styled } from 'ts/style/theme'; -export type TextTag = 'p' | 'div' | 'span' | 'label' | 'h1' | 'h2' | 'h3' | 'h4'; +export type TextTag = 'p' | 'div' | 'span' | 'label' | 'h1' | 'h2' | 'h3' | 'h4' | 'i'; export interface TextProps { className?: string; @@ -17,6 +17,7 @@ export interface TextProps { fontWeight?: number | string; textDecorationLine?: string; onClick?: (event: React.MouseEvent<HTMLElement>) => void; + hoverColor?: string; } const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick, Tag }) => ( @@ -37,7 +38,7 @@ export const Text = styled(PlainText)` ${props => (props.onClick ? 'cursor: pointer' : '')}; transition: color 0.5s ease; &:hover { - ${props => (props.onClick ? `color: ${darken(0.3, props.fontColor)}` : '')}; + ${props => (props.onClick ? `color: ${props.hoverColor || darken(0.3, props.fontColor)}` : '')}; } `; diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 6c8b398c4..1e0b9ec48 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -4,15 +4,22 @@ import * as _ from 'lodash'; import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; import * as React from 'react'; -import { Link } from 'react-router-dom'; import firstBy = require('thenby'); import { Blockchain } from 'ts/blockchain'; import { AccountConnection } from 'ts/components/ui/account_connection'; import { Container } from 'ts/components/ui/container'; +import { DropDown, DropdownMouseEvent } from 'ts/components/ui/drop_down'; import { IconButton } from 'ts/components/ui/icon_button'; import { Identicon } from 'ts/components/ui/identicon'; import { Island } from 'ts/components/ui/island'; +import { + CopyAddressSimpleMenuItem, + DifferentWalletSimpleMenuItem, + GoToAccountManagementSimpleMenuItem, + SimpleMenu, + SimpleMenuItem, +} from 'ts/components/ui/simple_menu'; import { Text } from 'ts/components/ui/text'; import { TokenIcon } from 'ts/components/ui/token_icon'; import { BodyOverlay } from 'ts/components/wallet/body_overlay'; @@ -33,7 +40,6 @@ import { TokenByAddress, TokenState, TokenStateByAddress, - WebsitePaths, } from 'ts/types'; import { analytics } from 'ts/utils/analytics'; import { constants } from 'ts/utils/constants'; @@ -83,7 +89,6 @@ const BODY_ITEM_KEY = 'BODY'; const HEADER_ITEM_KEY = 'HEADER'; const ETHER_ITEM_KEY = 'ETHER'; const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56; -const ACCOUNT_PATH = `${WebsitePaths.Portal}/account`; const PLACEHOLDER_COLOR = colors.grey300; const LOADING_ROWS_COUNT = 6; @@ -187,6 +192,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); } private _renderConnectedHeaderRows(): React.ReactElement<{}> { + const isMobile = this.props.screenWidth === ScreenWidths.Sm; const userAddress = this.props.userAddress; const accountState = this._getAccountState(); const main = ( @@ -197,15 +203,49 @@ export class Wallet extends React.Component<WalletProps, WalletState> { <AccountConnection accountState={accountState} injectedProviderName={this.props.injectedProviderName} /> </div> ); + const onClick = _.noop; + const accessory = ( + <DropDown + activeNode={ + // this container gives the menu button more of a hover target for the drop down + // it prevents accidentally closing the menu by moving off of the button + <Container paddingLeft="100px" paddingRight="15px"> + <Text + className="zmdi zmdi-more-horiz" + Tag="i" + fontSize="32px" + fontFamily="Material-Design-Iconic-Font" + fontColor={colors.darkGrey} + onClick={onClick} + hoverColor={colors.mediumBlue} + /> + </Container> + } + popoverContent={ + <SimpleMenu minWidth="150px"> + <CopyAddressSimpleMenuItem userAddress={this.props.userAddress} /> + <DifferentWalletSimpleMenuItem onClick={this.props.onToggleLedgerDialog} /> + <SimpleMenuItem displayText="Add Tokens..." onClick={this.props.onAddToken} /> + <SimpleMenuItem displayText="Remove Tokens..." onClick={this.props.onRemoveToken} /> + <GoToAccountManagementSimpleMenuItem /> + </SimpleMenu> + } + anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} + targetOrigin={{ horizontal: 'right', vertical: 'top' }} + zDepth={1} + activateEvent={DropdownMouseEvent.Click} + closeEvent={isMobile ? DropdownMouseEvent.Click : DropdownMouseEvent.Hover} + /> + ); return ( - <Link key={HEADER_ITEM_KEY} to={ACCOUNT_PATH} style={{ textDecoration: 'none' }}> - <StandardIconRow - icon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />} - main={main} - minHeight="60px" - backgroundColor={colors.white} - /> - </Link> + <StandardIconRow + key={HEADER_ITEM_KEY} + icon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />} + main={main} + accessory={accessory} + minHeight="60px" + backgroundColor={colors.white} + /> ); } private _renderBody(): React.ReactElement<{}> { diff --git a/packages/website/ts/redux/store.ts b/packages/website/ts/redux/store.ts index 0d0e6cea1..2672e3f61 100644 --- a/packages/website/ts/redux/store.ts +++ b/packages/website/ts/redux/store.ts @@ -13,9 +13,11 @@ export const store: ReduxStore<State> = createStore( ); store.subscribe( _.throttle(() => { + const state = store.getState(); // Persisted state stateStorage.saveState({ - hasPortalOnboardingBeenClosed: store.getState().hasPortalOnboardingBeenClosed, + hasPortalOnboardingBeenClosed: state.hasPortalOnboardingBeenClosed, + isPortalOnboardingShowing: state.isPortalOnboardingShowing, }); }, ONE_SECOND), ); diff --git a/packages/website/ts/style/media.ts b/packages/website/ts/style/media.ts new file mode 100644 index 000000000..3c992eb9f --- /dev/null +++ b/packages/website/ts/style/media.ts @@ -0,0 +1,14 @@ +import { css } from 'ts/style/theme'; +import { ScreenWidths } from 'ts/types'; + +const generateMediaWrapper = (screenWidth: ScreenWidths) => (...args: any[]) => css` + @media (max-width: ${screenWidth}) { + ${css.apply(css, args)}; + } +`; + +export const media = { + small: generateMediaWrapper(ScreenWidths.Sm), + medium: generateMediaWrapper(ScreenWidths.Md), + large: generateMediaWrapper(ScreenWidths.Lg), +}; diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index 324ba5b39..76c4b3452 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -215,10 +215,11 @@ export interface ContractEvent { } export type ValidatedBigNumberCallback = (isValid: boolean, amount?: BigNumber) => void; +// Associated values are in `em` units export enum ScreenWidths { - Sm = 'SM', - Md = 'MD', - Lg = 'LG', + Sm = 40, + Md = 52, + Lg = 64, } export enum AlertTypes { diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index c47beb480..d5e2c82b7 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -30,9 +30,6 @@ import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import * as u2f from 'ts/vendor/u2f_api'; -const LG_MIN_EM = 64; -const MD_MIN_EM = 52; - const isDogfood = (): boolean => _.includes(window.location.href, configs.DOMAIN_DOGFOOD); export const utils = { @@ -137,9 +134,9 @@ export const utils = { // This logic mirrors the CSS media queries in BassCSS for the `lg-`, `md-` and `sm-` CSS // class prefixes. Do not edit these. - if (widthInEm > LG_MIN_EM) { + if (widthInEm > ScreenWidths.Lg) { return ScreenWidths.Lg; - } else if (widthInEm > MD_MIN_EM) { + } else if (widthInEm > ScreenWidths.Md) { return ScreenWidths.Md; } else { return ScreenWidths.Sm; |