diff options
Diffstat (limited to 'packages/website/ts/components')
37 files changed, 677 insertions, 200 deletions
diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx index 196414407..c9727b553 100644 --- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx +++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx @@ -250,7 +250,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, }); return true; } - private _onDerivationPathChanged(e: any, derivationPath: string): void { + private _onDerivationPathChanged(_event: any, derivationPath: string): void { let derivationErrMsg = ''; if (!_.startsWith(derivationPath, VALID_ETHEREUM_DERIVATION_PATH_PREFIX)) { derivationErrMsg = 'Must be valid Ethereum path.'; @@ -295,7 +295,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, } return userAddresses; } - private _onSelectedNetworkUpdated(e: any, index: number, networkId: number): void { + private _onSelectedNetworkUpdated(_event: any, _index: number, networkId: number): void { this.setState({ preferredNetworkId: networkId, }); diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx index ca8634693..20b446155 100644 --- a/packages/website/ts/components/eth_wrappers.tsx +++ b/packages/website/ts/components/eth_wrappers.tsx @@ -221,9 +221,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </TableHeaderColumn> </TableRow> </TableHeader> - <TableBody displayRowCheckbox={false}> - {this._renderOutdatedWeths(etherToken, this.state.ethTokenState)} - </TableBody> + <TableBody displayRowCheckbox={false}>{this._renderOutdatedWeths(etherToken)}</TableBody> </Table> </div> </div> @@ -249,7 +247,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </div> ); } - private _renderOutdatedWeths(etherToken: Token, etherTokenState: TokenState): React.ReactNode { + private _renderOutdatedWeths(etherToken: Token): React.ReactNode { const rows = _.map( configs.OUTDATED_WRAPPED_ETHERS, (outdatedWETHByNetworkId: OutdatedWrappedEtherByNetworkId) => { diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx index b6b52993e..f3ea44286 100644 --- a/packages/website/ts/components/fill_order.tsx +++ b/packages/website/ts/components/fill_order.tsx @@ -351,7 +351,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { this._onFillOrderClickFireAndForgetAsync(); } } - private _onFillAmountChange(isValid: boolean, amount?: BigNumber): void { + private _onFillAmountChange(_isValid: boolean, amount?: BigNumber): void { this.props.dispatcher.updateOrderFillAmount(amount); } private _onFillOrderJSONChanged(event: any): void { diff --git a/packages/website/ts/components/footer.tsx b/packages/website/ts/components/footer.tsx index c44e41084..9fb332a98 100644 --- a/packages/website/ts/components/footer.tsx +++ b/packages/website/ts/components/footer.tsx @@ -235,7 +235,7 @@ export class Footer extends React.Component<FooterProps, FooterState> { </div> ); } - private _updateLanguage(e: any, index: number, value: Language): void { + private _updateLanguage(_event: any, _index: number, value: Language): void { this.setState({ selectedLanguage: value, }); diff --git a/packages/website/ts/components/generate_order/generate_order_form.tsx b/packages/website/ts/components/generate_order/generate_order_form.tsx index 52e3b73cd..d26b5c3fa 100644 --- a/packages/website/ts/components/generate_order/generate_order_form.tsx +++ b/packages/website/ts/components/generate_order/generate_order_form.tsx @@ -226,7 +226,7 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G </div> ); } - private _onTokenAmountChange(token: Token, side: Side, isValid: boolean, amount?: BigNumber): void { + private _onTokenAmountChange(token: Token, side: Side, _isValid: boolean, amount?: BigNumber): void { this.props.dispatcher.updateChosenAssetToken(side, { address: token.address, amount, diff --git a/packages/website/ts/components/generate_order/new_token_form.tsx b/packages/website/ts/components/generate_order/new_token_form.tsx index a9b8e9589..176a0807b 100644 --- a/packages/website/ts/components/generate_order/new_token_form.tsx +++ b/packages/website/ts/components/generate_order/new_token_form.tsx @@ -152,7 +152,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor }; this.props.onNewTokenSubmitted(newToken); } - private _onTokenNameChanged(e: any, name: string): void { + private _onTokenNameChanged(_event: any, name: string): void { let nameErrText = ''; const maxLength = 30; const tokens = _.values(this.props.tokenByAddress); @@ -173,7 +173,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor nameErrText, }); } - private _onTokenSymbolChanged(e: any, symbol: string): void { + private _onTokenSymbolChanged(_event: any, symbol: string): void { let symbolErrText = ''; const maxLength = 5; const tokens = _.values(this.props.tokenByAddress); @@ -193,7 +193,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor symbolErrText, }); } - private _onTokenDecimalsChanged(e: any, decimals: string): void { + private _onTokenDecimalsChanged(_event: any, decimals: string): void { let decimalsErrText = ''; const maxLength = 2; if (decimals === '') { diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx index 09791f125..0dd2a5aa5 100644 --- a/packages/website/ts/components/inputs/allowance_toggle.tsx +++ b/packages/website/ts/components/inputs/allowance_toggle.tsx @@ -1,5 +1,6 @@ import { constants as sharedConstants, 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'; @@ -16,11 +17,11 @@ interface AllowanceToggleProps { networkId: number; blockchain: Blockchain; dispatcher: Dispatcher; - onErrorOccurred: (errType: BalanceErrs) => void; token: Token; tokenState: TokenState; userAddress: string; - isDisabled: boolean; + isDisabled?: boolean; + onErrorOccurred?: (errType: BalanceErrs) => void; refetchTokenStateAsync: () => Promise<void>; } @@ -55,6 +56,10 @@ const styles: Styles = { }; export class AllowanceToggle extends React.Component<AllowanceToggleProps, AllowanceToggleState> { + public static defaultProps = { + onErrorOccurred: _.noop, + isDisabled: false, + }; constructor(props: AllowanceToggleProps) { super(props); this.state = { diff --git a/packages/website/ts/components/inputs/balance_bounded_input.tsx b/packages/website/ts/components/inputs/balance_bounded_input.tsx index e5b502b25..968609030 100644 --- a/packages/website/ts/components/inputs/balance_bounded_input.tsx +++ b/packages/website/ts/components/inputs/balance_bounded_input.tsx @@ -104,7 +104,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp /> ); } - private _onValueChange(e: any, amountString: string): void { + private _onValueChange(_event: any, amountString: string): void { this._setAmountState(amountString, this.props.balance, () => { const isValid = _.isUndefined(this._validate(amountString, this.props.balance)); const isPositiveNumber = utils.isNumeric(amountString) && !_.includes(amountString, '-'); diff --git a/packages/website/ts/components/inputs/expiration_input.tsx b/packages/website/ts/components/inputs/expiration_input.tsx index 5c68080fe..79dd2f568 100644 --- a/packages/website/ts/components/inputs/expiration_input.tsx +++ b/packages/website/ts/components/inputs/expiration_input.tsx @@ -80,7 +80,7 @@ export class ExpirationInput extends React.Component<ExpirationInputProps, Expir const defaultDateTime = utils.initialOrderExpiryUnixTimestampSec(); this.props.updateOrderExpiry(defaultDateTime); } - private _onDateChanged(e: any, date: Date): void { + private _onDateChanged(_event: any, date: Date): void { const dateMoment = moment(date); this.setState({ dateMoment, @@ -88,7 +88,7 @@ export class ExpirationInput extends React.Component<ExpirationInputProps, Expir const timestamp = utils.convertToUnixTimestampSeconds(dateMoment, this.state.timeMoment); this.props.updateOrderExpiry(timestamp); } - private _onTimeChanged(e: any, time: Date): void { + private _onTimeChanged(_event: any, time: Date): void { const timeMoment = moment(time); this.setState({ timeMoment, diff --git a/packages/website/ts/components/legacy_portal/legacy_portal.tsx b/packages/website/ts/components/legacy_portal/legacy_portal.tsx index 35e917eec..b4a174a03 100644 --- a/packages/website/ts/components/legacy_portal/legacy_portal.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal.tsx @@ -310,7 +310,7 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta /> ); } - private _renderFillOrder(match: any, location: Location, history: History): React.ReactNode { + private _renderFillOrder(_match: any, _location: Location, _history: History): React.ReactNode { const initialFillOrder = !_.isUndefined(this.props.userSuppliedOrderCache) ? this.props.userSuppliedOrderCache : this._sharedOrderIfExists; @@ -329,7 +329,7 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta /> ); } - private _renderGenerateOrderForm(match: any, location: Location, history: History): React.ReactNode { + private _renderGenerateOrderForm(_match: any, _location: Location, _history: History): React.ReactNode { return ( <GenerateOrderForm blockchain={this._blockchain} diff --git a/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx b/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx new file mode 100644 index 000000000..31ce99d31 --- /dev/null +++ b/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; + +export interface AddEthOnboardingStepProps {} + +export const AddEthOnboardingStep: React.StatelessComponent<AddEthOnboardingStepProps> = () => ( + <div className="flex items-center flex-column"> + <Text> Before you begin you will need to send some ETH to your metamask wallet.</Text> + <Container marginTop="15px" marginBottom="15px"> + <img src="/images/ether_alt.svg" height="50px" width="50px" /> + </Container> + <Text> + Click on the <img src="/images/metamask_icon.png" height="20px" width="20px" /> metamask extension in your + browser and click either <b>BUY</b> or <b>DEPOSIT</b>. + </Text> + </div> +); diff --git a/packages/website/ts/components/onboarding/congrats_onboarding_step.tsx b/packages/website/ts/components/onboarding/congrats_onboarding_step.tsx new file mode 100644 index 000000000..3a8db8c36 --- /dev/null +++ b/packages/website/ts/components/onboarding/congrats_onboarding_step.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; + +export interface CongratsOnboardingStepProps {} + +export const CongratsOnboardingStep: React.StatelessComponent<CongratsOnboardingStepProps> = () => ( + <div className="flex items-center flex-column"> + <Text>Your wallet is now set up for trading. Use it on any relayer in the 0x ecosystem.</Text> + <Container marginTop="25px" marginBottom="15px" className="flex justify-center"> + <img src="/images/zrx_ecosystem.svg" height="150px" /> + </Container> + <Text>No need to log in. Each relayer automatically detects and connects to your metamask wallet.</Text> + </div> +); diff --git a/packages/website/ts/components/onboarding/install_wallet_onboarding_step.tsx b/packages/website/ts/components/onboarding/install_wallet_onboarding_step.tsx new file mode 100644 index 000000000..a54496186 --- /dev/null +++ b/packages/website/ts/components/onboarding/install_wallet_onboarding_step.tsx @@ -0,0 +1,18 @@ +import { colors } from '@0xproject/react-shared'; +import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; + +export interface InstallWalletOnboardingStepProps {} + +export const InstallWalletOnboardingStep: React.StatelessComponent<InstallWalletOnboardingStepProps> = () => ( + <div className="flex items-center flex-column"> + <Container marginTop="15px" marginBottom="15px"> + <ActionAccountBalanceWallet style={{ width: '30px', height: '30px' }} color={colors.orange} /> + </Container> + <Text> + Before you begin, you need to connect to a wallet. This will be used across all 0x relayers and dApps. + </Text> + </div> +); diff --git a/packages/website/ts/components/onboarding/intro_onboarding_step.tsx b/packages/website/ts/components/onboarding/intro_onboarding_step.tsx new file mode 100644 index 000000000..548839218 --- /dev/null +++ b/packages/website/ts/components/onboarding/intro_onboarding_step.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; + +export interface IntroOnboardingStepProps {} + +export const IntroOnboardingStep: React.StatelessComponent<IntroOnboardingStepProps> = () => ( + <div className="flex items-center flex-column"> + <Text> + In order to start trading on any 0x relayer in the 0x ecosystem, you need to complete two simple steps. + </Text> + <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-around"> + <div className="flex flex-column items-center"> + <img src="/images/eth_token.svg" height="50px" width="50x" /> + <Text> Wrap ETH </Text> + </div> + <div className="flex flex-column items-center"> + <img src="/images/fake_toggle.svg" height="50px" width="50px" /> + <Text> Unlock tokens </Text> + </div> + </Container> + </div> +); diff --git a/packages/website/ts/components/onboarding/onboarding_flow.tsx b/packages/website/ts/components/onboarding/onboarding_flow.tsx index 9879cd387..34aeace82 100644 --- a/packages/website/ts/components/onboarding/onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/onboarding_flow.tsx @@ -10,9 +10,10 @@ export interface Step { title?: string; content: React.ReactNode; placement?: Placement; - hideBackButton?: boolean; - hideNextButton?: boolean; + shouldHideBackButton?: boolean; + shouldHideNextButton?: boolean; continueButtonDisplay?: ContinueButtonDisplay; + continueButtonText?: string; } export interface OnboardingFlowProps { @@ -54,17 +55,18 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { const step = steps[stepIndex]; const isLastStep = steps.length - 1 === stepIndex; return ( - <Container marginLeft="15px"> + <Container marginLeft="30px"> <OnboardingTooltip title={step.title} content={step.content} isLastStep={isLastStep} - hideBackButton={step.hideBackButton} - hideNextButton={step.hideNextButton} + shouldHideBackButton={step.shouldHideBackButton} + shouldHideNextButton={step.shouldHideNextButton} onClose={this.props.onClose} onClickNext={this._goToNextStep.bind(this)} onClickBack={this._goToPrevStep.bind(this)} continueButtonDisplay={step.continueButtonDisplay} + continueButtonText={step.continueButtonText} /> </Container> ); diff --git a/packages/website/ts/components/onboarding/onboarding_tooltip.tsx b/packages/website/ts/components/onboarding/onboarding_tooltip.tsx index 155c70c5f..45851b4de 100644 --- a/packages/website/ts/components/onboarding/onboarding_tooltip.tsx +++ b/packages/website/ts/components/onboarding/onboarding_tooltip.tsx @@ -1,7 +1,12 @@ +import { colors } from '@0xproject/react-shared'; import * as React from 'react'; +import { Button } from 'ts/components/ui/button'; import { Container } from 'ts/components/ui/container'; +import { IconButton } from 'ts/components/ui/icon_button'; import { Island } from 'ts/components/ui/island'; +import { Pointer, PointerDirection } from 'ts/components/ui/pointer'; +import { Text, Title } from 'ts/components/ui/text'; export type ContinueButtonDisplay = 'enabled' | 'disabled'; @@ -13,43 +18,73 @@ export interface OnboardingTooltipProps { onClickNext: () => void; onClickBack: () => void; continueButtonDisplay?: ContinueButtonDisplay; - hideBackButton?: boolean; - hideNextButton?: boolean; + shouldHideBackButton?: boolean; + shouldHideNextButton?: boolean; + pointerDirection?: PointerDirection; + continueButtonText?: string; + className?: string; } -// TODO: Make this more general button. -export interface ContinueButtonProps { - display: ContinueButtonDisplay; - children?: string; - onClick: () => void; -} +export const OnboardingTooltip: React.StatelessComponent<OnboardingTooltipProps> = ({ + title, + content, + continueButtonDisplay, + continueButtonText, + onClickNext, + onClickBack, + onClose, + shouldHideBackButton, + shouldHideNextButton, + pointerDirection, + className, +}) => ( + <Pointer className={className} direction={pointerDirection}> + <Island> + <Container paddingRight="30px" paddingLeft="30px" maxWidth={350} paddingTop="15px" paddingBottom="15px"> + <div className="flex flex-column"> + <div className="flex justify-between"> + <Title>{title}</Title> + <Container position="relative" bottom="20px" left="15px"> + <IconButton color={colors.grey} iconName="zmdi-close" onClick={onClose}> + Close + </IconButton> + </Container> + </div> + <Container marginBottom="15px"> + <Text>{content}</Text> + </Container> + {continueButtonDisplay && ( + <Button + isDisabled={continueButtonDisplay === 'disabled'} + onClick={onClickNext} + fontColor={colors.white} + fontSize="15px" + backgroundColor={colors.mediumBlue} + > + {continueButtonText} + </Button> + )} + <Container className="flex justify-between" marginTop="15px"> + {!shouldHideBackButton && ( + <Text fontColor={colors.grey} onClick={onClickBack}> + Back + </Text> + )} + {!shouldHideNextButton && ( + <Text fontColor={colors.grey} onClick={onClickNext}> + Skip + </Text> + )} + </Container> + </div> + </Container> + </Island> + </Pointer> +); -export const ContinueButton: React.StatelessComponent<ContinueButtonProps> = (props: ContinueButtonProps) => { - const isDisabled = props.display === 'disabled'; - return ( - <button disabled={isDisabled} onClick={isDisabled ? undefined : props.onClick}> - {props.children} - </button> - ); +OnboardingTooltip.defaultProps = { + pointerDirection: 'left', + continueButtonText: 'Continue', }; -export const OnboardingTooltip: React.StatelessComponent<OnboardingTooltipProps> = (props: OnboardingTooltipProps) => ( - <Island> - <Container paddingRight="30px" paddingLeft="30px" maxWidth={350} paddingTop="15px" paddingBottom="15px"> - <div className="flex flex-column"> - {props.title} - {props.content} - {props.continueButtonDisplay && ( - <ContinueButton onClick={props.onClickNext} display={props.continueButtonDisplay}> - Continue - </ContinueButton> - )} - {!props.hideBackButton && <button onClick={props.onClickBack}>Back</button>} - {!props.hideNextButton && <button onClick={props.onClickNext}>Skip</button>} - <button onClick={props.onClose}>Close</button> - </div> - </Container> - </Island> -); - 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 bf52684d7..4283022e2 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -2,11 +2,21 @@ import * as _ from 'lodash'; import * as React from 'react'; import { BigNumber } from '@0xproject/utils'; +import { Blockchain } from 'ts/blockchain'; +import { AddEthOnboardingStep } from 'ts/components/onboarding/add_eth_onboarding_step'; +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 { ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types'; +import { SetAllowancesOnboardingStep } from 'ts/components/onboarding/set_allowances_onboarding_step'; +import { UnlockWalletOnboardingStep } from 'ts/components/onboarding/unlock_wallet_onboarding_step'; +import { WrapEthOnboardingStep } from 'ts/components/onboarding/wrap_eth_onboarding_step'; +import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; +import { ProviderType, Token, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { utils } from 'ts/utils/utils'; export interface PortalOnboardingFlowProps { + blockchain: Blockchain; stepIndex: number; isRunning: boolean; userAddress: string; @@ -19,6 +29,7 @@ export interface PortalOnboardingFlowProps { trackedTokenStateByAddress: TokenStateByAddress; updateIsRunning: (isRunning: boolean) => void; updateOnboardingStep: (stepIndex: number) => void; + refetchTokenStateAsync: (tokenAddress: string) => Promise<void>; } export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowProps> { @@ -39,73 +50,117 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr /> ); } - private _getSteps(): Step[] { const steps: Step[] = [ { target: '.wallet', - content: - 'Before you begin, you need to connect to a wallet. This will be used across all 0x relayers and dApps', + title: '0x Ecosystem Setup', + content: <InstallWalletOnboardingStep />, placement: 'right', - hideBackButton: true, - hideNextButton: true, + shouldHideBackButton: true, + shouldHideNextButton: true, }, { target: '.wallet', - content: 'Unlock your metamask extension to begin', + title: '0x Ecosystem Setup', + content: <UnlockWalletOnboardingStep />, placement: 'right', - hideBackButton: true, - hideNextButton: true, + shouldHideBackButton: true, + shouldHideNextButton: true, }, { target: '.wallet', - content: - 'In order to start trading on any 0x relayer in the 0x ecosystem, you need to complete two simple steps', + title: '0x Ecosystem Account Setup', + content: <IntroOnboardingStep />, placement: 'right', - hideBackButton: true, + shouldHideBackButton: true, continueButtonDisplay: 'enabled', }, { target: '.eth-row', - content: 'Before you begin you will need to send some ETH to your metamask wallet', + title: 'Add ETH', + content: <AddEthOnboardingStep />, placement: 'right', continueButtonDisplay: this._userHasVisibleEth() ? 'enabled' : 'disabled', }, { target: '.weth-row', - content: 'You need to convert some of your ETH into tradeable Wrapped ETH (WETH)', + title: 'Step 1/2', + content: ( + <WrapEthOnboardingStep + formattedEthBalanceIfExists={ + this._userHasVisibleWeth() ? this._getFormattedWethBalance() : undefined + } + /> + ), + placement: 'right', + continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : undefined, + }, + { + target: '.weth-row', + title: 'Step 2/2', + content: ( + <SetAllowancesOnboardingStep + zrxAllowanceToggle={this._renderZrxAllowanceToggle()} + ethAllowanceToggle={this._renderEthAllowanceToggle()} + /> + ), + placement: 'right', + continueButtonDisplay: this._userHasAllowancesForWethAndZrx() ? 'enabled' : 'disabled', + }, + { + target: '.wallet', + title: '🎉 Congrats! The ecosystem awaits.', + content: <CongratsOnboardingStep />, placement: 'right', - continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled', + continueButtonDisplay: 'enabled', + shouldHideNextButton: true, + continueButtonText: 'Enter the 0x Ecosystem', }, ]; return steps; } - private _isAddressAvailable(): boolean { return !_.isEmpty(this.props.userAddress); } - private _userHasVisibleEth(): boolean { return this.props.userEtherBalanceInWei > new BigNumber(0); } - - private _userHasVisibleWeth(): boolean { + private _getWethBalance(): BigNumber { const ethToken = utils.getEthToken(this.props.tokenByAddress); if (!ethToken) { - return false; + return new BigNumber(0); } - const wethTokenState = this.props.trackedTokenStateByAddress[ethToken.address]; - return wethTokenState.balance > new BigNumber(0); + const ethTokenState = this.props.trackedTokenStateByAddress[ethToken.address]; + return ethTokenState.balance; + } + private _getFormattedWethBalance(): string { + const ethToken = utils.getEthToken(this.props.tokenByAddress); + const ethTokenState = this.props.trackedTokenStateByAddress[ethToken.address]; + return utils.getFormattedAmountFromToken(ethToken, ethTokenState); + } + private _userHasVisibleWeth(): boolean { + return this._getWethBalance() > new BigNumber(0); + } + private _userHasAllowancesForWethAndZrx(): boolean { + const ethToken = utils.getEthToken(this.props.tokenByAddress); + const zrxToken = utils.getZrxToken(this.props.tokenByAddress); + if (ethToken && zrxToken) { + const ethTokenAllowance = this.props.trackedTokenStateByAddress[ethToken.address].allowance; + const zrxTokenAllowance = this.props.trackedTokenStateByAddress[zrxToken.address].allowance; + return ethTokenAllowance > new BigNumber(0) && zrxTokenAllowance > new BigNumber(0); + } + return false; } - private _overrideOnboardingStateIfShould(): void { this._autoStartOnboardingIfShould(); this._adjustStepIfShould(); } private _adjustStepIfShould(): void { + const stepIndex = this.props.stepIndex; if (this._isAddressAvailable()) { - if (this.props.stepIndex < 2) { + if (stepIndex < 2) { this.props.updateOnboardingStep(2); } return; @@ -115,14 +170,42 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr this.props.injectedProviderName, ); if (isExternallyInjected) { - this.props.updateOnboardingStep(1); + if (stepIndex !== 1) { + this.props.updateOnboardingStep(1); + } return; } - this.props.updateOnboardingStep(0); + if (stepIndex !== 0) { + this.props.updateOnboardingStep(0); + } } private _autoStartOnboardingIfShould(): void { if (!this.props.isRunning && !this.props.hasBeenSeen && this.props.blockchainIsLoaded) { this.props.updateIsRunning(true); } } + private _renderZrxAllowanceToggle(): React.ReactNode { + const zrxToken = utils.getZrxToken(this.props.tokenByAddress); + return this._renderAllowanceToggle(zrxToken); + } + private _renderEthAllowanceToggle(): React.ReactNode { + const ethToken = utils.getEthToken(this.props.tokenByAddress); + return this._renderAllowanceToggle(ethToken); + } + private _renderAllowanceToggle(token: Token): React.ReactNode { + if (!token) { + return null; + } + const tokenState = this.props.trackedTokenStateByAddress[token.address]; + return ( + <AllowanceToggle + token={token} + tokenState={tokenState} + isDisabled={!tokenState.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/onboarding/set_allowances_onboarding_step.tsx b/packages/website/ts/components/onboarding/set_allowances_onboarding_step.tsx new file mode 100644 index 000000000..1ff248c40 --- /dev/null +++ b/packages/website/ts/components/onboarding/set_allowances_onboarding_step.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; + +export interface SetAllowancesOnboardingStepProps { + zrxAllowanceToggle: React.ReactNode; + ethAllowanceToggle: React.ReactNode; +} + +export const SetAllowancesOnboardingStep: React.StatelessComponent<SetAllowancesOnboardingStepProps> = ({ + ethAllowanceToggle, + zrxAllowanceToggle, +}) => ( + <div className="flex items-center flex-column"> + <Text>Unlock your tokens for trading. You only need to do this once for each token.</Text> + <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-around"> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> Enable WETH </Text> + <Container marginTop="10px">{ethAllowanceToggle}</Container> + </div> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> Enable ZRX </Text> + <Container marginTop="10px">{zrxAllowanceToggle}</Container> + </div> + </Container> + </div> +); diff --git a/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx b/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx new file mode 100644 index 000000000..6e6a74a06 --- /dev/null +++ b/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; + +export interface UnlockWalletOnboardingStepProps {} + +export const UnlockWalletOnboardingStep: React.StatelessComponent<UnlockWalletOnboardingStepProps> = () => ( + <div className="flex items-center flex-column"> + <div className="flex items-center flex-column"> + <Container marginTop="15px" marginBottom="15px"> + <img src="/images/metamask_icon.png" height="50px" width="50px" /> + </Container> + <Text>Unlock your metamask extension to begin.</Text> + </div> + </div> +); diff --git a/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx b/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx new file mode 100644 index 000000000..b21b39341 --- /dev/null +++ b/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx @@ -0,0 +1,73 @@ +import { colors } from '@0xproject/react-shared'; +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { IconButton } from 'ts/components/ui/icon_button'; +import { Text } from 'ts/components/ui/text'; + +export interface WrapEthOnboardingStepProps { + formattedEthBalanceIfExists?: string; +} + +export const WrapEthOnboardingStep: React.StatelessComponent<WrapEthOnboardingStepProps> = ({ + formattedEthBalanceIfExists, +}) => { + if (formattedEthBalanceIfExists) { + return ( + <div className="flex items-center flex-column"> + <Text>Congrats you now have {formattedEthBalanceIfExists} in your wallet.</Text> + <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-center"> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> 1 ETH </Text> + <img src="/images/eth_dollar.svg" height="75px" width="75x" /> + </div> + <Container marginRight="25px" marginLeft="25px" position="relative" top="20px"> + <Text fontSize="25px"> + <i className="zmdi zmdi-long-arrow-right" /> + </Text> + </Container> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> 1 WETH </Text> + <img src="/images/eth_token_erc20.svg" height="75px" width="75px" /> + </div> + </Container> + </div> + ); + } else { + return ( + <div className="flex items-center flex-column"> + <Text> + You need to convert some of your ETH into tradeable <b>Wrapped ETH (WETH)</b>. + </Text> + <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-center"> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> 1 ETH </Text> + <img src="/images/eth_dollar.svg" height="75px" width="75x" /> + </div> + <Container marginRight="25px" marginLeft="25px" position="relative" top="20px"> + <Text fontSize="36px">=</Text> + </Container> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> 1 WETH </Text> + <img src="/images/eth_token_erc20.svg" height="75px" width="75px" /> + </div> + </Container> + <Text> + Think of it like the coin version of a paper note. It has the same value, but some machines only + take coins. + </Text> + <Text> + Click + <Container display="inline-block" marginLeft="10px" marginRight="10px"> + <IconButton + iconName="zmdi-long-arrow-down" + color={colors.mediumBlue} + labelText="wrap" + display="inline-flex" + /> + </Container> + to wrap your ETH. + </Text> + </div> + ); + } +}; diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index fb40a9580..28a303793 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -1,6 +1,7 @@ import { colors, Styles } from '@0xproject/react-shared'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; +import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; import * as React from 'react'; import * as DocumentTitle from 'react-document-title'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; @@ -24,6 +25,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 { Island } from 'ts/components/ui/island'; +import { Text } from 'ts/components/ui/text'; import { Wallet } from 'ts/components/wallet/wallet'; import { GenerateOrderForm } from 'ts/containers/generate_order_form'; import { PortalOnboardingFlow } from 'ts/containers/portal_onboarding_flow'; @@ -100,6 +102,7 @@ const THROTTLE_TIMEOUT = 100; const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded); const LEFT_COLUMN_WIDTH = 346; const MENU_PADDING_LEFT = 185; +const LARGE_LAYOUT_MAX_WIDTH = 1200; const styles: Styles = { root: { @@ -235,7 +238,11 @@ export class Portal extends React.Component<PortalProps, PortalState> { : TokenVisibility.TRACKED; return ( <div style={styles.root}> - <PortalOnboardingFlow trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} /> + <PortalOnboardingFlow + blockchain={this._blockchain} + trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} + refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)} + /> <DocumentTitle title="0x Portal DApp" /> <TopBar userAddress={this.props.userAddress} @@ -250,6 +257,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { translate={this.props.translate} displayType={TopBarDisplayType.Expanded} style={{ backgroundColor: colors.lightestGrey }} + maxWidth={LARGE_LAYOUT_MAX_WIDTH} /> <div id="portal" style={styles.body}> <Switch> @@ -349,8 +357,26 @@ export class Portal extends React.Component<PortalProps, PortalState> { /> <Container marginTop="15px"> <Island> - {/** TODO: Implement real styles. */} - <p onClick={this._startOnboarding.bind(this)}>Start onboarding flow.</p> + <Container + marginTop="30px" + marginBottom="30px" + marginLeft="30px" + marginRight="30px" + className="flex justify-around items-center" + > + <ActionAccountBalanceWallet + style={{ width: '30px', height: '30px' }} + color={colors.orange} + /> + <Text + fontColor={colors.grey} + fontSize="16px" + center={true} + onClick={this._startOnboarding.bind(this)} + > + Learn how to set up your account + </Text> + </Container> </Island> </Container> </div> @@ -634,7 +660,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { const tokenSymbols = _.keys(tokenAddressBySymbol); try { const priceBySymbol = await backendClient.getPriceInfoAsync(tokenSymbols); - const priceByAddress = _.mapKeys(priceBySymbol, (value, symbol) => _.get(tokenAddressBySymbol, symbol)); + const priceByAddress = _.mapKeys(priceBySymbol, (_value, symbol) => _.get(tokenAddressBySymbol, symbol)); const result = _.mapValues(priceByAddress, price => { const priceBigNumber = new BigNumber(price); return priceBigNumber; @@ -656,11 +682,11 @@ interface LargeLayoutProps { } const LargeLayout = (props: LargeLayoutProps) => { return ( - <div className="sm-flex flex-center"> - <div className="flex-last px3"> + <div className="mx-auto flex flex-center" style={{ maxWidth: LARGE_LAYOUT_MAX_WIDTH }}> + <div className="flex-last px2"> <div style={styles.leftColumn}>{props.left}</div> </div> - <div className="flex-auto px3" style={styles.scrollContainer}> + <div className="flex-auto px2" style={styles.scrollContainer}> {props.right} </div> </div> @@ -672,7 +698,7 @@ interface SmallLayoutProps { } const SmallLayout = (props: SmallLayoutProps) => { return ( - <div className="sm-flex flex-center"> + <div className="flex flex-center"> <div className="flex-auto px3" style={styles.scrollContainer}> {props.content} </div> diff --git a/packages/website/ts/components/redirecter.tsx b/packages/website/ts/components/redirector.tsx index 07432a926..a02693003 100644 --- a/packages/website/ts/components/redirecter.tsx +++ b/packages/website/ts/components/redirector.tsx @@ -1,9 +1,9 @@ import { constants } from 'ts/utils/constants'; -interface RedirecterProps { +interface RedirectorProps { location: string; } -export function Redirecter(props: RedirecterProps): void { +export function Redirector(_props: RedirectorProps): void { window.location.href = constants.URL_ANGELLIST; } 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 fbb634164..98d6dc0b3 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { TopTokens } from 'ts/components/relayer_index/relayer_top_tokens'; 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 { WebsiteBackendRelayerInfo } from 'ts/types'; @@ -26,7 +27,6 @@ const styles: Styles = { header: { height: '50%', width: '100%', - objectFit: 'cover', borderBottomRightRadius: 4, borderBottomLeftRadius: 4, borderTopRightRadius: 4, @@ -58,21 +58,34 @@ const styles: Styles = { }; const FALLBACK_IMG_SRC = '/images/landing/hero_chip_image.png'; +const FALLBACK_PRIMARY_COLOR = colors.grey200; const NO_CONTENT_MESSAGE = '--'; +const RELAYER_ICON_HEIGHT = '110px'; export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (props: RelayerGridTileProps) => { const link = props.relayerInfo.appUrl || props.relayerInfo.url; const topTokens = props.relayerInfo.topTokens; const weeklyTxnVolume = props.relayerInfo.weeklyTxnVolume; + const headerImageUrl = props.relayerInfo.logoImgUrl; + const headerBackgroundColor = + !_.isUndefined(headerImageUrl) && !_.isUndefined(props.relayerInfo.primaryColor) + ? props.relayerInfo.primaryColor + : FALLBACK_PRIMARY_COLOR; return ( <Island style={styles.root} Component={GridTile}> <div style={styles.innerDiv}> <a href={link} target="_blank" style={{ textDecoration: 'none' }}> - <ImgWithFallback - src={props.relayerInfo.headerImgUrl} - fallbackSrc={FALLBACK_IMG_SRC} - style={styles.header} - /> + <div + className="flex items-center" + style={{ ...styles.header, backgroundColor: headerBackgroundColor }} + > + <Image + className="mx-auto" + src={props.relayerInfo.logoImgUrl} + fallbackSrc={FALLBACK_IMG_SRC} + height={RELAYER_ICON_HEIGHT} + /> + </div> </a> <div style={styles.body}> <div className="py1" style={styles.relayerNameLabel}> @@ -108,32 +121,3 @@ const Section = (props: SectionProps) => { }; const NoContent = () => <div style={styles.subLabel}>{NO_CONTENT_MESSAGE}</div>; - -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(): React.ReactNode { - 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(): void { - 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 683f7084b..d565eb608 100644 --- a/packages/website/ts/components/relayer_index/relayer_index.tsx +++ b/packages/website/ts/components/relayer_index/relayer_index.tsx @@ -1,11 +1,11 @@ import { 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'; import { RelayerGridTile } from 'ts/components/relayer_index/relayer_grid_tile'; +import { Retry } from 'ts/components/ui/retry'; import { colors } from 'ts/style/colors'; import { ScreenWidths, WebsiteBackendRelayerInfo } from 'ts/types'; import { backendClient } from 'ts/utils/backend_client'; @@ -37,9 +37,9 @@ const styles: Styles = { }; const CELL_HEIGHT = 290; -const NUMBER_OF_COLUMNS_LARGE = 4; -const NUMBER_OF_COLUMNS_MEDIUM = 3; -const NUMBER_OF_COLUMNS_SMALL = 1; +const NUMBER_OF_COLUMNS_LARGE = 3; +const NUMBER_OF_COLUMNS_MEDIUM = 2; +const NUMBER_OF_COLUMNS_SMALL = 2; const GRID_PADDING = 20; export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerIndexState> { @@ -63,7 +63,8 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde const isReadyToRender = _.isUndefined(this.state.error) && !_.isUndefined(this.state.relayerInfos); if (!isReadyToRender) { return ( - // TODO: consolidate this loading component with the one in portal + // TODO: consolidate this loading component with the one in portal and OpenPositions + // TODO: possibly refactor into a generic loading container with spinner and retry UI <div className="center"> {_.isUndefined(this.state.error) ? ( <CircularProgress size={40} thickness={5} /> @@ -124,31 +125,3 @@ 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/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 555a59830..7af80745c 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -20,11 +20,11 @@ import ReactTooltip = require('react-tooltip'); import firstBy = require('thenby'); import { Blockchain } from 'ts/blockchain'; import { AssetPicker } from 'ts/components/generate_order/asset_picker'; -import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle'; 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 { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; import { @@ -362,13 +362,10 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala </TableRowColumn> <TableRowColumn> <AllowanceToggle - networkId={this.props.networkId} blockchain={this.props.blockchain} - dispatcher={this.props.dispatcher} token={token} tokenState={tokenState} onErrorOccurred={this._onErrorOccurred.bind(this)} - userAddress={this.props.userAddress} isDisabled={!tokenState.isLoaded} refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)} /> @@ -581,7 +578,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala } return true; } - private _onErrorDialogToggle(isOpen: boolean): void { + private _onErrorDialogToggle(_isOpen: boolean): void { this.setState({ errorType: undefined, }); diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 05cc6e3ad..1a69827a4 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -45,6 +45,7 @@ export interface TopBarProps { isNightVersion?: boolean; onVersionSelected?: (semver: string) => void; sidebarHeader?: React.ReactNode; + maxWidth?: number; } interface TopBarState { @@ -213,7 +214,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { const shouldShowPortalV2Drawer = this._isViewingPortal() && utils.shouldShowPortalV2(); return ( <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style, ...{ height } }} className="pb1"> - <div className={parentClassNames}> + <div className={parentClassNames} style={{ maxWidth: this.props.maxWidth }}> <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" /> diff --git a/packages/website/ts/components/ui/button.tsx b/packages/website/ts/components/ui/button.tsx index 4c7d59839..cb542a386 100644 --- a/packages/website/ts/components/ui/button.tsx +++ b/packages/website/ts/components/ui/button.tsx @@ -1,5 +1,5 @@ import { colors } from '@0xproject/react-shared'; -import { darken } from 'polished'; +import { darken, grayscale } from 'polished'; import * as React from 'react'; import { styled } from 'ts/style/theme'; @@ -7,36 +7,39 @@ export interface ButtonProps { className?: string; fontSize?: string; fontColor?: string; + fontFamily?: string; backgroundColor?: string; borderColor?: string; width?: string; type?: string; + isDisabled?: boolean; onClick?: (event: React.MouseEvent<HTMLElement>) => void; } -const PlainButton: React.StatelessComponent<ButtonProps> = ({ children, onClick, type, className }) => ( - <button type={type} className={className} onClick={onClick}> +const PlainButton: React.StatelessComponent<ButtonProps> = ({ children, isDisabled, onClick, type, className }) => ( + <button type={type} className={className} onClick={isDisabled ? undefined : onClick}> {children} </button> ); export const Button = styled(PlainButton)` - cursor: pointer; + cursor: ${props => (props.isDisabled ? 'default' : 'pointer')}; font-size: ${props => props.fontSize}; color: ${props => props.fontColor}; + transition: background-color 0.5s ease; padding: 0.8em 2.2em; border-radius: 6px; box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25); font-weight: 500; - font-family: 'Roboto'; + font-family: ${props => props.fontFamily}; width: ${props => props.width}; - background-color: ${props => props.backgroundColor}; + background-color: ${props => (props.isDisabled ? grayscale(props.backgroundColor) : props.backgroundColor)}; border: ${props => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')}; &:hover { - background-color: ${props => darken(0.1, props.backgroundColor)}; + background-color: ${props => (!props.isDisabled ? darken(0.1, props.backgroundColor) : '')}; } &:active { - background-color: ${props => darken(0.2, props.backgroundColor)}; + background-color: ${props => (!props.isDisabled ? darken(0.2, props.backgroundColor) : '')}; } `; @@ -44,6 +47,8 @@ Button.defaultProps = { fontSize: '12px', backgroundColor: colors.white, width: 'auto', + fontFamily: 'Roboto', + isDisabled: false, }; Button.displayName = 'Button'; diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index c6a78e181..1776345da 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -14,8 +14,15 @@ export interface ContainerProps { backgroundColor?: string; borderRadius?: StringOrNum; maxWidth?: StringOrNum; + width?: StringOrNum; isHidden?: boolean; className?: string; + position?: 'absolute' | 'fixed' | 'relative' | 'unset'; + display?: 'inline-block' | 'block' | 'inline-flex' | 'inline'; + top?: string; + left?: string; + right?: string; + bottom?: string; } export const Container: React.StatelessComponent<ContainerProps> = ({ children, className, isHidden, ...style }) => { diff --git a/packages/website/ts/components/ui/drop_down.tsx b/packages/website/ts/components/ui/drop_down.tsx index db79ca1df..22cb942f8 100644 --- a/packages/website/ts/components/ui/drop_down.tsx +++ b/packages/website/ts/components/ui/drop_down.tsx @@ -42,7 +42,7 @@ export class DropDown extends React.Component<DropDownProps, DropDownState> { public componentWillUnmount(): void { window.clearInterval(this._popoverCloseCheckIntervalId); } - public componentWillReceiveProps(nextProps: DropDownProps): void { + public componentWillReceiveProps(_nextProps: DropDownProps): void { // HACK: If the popoverContent is updated to a different dimension and the users // mouse is no longer above it, the dropdown can enter an inconsistent state where // it believes the user is still hovering over it. In order to remedy this, we diff --git a/packages/website/ts/components/ui/filled_image.tsx b/packages/website/ts/components/ui/filled_image.tsx new file mode 100644 index 000000000..7f58ee5b9 --- /dev/null +++ b/packages/website/ts/components/ui/filled_image.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +export interface FilledImageProps { + src: string; +} +export const FilledImage = (props: FilledImageProps) => ( + <div + style={{ + width: '100%', + height: '100%', + objectFit: 'cover', + backgroundImage: `url(${props.src})`, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + backgroundSize: 'cover', + }} + /> +); diff --git a/packages/website/ts/components/ui/icon_button.tsx b/packages/website/ts/components/ui/icon_button.tsx index 2f5172f05..13cd239da 100644 --- a/packages/website/ts/components/ui/icon_button.tsx +++ b/packages/website/ts/components/ui/icon_button.tsx @@ -5,15 +5,15 @@ import * as React from 'react'; export interface IconButtonProps { iconName: string; labelText?: string; - onClick: () => void; + onClick?: () => void; color?: string; + display?: string; } interface IconButtonState { isHovering: boolean; } export class IconButton extends React.Component<IconButtonProps, IconButtonState> { public static defaultProps: Partial<IconButtonProps> = { - onClick: _.noop, labelText: '', color: colors.mediumBlue, }; @@ -26,8 +26,9 @@ export class IconButton extends React.Component<IconButtonProps, IconButtonState public render(): React.ReactNode { const styles: Styles = { root: { - cursor: 'pointer', - opacity: this.state.isHovering ? 0.5 : 1, + cursor: this.props.onClick ? 'pointer' : 'undefined', + opacity: this.state.isHovering && this.props.onClick ? 0.5 : 1, + display: this.props.display, }, icon: { color: this.props.color, diff --git a/packages/website/ts/components/ui/image.tsx b/packages/website/ts/components/ui/image.tsx new file mode 100644 index 000000000..0958d2e5e --- /dev/null +++ b/packages/website/ts/components/ui/image.tsx @@ -0,0 +1,37 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +export interface ImageProps { + className?: string; + src?: string; + fallbackSrc?: string; + height?: string; +} +interface ImageState { + imageLoadFailed: boolean; +} +export class Image extends React.Component<ImageProps, ImageState> { + constructor(props: ImageProps) { + super(props); + this.state = { + imageLoadFailed: false, + }; + } + public render(): React.ReactNode { + const src = + this.state.imageLoadFailed || _.isUndefined(this.props.src) ? this.props.fallbackSrc : this.props.src; + return ( + <img + className={this.props.className} + onError={this._onError.bind(this)} + src={src} + height={this.props.height} + /> + ); + } + private _onError(): void { + this.setState({ + imageLoadFailed: true, + }); + } +} diff --git a/packages/website/ts/components/ui/pointer.tsx b/packages/website/ts/components/ui/pointer.tsx new file mode 100644 index 000000000..448786bb4 --- /dev/null +++ b/packages/website/ts/components/ui/pointer.tsx @@ -0,0 +1,67 @@ +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 interface PointerProps { + className?: string; + color?: string; + size?: number; + direction: PointerDirection; +} + +const PlainPointer: React.StatelessComponent<PointerProps> = props => <div {...props} />; + +const positionToCss = (props: PointerProps) => { + const position = { + top: `bottom: 100%; left: 50%;`, + right: `left: 100%; top: 50%;`, + bottom: `top: 100%; left: 50%;`, + left: `right: 100%; top: 50%;`, + }[props.direction]; + + const borderColorSide = { + top: 'border-bottom-color', + right: 'border-left-color', + bottom: 'border-top-color', + left: 'border-right-color', + }[props.direction]; + const border = `${borderColorSide}: ${props.color};`; + const marginSide = { + top: 'margin-left', + right: 'margin-top', + bottom: 'margin-left', + left: 'margin-top', + }[props.direction]; + const margin = `${marginSide}: -${props.size}px`; + return { + position, + border, + margin, + }; +}; + +export const Pointer = styled(PlainPointer)` + position: relative; + &:after { + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(136, 183, 213, 0); + border-width: ${props => `${props.size}px`}; + ${props => positionToCss(props).position} + ${props => positionToCss(props).border} + ${props => positionToCss(props).margin} + } +`; + +Pointer.defaultProps = { + color: colors.white, + size: 16, +}; + +Pointer.displayName = 'Pointer'; diff --git a/packages/website/ts/components/ui/retry.tsx b/packages/website/ts/components/ui/retry.tsx new file mode 100644 index 000000000..543b7df4b --- /dev/null +++ b/packages/website/ts/components/ui/retry.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +import { Button } from 'ts/components/ui/button'; +import { colors } from 'ts/style/colors'; + +const BUTTON_TEXT = 'reload'; + +export interface RetryProps { + onRetry: () => void; +} +export 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"> + <Button + type="button" + backgroundColor={colors.black} + width="290px" + fontColor={colors.white} + fontSize="18px" + fontFamily="Roboto Mono" + onClick={props.onRetry} + > + {BUTTON_TEXT} + </Button> + </div> + </div> + </div> +); diff --git a/packages/website/ts/components/ui/text.tsx b/packages/website/ts/components/ui/text.tsx index e90c1707d..1e2a123b7 100644 --- a/packages/website/ts/components/ui/text.tsx +++ b/packages/website/ts/components/ui/text.tsx @@ -1,8 +1,9 @@ import { colors } from '@0xproject/react-shared'; +import { darken } from 'polished'; import * as React from 'react'; import { styled } from 'ts/style/theme'; -export type TextTag = 'p' | 'div' | 'span' | 'label'; +export type TextTag = 'p' | 'div' | 'span' | 'label' | 'h1' | 'h2' | 'h3' | 'h4'; export interface TextProps { className?: string; @@ -11,12 +12,16 @@ export interface TextProps { fontFamily?: string; fontColor?: string; lineHeight?: string; + minHeight?: string; center?: boolean; - fontWeight?: number; + fontWeight?: number | string; + onClick?: () => void; } -const PlainText: React.StatelessComponent<TextProps> = ({ children, className, Tag }) => ( - <Tag className={className}>{children}</Tag> +const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick, Tag }) => ( + <Tag className={className} onClick={onClick}> + {children} + </Tag> ); export const Text = styled(PlainText)` @@ -26,14 +31,32 @@ export const Text = styled(PlainText)` ${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')}; ${props => (props.center ? 'text-align: center' : '')}; color: ${props => props.fontColor}; + ${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')}; + ${props => (props.onClick ? 'cursor: pointer' : '')}; + transition: color 0.5s ease; + &:hover { + ${props => (props.onClick ? `color: ${darken(0.1, props.fontColor)}` : '')}; + } `; Text.defaultProps = { fontFamily: 'Roboto', fontWeight: 400, - fontColor: colors.white, - fontSize: '14px', + fontColor: colors.black, + fontSize: '15px', + lineHeight: '1.5em', Tag: 'div', }; Text.displayName = 'Text'; + +export const Title: React.StatelessComponent<TextProps> = props => <Text {...props} />; + +Title.defaultProps = { + Tag: 'h2', + fontSize: '20px', + fontWeight: 600, + fontColor: colors.black, +}; + +Title.displayName = 'Title'; diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index bc2ee227d..3a6d9942d 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -13,7 +13,6 @@ import { Link } from 'react-router-dom'; import firstBy = require('thenby'); import { Blockchain } from 'ts/blockchain'; -import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle'; import { Container } from 'ts/components/ui/container'; import { IconButton } from 'ts/components/ui/icon_button'; import { Identicon } from 'ts/components/ui/identicon'; @@ -21,6 +20,7 @@ import { Island } from 'ts/components/ui/island'; import { TokenIcon } from 'ts/components/ui/token_icon'; import { WalletDisconnectedItem } from 'ts/components/wallet/wallet_disconnected_item'; import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item'; +import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; import { Dispatcher } from 'ts/redux/dispatcher'; import { colors } from 'ts/style/colors'; import { zIndex } from 'ts/style/z_index'; @@ -124,7 +124,6 @@ const styles: Styles = { const ETHER_ICON_PATH = '/images/ether.png'; const ICON_DIMENSION = 28; -const TOKEN_AMOUNT_DISPLAY_PRECISION = 5; const BODY_ITEM_KEY = 'BODY'; const HEADER_ITEM_KEY = 'HEADER'; const FOOTER_ITEM_KEY = 'FOOTER'; @@ -222,7 +221,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { </div> ); } - private _onSidebarHover(event: React.FormEvent<HTMLInputElement>): void { + private _onSidebarHover(_event: React.FormEvent<HTMLInputElement>): void { this.setState({ isHoveringSidebar: true, }); @@ -314,7 +313,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); } - private _renderTokenRow(token: Token, index: number): React.ReactNode { + private _renderTokenRow(token: Token, _index: number): React.ReactNode { const tokenState = this.props.trackedTokenStateByAddress[token.address]; const tokenLink = sharedUtils.getEtherScanLinkIfExists( token.address, @@ -414,15 +413,12 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); } private _renderAllowanceToggle(config: AllowanceToggleConfig): React.ReactNode { + // TODO: Error handling return ( <AllowanceToggle - networkId={this.props.networkId} blockchain={this.props.blockchain} - dispatcher={this.props.dispatcher} token={config.token} tokenState={config.tokenState} - onErrorOccurred={_.noop} // TODO: Error handling - userAddress={this.props.userAddress} isDisabled={!config.tokenState.isLoaded} refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)} /> @@ -441,10 +437,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { </PlaceHolder> ); } else { - const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); - const precision = Math.min(TOKEN_AMOUNT_DISPLAY_PRECISION, unitAmount.decimalPlaces()); - const formattedAmount = unitAmount.toFixed(precision); - const result = `${formattedAmount} ${symbol}`; + const result = utils.getFormattedAmount(amount, decimals, symbol); return <div style={styles.amountLabel}>{result}</div>; } } diff --git a/packages/website/ts/components/wallet/wrap_ether_item.tsx b/packages/website/ts/components/wallet/wrap_ether_item.tsx index a5052735b..f65257142 100644 --- a/packages/website/ts/components/wallet/wrap_ether_item.tsx +++ b/packages/website/ts/components/wallet/wrap_ether_item.tsx @@ -145,7 +145,7 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther </div> ); } - private _onValueChange(isValid: boolean, amount?: BigNumber): void { + private _onValueChange(_isValid: boolean, amount?: BigNumber): void { this.setState({ currentInputAmount: amount, }); |