diff options
author | Francesco Agosti <francesco.agosti93@gmail.com> | 2018-07-28 03:02:29 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-28 03:02:29 +0800 |
commit | 48e538f5c7dc208775ec71635e4d4866b5907228 (patch) | |
tree | 82bb74e4521804377869c099cf5eba019f06d4d5 | |
parent | 44d1be27e626c4e80ccf80d3f070a890c45f02ac (diff) | |
parent | c851c3763060ca4cf9e37d75e36e534f9b7d9294 (diff) | |
download | dexon-0x-contracts-48e538f5c7dc208775ec71635e4d4866b5907228.tar dexon-0x-contracts-48e538f5c7dc208775ec71635e4d4866b5907228.tar.gz dexon-0x-contracts-48e538f5c7dc208775ec71635e4d4866b5907228.tar.bz2 dexon-0x-contracts-48e538f5c7dc208775ec71635e4d4866b5907228.tar.lz dexon-0x-contracts-48e538f5c7dc208775ec71635e4d4866b5907228.tar.xz dexon-0x-contracts-48e538f5c7dc208775ec71635e4d4866b5907228.tar.zst dexon-0x-contracts-48e538f5c7dc208775ec71635e4d4866b5907228.zip |
Merge pull request #910 from 0xProject/feature/website/upgrade-allowance-toggles-to-locks-and-checks
[website] Use new designs with tooltips for allowance toggles
-rw-r--r-- | packages/website/ts/components/inputs/allowance_state_toggle.tsx | 160 | ||||
-rw-r--r-- | packages/website/ts/components/inputs/allowance_toggle.tsx | 140 | ||||
-rw-r--r-- | packages/website/ts/components/onboarding/onboarding_tooltip.tsx | 2 | ||||
-rw-r--r-- | packages/website/ts/components/onboarding/portal_onboarding_flow.tsx | 19 | ||||
-rw-r--r-- | packages/website/ts/components/portal/portal.tsx | 4 | ||||
-rw-r--r-- | packages/website/ts/components/token_balances.tsx | 19 | ||||
-rw-r--r-- | packages/website/ts/components/ui/allowance_state_view.tsx | 51 | ||||
-rw-r--r-- | packages/website/ts/components/ui/container.tsx | 1 | ||||
-rw-r--r-- | packages/website/ts/components/ui/pointer.tsx | 7 | ||||
-rw-r--r-- | packages/website/ts/components/ui/spinner.tsx | 54 | ||||
-rw-r--r-- | packages/website/ts/components/ui/text.tsx | 3 | ||||
-rw-r--r-- | packages/website/ts/components/wallet/wallet.tsx | 31 | ||||
-rw-r--r-- | packages/website/ts/containers/inputs/allowance_state_toggle.ts (renamed from packages/website/ts/containers/inputs/allowance_toggle.ts) | 13 | ||||
-rw-r--r-- | packages/website/ts/style/keyframes.ts | 22 |
14 files changed, 346 insertions, 180 deletions
diff --git a/packages/website/ts/components/inputs/allowance_state_toggle.tsx b/packages/website/ts/components/inputs/allowance_state_toggle.tsx new file mode 100644 index 000000000..39d2e3030 --- /dev/null +++ b/packages/website/ts/components/inputs/allowance_state_toggle.tsx @@ -0,0 +1,160 @@ +import { colors } from '@0xproject/react-shared'; +import { BigNumber, logUtils } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; +import ReactTooltip = require('react-tooltip'); +import { Blockchain } from 'ts/blockchain'; +import { AllowanceState, AllowanceStateView } from 'ts/components/ui/allowance_state_view'; +import { Container } from 'ts/components/ui/container'; +import { PointerDirection } from 'ts/components/ui/pointer'; +import { Text } from 'ts/components/ui/text'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { BalanceErrs, Token, TokenState } from 'ts/types'; +import { analytics } from 'ts/utils/analytics'; +import { errorReporter } from 'ts/utils/error_reporter'; +import { utils } from 'ts/utils/utils'; + +export interface AllowanceStateToggleProps { + networkId: number; + blockchain: Blockchain; + dispatcher: Dispatcher; + token: Token; + tokenState: TokenState; + userAddress: string; + onErrorOccurred?: (errType: BalanceErrs) => void; + refetchTokenStateAsync: () => Promise<void>; + tooltipDirection?: PointerDirection; +} + +export interface AllowanceStateToggleState { + allowanceState: AllowanceState; + prevTokenState: TokenState; + loadingMessage?: string; +} + +const DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); + +export class AllowanceStateToggle extends React.Component<AllowanceStateToggleProps, AllowanceStateToggleState> { + public static defaultProps = { + onErrorOccurred: _.noop.bind(_), + tooltipDirection: PointerDirection.Right, + }; + private static _getAllowanceState(tokenState: TokenState): AllowanceState { + if (!tokenState.isLoaded) { + return AllowanceState.Loading; + } + if (tokenState.allowance.gt(0)) { + return AllowanceState.Unlocked; + } + return AllowanceState.Locked; + } + constructor(props: AllowanceStateToggleProps) { + super(props); + const tokenState = props.tokenState; + this.state = { + allowanceState: AllowanceStateToggle._getAllowanceState(tokenState), + prevTokenState: tokenState, + }; + } + + public render(): React.ReactNode { + const tooltipId = `tooltip-id-${this.props.token.symbol}`; + return ( + <Container cursor="pointer"> + <ReactTooltip id={tooltipId} effect="solid" offset={{ top: 3 }}> + {this._getTooltipContent()} + </ReactTooltip> + <div + data-tip={true} + data-for={tooltipId} + data-place={this.props.tooltipDirection} + onClick={this._onToggleAllowanceAsync.bind(this)} + > + <AllowanceStateView allowanceState={this.state.allowanceState} /> + </div> + </Container> + ); + } + public componentWillReceiveProps(nextProps: AllowanceStateToggleProps): void { + const nextTokenState = nextProps.tokenState; + const prevTokenState = this.state.prevTokenState; + if ( + !nextTokenState.allowance.eq(prevTokenState.allowance) || + nextTokenState.isLoaded !== prevTokenState.isLoaded + ) { + const tokenState = nextProps.tokenState; + this.setState({ + prevTokenState: tokenState, + allowanceState: AllowanceStateToggle._getAllowanceState(nextTokenState), + }); + } + } + private _getTooltipContent(): React.ReactNode { + const symbol = this.props.token.symbol; + switch (this.state.allowanceState) { + case AllowanceState.Loading: + return ( + <Text noWrap={true} fontColor={colors.white}> + {this.state.loadingMessage || 'Loading...'} + </Text> + ); + case AllowanceState.Locked: + return ( + <Text noWrap={true} fontColor={colors.white}> + Click to enable <b>{symbol}</b> for trading + </Text> + ); + case AllowanceState.Unlocked: + return ( + <Text noWrap={true} fontColor={colors.white}> + <b>{symbol}</b> is available for trading + </Text> + ); + default: + return null; + } + } + private async _onToggleAllowanceAsync(): Promise<void> { + // Close all tooltips + ReactTooltip.hide(); + if (this.props.userAddress === '') { + this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); + return; + } + + let newAllowanceAmountInBaseUnits = new BigNumber(0); + if (!this._isAllowanceSet()) { + newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS; + } + const isUnlockingToken = newAllowanceAmountInBaseUnits.gt(0); + this.setState({ + allowanceState: AllowanceState.Loading, + loadingMessage: `${isUnlockingToken ? 'Unlocking' : 'Locking'} ${this.props.token.symbol}`, + }); + const logData = { + tokenSymbol: this.props.token.symbol, + newAllowance: newAllowanceAmountInBaseUnits.toNumber(), + }; + try { + await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits); + analytics.track('Set Allowances Success', logData); + await this.props.refetchTokenStateAsync(); + } catch (err) { + analytics.track('Set Allowance Failure', logData); + this.setState({ + allowanceState: AllowanceStateToggle._getAllowanceState(this.state.prevTokenState), + }); + const errMsg = `${err}`; + if (utils.didUserDenyWeb3Request(errMsg)) { + return; + } + logUtils.log(`Unexpected error encountered: ${err}`); + logUtils.log(err.stack); + this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed); + errorReporter.report(err); + } + } + private _isAllowanceSet(): boolean { + return !this.props.tokenState.allowance.eq(0); + } +} diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx deleted file mode 100644 index 05dce134a..000000000 --- a/packages/website/ts/components/inputs/allowance_toggle.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Styles } from '@0xproject/react-shared'; -import { BigNumber, logUtils } from '@0xproject/utils'; -import * as _ from 'lodash'; -import Toggle from 'material-ui/Toggle'; -import * as React from 'react'; -import { Blockchain } from 'ts/blockchain'; -import { Dispatcher } from 'ts/redux/dispatcher'; -import { colors } from 'ts/style/colors'; -import { BalanceErrs, Token, TokenState } from 'ts/types'; -import { analytics } from 'ts/utils/analytics'; -import { errorReporter } from 'ts/utils/error_reporter'; -import { utils } from 'ts/utils/utils'; - -const DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); - -interface AllowanceToggleProps { - networkId: number; - blockchain: Blockchain; - dispatcher: Dispatcher; - token: Token; - tokenState: TokenState; - userAddress: string; - isDisabled?: boolean; - onErrorOccurred?: (errType: BalanceErrs) => void; - refetchTokenStateAsync: () => Promise<void>; -} - -interface AllowanceToggleState { - isSpinnerVisible: boolean; - prevAllowance: BigNumber; -} - -const styles: Styles = { - baseThumbStyle: { - height: 10, - width: 10, - top: 6, - backgroundColor: colors.white, - boxShadow: `0px 0px 0px ${colors.allowanceToggleShadow}`, - }, - offThumbStyle: { - left: 4, - }, - onThumbStyle: { - left: 25, - }, - baseTrackStyle: { - width: 25, - }, - offTrackStyle: { - backgroundColor: colors.grey300, - }, - onTrackStyle: { - backgroundColor: colors.mediumBlue, - }, -}; - -export class AllowanceToggle extends React.Component<AllowanceToggleProps, AllowanceToggleState> { - public static defaultProps = { - onErrorOccurred: _.noop.bind(_), - isDisabled: false, - }; - constructor(props: AllowanceToggleProps) { - super(props); - this.state = { - isSpinnerVisible: false, - prevAllowance: props.tokenState.allowance, - }; - } - public componentWillReceiveProps(nextProps: AllowanceToggleProps): void { - if (!nextProps.tokenState.allowance.eq(this.state.prevAllowance)) { - this.setState({ - isSpinnerVisible: false, - prevAllowance: nextProps.tokenState.allowance, - }); - } - } - public render(): React.ReactNode { - return ( - <div className="flex"> - <div> - <Toggle - disabled={this.state.isSpinnerVisible || this.props.isDisabled} - toggled={this._isAllowanceSet()} - onToggle={this._onToggleAllowanceAsync.bind(this)} - thumbStyle={{ ...styles.baseThumbStyle, ...styles.offThumbStyle }} - thumbSwitchedStyle={{ ...styles.baseThumbStyle, ...styles.onThumbStyle }} - trackStyle={{ ...styles.baseTrackStyle, ...styles.offTrackStyle }} - trackSwitchedStyle={{ ...styles.baseTrackStyle, ...styles.onTrackStyle }} - /> - </div> - {this.state.isSpinnerVisible && ( - <div className="pl1" style={{ paddingTop: 3 }}> - <i className="zmdi zmdi-spinner zmdi-hc-spin" /> - </div> - )} - </div> - ); - } - private async _onToggleAllowanceAsync(): Promise<void> { - if (this.props.userAddress === '') { - this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); - return; - } - - this.setState({ - isSpinnerVisible: true, - }); - - let newAllowanceAmountInBaseUnits = new BigNumber(0); - if (!this._isAllowanceSet()) { - newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS; - } - const logData = { - tokenSymbol: this.props.token.symbol, - newAllowance: newAllowanceAmountInBaseUnits.toNumber(), - }; - try { - await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits); - analytics.track('Set Allowances Success', logData); - await this.props.refetchTokenStateAsync(); - } catch (err) { - analytics.track('Set Allowance Failure', logData); - this.setState({ - isSpinnerVisible: false, - }); - const errMsg = `${err}`; - if (utils.didUserDenyWeb3Request(errMsg)) { - return; - } - logUtils.log(`Unexpected error encountered: ${err}`); - logUtils.log(err.stack); - this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed); - errorReporter.report(err); - } - } - private _isAllowanceSet(): boolean { - return !this.props.tokenState.allowance.eq(0); - } -} diff --git a/packages/website/ts/components/onboarding/onboarding_tooltip.tsx b/packages/website/ts/components/onboarding/onboarding_tooltip.tsx index 15d47908d..ff5f0bab6 100644 --- a/packages/website/ts/components/onboarding/onboarding_tooltip.tsx +++ b/packages/website/ts/components/onboarding/onboarding_tooltip.tsx @@ -24,7 +24,7 @@ export const OnboardingTooltip: React.StatelessComponent<OnboardingTooltipProps> ); }; OnboardingTooltip.defaultProps = { - pointerDisplay: 'left', + pointerDisplay: PointerDirection.Left, }; OnboardingTooltip.displayName = 'OnboardingTooltip'; diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index f395674a1..522687758 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -21,7 +21,7 @@ import { WrapEthOnboardingStep2, WrapEthOnboardingStep3, } from 'ts/components/onboarding/wrap_eth_onboarding_step'; -import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; +import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle'; import { BrowserType, ProviderType, ScreenWidths, Token, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { analytics } from 'ts/utils/analytics'; import { utils } from 'ts/utils/utils'; @@ -149,8 +149,8 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp title: 'Step 3: Unlock Tokens', content: ( <SetAllowancesOnboardingStep - zrxAllowanceToggle={this._renderZrxAllowanceToggle()} - ethAllowanceToggle={this._renderEthAllowanceToggle()} + zrxAllowanceToggle={this._renderZrxAllowanceStateToggle()} + ethAllowanceToggle={this._renderEthAllowanceStateToggle()} doesUserHaveAllowancesForWethAndZrx={this._doesUserHaveAllowancesForWethAndZrx()} /> ), @@ -243,15 +243,15 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp stepIndex: this.props.stepIndex, }); } - private _renderZrxAllowanceToggle(): React.ReactNode { + private _renderZrxAllowanceStateToggle(): React.ReactNode { const zrxToken = utils.getZrxToken(this.props.tokenByAddress); - return this._renderAllowanceToggle(zrxToken); + return this._renderAllowanceStateToggle(zrxToken); } - private _renderEthAllowanceToggle(): React.ReactNode { + private _renderEthAllowanceStateToggle(): React.ReactNode { const ethToken = utils.getEthToken(this.props.tokenByAddress); - return this._renderAllowanceToggle(ethToken); + return this._renderAllowanceStateToggle(ethToken); } - private _renderAllowanceToggle(token: Token): React.ReactNode { + private _renderAllowanceStateToggle(token: Token): React.ReactNode { if (!token) { return null; } @@ -260,10 +260,9 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp return null; } return ( - <AllowanceToggle + <AllowanceStateToggle token={token} tokenState={tokenStateIfExists} - isDisabled={!tokenStateIfExists.isLoaded} blockchain={this.props.blockchain} // tslint:disable-next-line:jsx-no-lambda refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(token.address)} diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 1790a9678..c61d04906 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -24,6 +24,7 @@ import { TradeHistory } from 'ts/components/trade_history/trade_history'; import { Container } from 'ts/components/ui/container'; import { FlashMessage } from 'ts/components/ui/flash_message'; import { Image } from 'ts/components/ui/image'; +import { PointerDirection } from 'ts/components/ui/pointer'; import { Text } from 'ts/components/ui/text'; import { Wallet } from 'ts/components/wallet/wallet'; import { GenerateOrderForm } from 'ts/containers/generate_order_form'; @@ -355,6 +356,9 @@ export class Portal extends React.Component<PortalProps, PortalState> { onAddToken={this._onAddToken.bind(this)} onRemoveToken={this._onRemoveToken.bind(this)} refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)} + toggleTooltipDirection={ + this.props.isPortalOnboardingShowing ? PointerDirection.Left : PointerDirection.Right + } /> </Container> {!isMobile && <Container marginTop="8px">{this._renderStartOnboarding()}</Container>} diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 550438e76..969ef32ff 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -24,7 +24,7 @@ import { SendButton } from 'ts/components/send_button'; import { HelpTooltip } from 'ts/components/ui/help_tooltip'; import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button'; import { TokenIcon } from 'ts/components/ui/token_icon'; -import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; +import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle'; import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; import { @@ -372,14 +372,15 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala )} </TableRowColumn> <TableRowColumn> - <AllowanceToggle - blockchain={this.props.blockchain} - token={token} - tokenState={tokenState} - onErrorOccurred={this._onErrorOccurred.bind(this)} - isDisabled={!tokenState.isLoaded} - refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)} - /> + <div className="flex justify-center"> + <AllowanceStateToggle + blockchain={this.props.blockchain} + token={token} + tokenState={tokenState} + onErrorOccurred={this._onErrorOccurred.bind(this)} + refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)} + /> + </div> </TableRowColumn> {utils.isTestNetwork(this.props.networkId) && ( <TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}> diff --git a/packages/website/ts/components/ui/allowance_state_view.tsx b/packages/website/ts/components/ui/allowance_state_view.tsx new file mode 100644 index 000000000..93d6b0ebb --- /dev/null +++ b/packages/website/ts/components/ui/allowance_state_view.tsx @@ -0,0 +1,51 @@ +import { colors } from '@0xproject/react-shared'; +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { Spinner } from 'ts/components/ui/spinner'; + +export enum AllowanceState { + Locked, + Unlocked, + Loading, +} + +export interface AllowanceStateViewProps { + allowanceState: AllowanceState; +} + +export const AllowanceStateView: React.StatelessComponent<AllowanceStateViewProps> = ({ allowanceState }) => { + switch (allowanceState) { + case AllowanceState.Locked: + return renderLock(); + case AllowanceState.Unlocked: + return renderCheck(); + case AllowanceState.Loading: + return ( + <Container position="relative" top="3px" left="5px"> + <Spinner size={18} strokeSize={2} /> + </Container> + ); + default: + return null; + } +}; + +const renderCheck = (color: string = colors.lightGreen) => ( + <svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle cx="8.5" cy="8.5" r="8.5" fill={color} /> + <path + d="M2.5 4.5L1.79289 5.20711L2.5 5.91421L3.20711 5.20711L2.5 4.5ZM-0.707107 2.70711L1.79289 5.20711L3.20711 3.79289L0.707107 1.29289L-0.707107 2.70711ZM3.20711 5.20711L7.70711 0.707107L6.29289 -0.707107L1.79289 3.79289L3.20711 5.20711Z" + transform="translate(5 6.5)" + fill="white" + /> + </svg> +); + +const renderLock = () => ( + <svg width="12" height="15" viewBox="0 0 12 15" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M6 0C3.51604 0 1.48688 2.0495 1.48688 4.55837V5.86581C0.664723 5.86581 -3.33647e-08 6.53719 -3.33647e-08 7.36759V13.3217C-3.33647e-08 14.1521 0.664723 14.8235 1.48688 14.8235H10.5131C11.3353 14.8235 12 14.1521 12 13.3217V7.36759C12 6.53719 11.3353 5.86581 10.5131 5.86581V4.55837C10.5131 2.0495 8.48396 0 6 0ZM8.93878 5.86581H3.06122V4.55837C3.06122 2.9329 4.37318 1.59013 6 1.59013C7.62682 1.59013 8.93878 2.9329 8.93878 4.55837V5.86581Z" + fill="black" + /> + </svg> +); diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index 502069dee..540661ea5 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -32,6 +32,7 @@ export interface ContainerProps { bottom?: string; zIndex?: number; Tag?: ContainerTag; + cursor?: string; id?: string; onClick?: (event: React.MouseEvent<HTMLElement>) => void; } diff --git a/packages/website/ts/components/ui/pointer.tsx b/packages/website/ts/components/ui/pointer.tsx index 448786bb4..db0a4188d 100644 --- a/packages/website/ts/components/ui/pointer.tsx +++ b/packages/website/ts/components/ui/pointer.tsx @@ -2,7 +2,12 @@ import { colors } from '@0xproject/react-shared'; import * as React from 'react'; import { styled } from 'ts/style/theme'; -export type PointerDirection = 'top' | 'right' | 'bottom' | 'left'; +export enum PointerDirection { + Top = 'top', + Right = 'right', + Bottom = 'bottom', + Left = 'left', +} export interface PointerProps { className?: string; diff --git a/packages/website/ts/components/ui/spinner.tsx b/packages/website/ts/components/ui/spinner.tsx new file mode 100644 index 000000000..e8670cc3e --- /dev/null +++ b/packages/website/ts/components/ui/spinner.tsx @@ -0,0 +1,54 @@ +import { colors } from '@0xproject/react-shared'; +import * as React from 'react'; +import { styled } from 'ts/style/theme'; + +import { dash, rotate } from 'ts/style/keyframes'; + +interface SpinnerSvgProps { + color: string; + size: number; + viewBox?: string; +} + +const SpinnerSvg: React.StatelessComponent<SpinnerSvgProps> = props => <svg {...props} />; + +const StyledSpinner = styled(SpinnerSvg)` + animation: ${rotate} 3s linear infinite; + margin: ${props => `-${props.size / 2}px 0 0 -${props.size / 2}px`}; + margin-top: ${props => `-${props.size / 2}px`}; + margin-left: ${props => `-${props.size / 2}px`}; + margin-bottom: 0px; + margin-right: 0px; + size: ${props => `${props.size}px`}; + height: ${props => `${props.size}px`}; + + & .path { + stroke: ${props => props.color}; + stroke-linecap: round; + animation: ${dash} 2.5s ease-in-out infinite; + } +`; + +export interface SpinnerProps { + size?: number; + strokeSize?: number; + color?: string; +} + +export const Spinner: React.StatelessComponent<SpinnerProps> = ({ size, strokeSize, color }) => { + const c = size / 2; + const r = c - strokeSize; + return ( + <StyledSpinner color={color} size={size} viewBox={`0 0 ${size} ${size}`}> + <circle className="path" cx={c} cy={c} r={r} fill="none" strokeWidth={strokeSize} /> + </StyledSpinner> + ); +}; + +Spinner.defaultProps = { + size: 50, + color: colors.mediumBlue, + strokeSize: 4, +}; + +Spinner.displayName = 'Spinner'; diff --git a/packages/website/ts/components/ui/text.tsx b/packages/website/ts/components/ui/text.tsx index b71d8225b..734483564 100644 --- a/packages/website/ts/components/ui/text.tsx +++ b/packages/website/ts/components/ui/text.tsx @@ -19,6 +19,7 @@ export interface TextProps { textDecorationLine?: string; onClick?: (event: React.MouseEvent<HTMLElement>) => void; hoverColor?: string; + noWrap?: boolean; } const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick, Tag }) => ( @@ -39,6 +40,7 @@ export const Text = styled(PlainText)` ${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')}; ${props => (props.onClick ? 'cursor: pointer' : '')}; transition: color 0.5s ease; + ${props => (props.noWrap ? 'white-space: nowrap' : '')}; &:hover { ${props => (props.onClick ? `color: ${props.hoverColor || darken(0.3, props.fontColor)}` : '')}; } @@ -53,6 +55,7 @@ Text.defaultProps = { lineHeight: '1.5em', textDecorationLine: 'none', Tag: 'div', + noWrap: false, }; Text.displayName = 'Text'; diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 40a8a23ea..6abaa840b 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -14,6 +14,7 @@ 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 { PointerDirection } from 'ts/components/ui/pointer'; import { CopyAddressSimpleMenuItem, DifferentWalletSimpleMenuItem, @@ -28,7 +29,7 @@ import { NullTokenRow } from 'ts/components/wallet/null_token_row'; import { PlaceHolder } from 'ts/components/wallet/placeholder'; import { StandardIconRow } from 'ts/components/wallet/standard_icon_row'; import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item'; -import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; +import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle'; import { Dispatcher } from 'ts/redux/dispatcher'; import { colors } from 'ts/style/colors'; import { @@ -67,6 +68,7 @@ export interface WalletProps { onRemoveToken: () => void; refetchTokenStateAsync: (tokenAddress: string) => Promise<void>; style: React.CSSProperties; + toggleTooltipDirection?: PointerDirection; } interface WalletState { @@ -74,14 +76,14 @@ interface WalletState { isHoveringSidebar: boolean; } -interface AllowanceToggleConfig { +interface AllowanceStateToggleConfig { token: Token; tokenState: TokenState; } interface AccessoryItemConfig { wrappedEtherDirection?: Side; - allowanceToggleConfig?: AllowanceToggleConfig; + allowanceStateToggleConfig?: AllowanceStateToggleConfig; } const ETHER_ICON_PATH = '/images/ether.png'; @@ -89,7 +91,8 @@ const ICON_DIMENSION = 28; const BODY_ITEM_KEY = 'BODY'; const HEADER_ITEM_KEY = 'HEADER'; const ETHER_ITEM_KEY = 'ETHER'; -const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56; +const WRAP_ROW_ALLOWANCE_TOGGLE_WIDTH = 67; +const ALLOWANCE_TOGGLE_WIDTH = 56; const PLACEHOLDER_COLOR = colors.grey300; const LOADING_ROWS_COUNT = 6; @@ -338,7 +341,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); const accessoryItemConfig: AccessoryItemConfig = { wrappedEtherDirection, - allowanceToggleConfig: { + allowanceStateToggleConfig: { token, tokenState, }, @@ -393,13 +396,15 @@ export class Wallet extends React.Component<WalletProps, WalletState> { } private _renderAccessoryItems(config: AccessoryItemConfig): React.ReactElement<{}> { const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherDirection); - const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig); + const shouldShowToggle = !_.isUndefined(config.allowanceStateToggleConfig); // if we don't have a toggle, we still want some space to the right of the "wrap" button so that it aligns with // the "unwrap" button in the row below - const toggle = shouldShowToggle ? ( - this._renderAllowanceToggle(config.allowanceToggleConfig) - ) : ( - <div style={{ width: NO_ALLOWANCE_TOGGLE_SPACE_WIDTH }} /> + const isWrapEtherRow = shouldShowWrappedEtherAction && config.wrappedEtherDirection === Side.Deposit; + const width = isWrapEtherRow ? WRAP_ROW_ALLOWANCE_TOGGLE_WIDTH : ALLOWANCE_TOGGLE_WIDTH; + const toggle = ( + <Container className="flex justify-center" width={width}> + {shouldShowToggle && this._renderAllowanceToggle(config.allowanceStateToggleConfig)} + </Container> ); return ( <div className="flex items-center"> @@ -410,14 +415,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> { </div> ); } - private _renderAllowanceToggle(config: AllowanceToggleConfig): React.ReactNode { + private _renderAllowanceToggle(config: AllowanceStateToggleConfig): React.ReactNode { // TODO: Error handling return ( - <AllowanceToggle + <AllowanceStateToggle blockchain={this.props.blockchain} token={config.token} tokenState={config.tokenState} - isDisabled={!config.tokenState.isLoaded} + tooltipDirection={this.props.toggleTooltipDirection} refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)} /> ); diff --git a/packages/website/ts/containers/inputs/allowance_toggle.ts b/packages/website/ts/containers/inputs/allowance_state_toggle.ts index 545708f92..70712685e 100644 --- a/packages/website/ts/containers/inputs/allowance_toggle.ts +++ b/packages/website/ts/containers/inputs/allowance_state_toggle.ts @@ -2,19 +2,20 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { Blockchain } from 'ts/blockchain'; +import { PointerDirection } from 'ts/components/ui/pointer'; import { State } from 'ts/redux/reducer'; import { BalanceErrs, Token, TokenState } from 'ts/types'; -import { AllowanceToggle as AllowanceToggleComponent } from 'ts/components/inputs/allowance_toggle'; +import { AllowanceStateToggle as AllowanceStateToggleComponent } from 'ts/components/inputs/allowance_state_toggle'; import { Dispatcher } from 'ts/redux/dispatcher'; -interface AllowanceToggleProps { +interface AllowanceStateToggleProps { blockchain: Blockchain; onErrorOccurred?: (errType: BalanceErrs) => void; token: Token; tokenState: TokenState; - isDisabled?: boolean; refetchTokenStateAsync: () => Promise<void>; + tooltipDirection?: PointerDirection; } interface ConnectedState { @@ -26,7 +27,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, _ownProps: AllowanceToggleProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: AllowanceStateToggleProps): ConnectedState => ({ networkId: state.networkId, userAddress: state.userAddress, }); @@ -35,7 +36,7 @@ const mapDispatchTopProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({ dispatcher: new Dispatcher(dispatch), }); -export const AllowanceToggle: React.ComponentClass<AllowanceToggleProps> = connect( +export const AllowanceStateToggle: React.ComponentClass<AllowanceStateToggleProps> = connect( mapStateToProps, mapDispatchTopProps, -)(AllowanceToggleComponent); +)(AllowanceStateToggleComponent); diff --git a/packages/website/ts/style/keyframes.ts b/packages/website/ts/style/keyframes.ts new file mode 100644 index 000000000..28ea50247 --- /dev/null +++ b/packages/website/ts/style/keyframes.ts @@ -0,0 +1,22 @@ +import { keyframes } from 'ts/style/theme'; + +export const rotate = keyframes` + 100% { + transform: rotate(360deg); + } +`; + +export const dash = keyframes` + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +`; |