diff options
Diffstat (limited to 'packages/website/ts')
79 files changed, 1528 insertions, 236 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 6e4d03e27..3ebdd1dee 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -132,7 +132,7 @@ export class Blockchain { return provider; } - constructor(dispatcher: Dispatcher, isSalePage: boolean = false) { + constructor(dispatcher: Dispatcher) { this._dispatcher = dispatcher; const defaultGasPrice = GWEI_IN_WEI * 30; this._defaultGasPrice = new BigNumber(defaultGasPrice); @@ -577,13 +577,13 @@ export class Blockchain { trackedTokensByAddress[token.address] = token; }); if (!_.isUndefined(this._userAddressIfExists)) { - _.each(trackedTokensByAddress, (token: Token, address: string) => { + _.each(trackedTokensByAddress, (token: Token) => { trackedTokenStorage.addTrackedTokenToUser(this._userAddressIfExists, this.networkId, token); }); } } else { // Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array - _.each(trackedTokensByAddress, (trackedToken: Token, address: string) => { + _.each(trackedTokensByAddress, (_trackedToken: Token, address: string) => { if (!_.isUndefined(tokenRegistryTokensByAddress[address])) { tokenRegistryTokensByAddress[address].isTracked = true; } @@ -625,6 +625,7 @@ export class Blockchain { ); const provider = this._contractWrappers.getProvider(); const web3Wrapper = new Web3Wrapper(provider); + web3Wrapper.abiDecoder.addABI(this._contractWrappers.exchange.abi); const receipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash); return receipt; } @@ -754,7 +755,7 @@ export class Blockchain { const tokenRegistryTokens = await this._contractWrappers.tokenRegistry.getTokensAsync(); const tokenByAddress: TokenByAddress = {}; - _.each(tokenRegistryTokens, (t: ZeroExToken, i: number) => { + _.each(tokenRegistryTokens, (t: ZeroExToken) => { // HACK: For now we have a hard-coded list of iconUrls for the dummyTokens // TODO: Refactor this out and pull the iconUrl directly from the TokenRegistry const iconUrl = configs.ICON_URL_BY_SYMBOL[t.symbol]; 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, }); diff --git a/packages/website/ts/containers/about.ts b/packages/website/ts/containers/about.ts index 3dbdcd16b..3b1c99d79 100644 --- a/packages/website/ts/containers/about.ts +++ b/packages/website/ts/containers/about.ts @@ -14,7 +14,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: AboutProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: AboutProps): ConnectedState => ({ translate: state.translate, }); diff --git a/packages/website/ts/containers/connect_documentation.ts b/packages/website/ts/containers/connect_documentation.ts index 2ecd8107a..f939ef0df 100644 --- a/packages/website/ts/containers/connect_documentation.ts +++ b/packages/website/ts/containers/connect_documentation.ts @@ -89,7 +89,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/ethereum_types_documentation.ts b/packages/website/ts/containers/ethereum_types_documentation.ts index f6d614779..285438835 100644 --- a/packages/website/ts/containers/ethereum_types_documentation.ts +++ b/packages/website/ts/containers/ethereum_types_documentation.ts @@ -106,7 +106,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/faq.ts b/packages/website/ts/containers/faq.ts index b91c47889..a2b5735f6 100644 --- a/packages/website/ts/containers/faq.ts +++ b/packages/website/ts/containers/faq.ts @@ -14,7 +14,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: FAQProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: FAQProps): ConnectedState => ({ translate: state.translate, }); diff --git a/packages/website/ts/containers/generate_order_form.ts b/packages/website/ts/containers/generate_order_form.ts index 44979b104..92296dbab 100644 --- a/packages/website/ts/containers/generate_order_form.ts +++ b/packages/website/ts/containers/generate_order_form.ts @@ -30,7 +30,7 @@ interface ConnectedState { lastForceTokenStateRefetch: number; } -const mapStateToProps = (state: State, ownProps: GenerateOrderFormProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: GenerateOrderFormProps): ConnectedState => ({ blockchainErr: state.blockchainErr, blockchainIsLoaded: state.blockchainIsLoaded, orderExpiryTimestamp: state.orderExpiryTimestamp, diff --git a/packages/website/ts/containers/inputs/allowance_toggle.ts b/packages/website/ts/containers/inputs/allowance_toggle.ts new file mode 100644 index 000000000..545708f92 --- /dev/null +++ b/packages/website/ts/containers/inputs/allowance_toggle.ts @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { Blockchain } from 'ts/blockchain'; +import { State } from 'ts/redux/reducer'; +import { BalanceErrs, Token, TokenState } from 'ts/types'; + +import { AllowanceToggle as AllowanceToggleComponent } from 'ts/components/inputs/allowance_toggle'; +import { Dispatcher } from 'ts/redux/dispatcher'; + +interface AllowanceToggleProps { + blockchain: Blockchain; + onErrorOccurred?: (errType: BalanceErrs) => void; + token: Token; + tokenState: TokenState; + isDisabled?: boolean; + refetchTokenStateAsync: () => Promise<void>; +} + +interface ConnectedState { + networkId: number; + userAddress: string; +} + +interface ConnectedDispatch { + dispatcher: Dispatcher; +} + +const mapStateToProps = (state: State, _ownProps: AllowanceToggleProps): ConnectedState => ({ + networkId: state.networkId, + userAddress: state.userAddress, +}); + +const mapDispatchTopProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({ + dispatcher: new Dispatcher(dispatch), +}); + +export const AllowanceToggle: React.ComponentClass<AllowanceToggleProps> = connect( + mapStateToProps, + mapDispatchTopProps, +)(AllowanceToggleComponent); diff --git a/packages/website/ts/containers/jobs.ts b/packages/website/ts/containers/jobs.ts new file mode 100644 index 000000000..b18485882 --- /dev/null +++ b/packages/website/ts/containers/jobs.ts @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { Jobs as JobsComponent, JobsProps } from 'ts/pages/jobs/jobs'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { State } from 'ts/redux/reducer'; +import { ScreenWidths } from 'ts/types'; +import { Translate } from 'ts/utils/translate'; + +interface ConnectedState { + translate: Translate; + screenWidth: ScreenWidths; +} + +interface ConnectedDispatch { + dispatcher: Dispatcher; +} + +const mapStateToProps = (state: State, _ownProps: JobsProps): ConnectedState => ({ + translate: state.translate, + screenWidth: state.screenWidth, +}); + +const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({ + dispatcher: new Dispatcher(dispatch), +}); + +export const Jobs: React.ComponentClass<JobsProps> = connect(mapStateToProps, mapDispatchToProps)(JobsComponent); diff --git a/packages/website/ts/containers/json_schemas_documentation.ts b/packages/website/ts/containers/json_schemas_documentation.ts index 4df7ddecb..67740d4c6 100644 --- a/packages/website/ts/containers/json_schemas_documentation.ts +++ b/packages/website/ts/containers/json_schemas_documentation.ts @@ -70,7 +70,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/landing.ts b/packages/website/ts/containers/landing.ts index a9fc1d9d1..972ed4c23 100644 --- a/packages/website/ts/containers/landing.ts +++ b/packages/website/ts/containers/landing.ts @@ -14,7 +14,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: LandingProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: LandingProps): ConnectedState => ({ translate: state.translate, }); diff --git a/packages/website/ts/containers/legacy_portal.ts b/packages/website/ts/containers/legacy_portal.ts index eae450c21..e99f47fb7 100644 --- a/packages/website/ts/containers/legacy_portal.ts +++ b/packages/website/ts/containers/legacy_portal.ts @@ -37,7 +37,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: LegacyPortalComponentProps): ConnectedState => { +const mapStateToProps = (state: State, _ownProps: LegacyPortalComponentProps): ConnectedState => { const receiveAssetToken = state.sideToAssetToken[Side.Receive]; const depositAssetToken = state.sideToAssetToken[Side.Deposit]; const receiveAddress = !_.isUndefined(receiveAssetToken.address) diff --git a/packages/website/ts/containers/not_found.ts b/packages/website/ts/containers/not_found.ts index 4fdc325ea..f384dab89 100644 --- a/packages/website/ts/containers/not_found.ts +++ b/packages/website/ts/containers/not_found.ts @@ -14,7 +14,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: NotFoundProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: NotFoundProps): ConnectedState => ({ translate: state.translate, }); diff --git a/packages/website/ts/containers/order_utils_documentation.ts b/packages/website/ts/containers/order_utils_documentation.ts index cdf97e5c8..37b7f2273 100644 --- a/packages/website/ts/containers/order_utils_documentation.ts +++ b/packages/website/ts/containers/order_utils_documentation.ts @@ -81,7 +81,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/portal.ts b/packages/website/ts/containers/portal.ts index b8c8fb999..5876e65f5 100644 --- a/packages/website/ts/containers/portal.ts +++ b/packages/website/ts/containers/portal.ts @@ -34,7 +34,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: PortalComponentProps): ConnectedState => { +const mapStateToProps = (state: State, _ownProps: PortalComponentProps): ConnectedState => { const receiveAssetToken = state.sideToAssetToken[Side.Receive]; const depositAssetToken = state.sideToAssetToken[Side.Deposit]; const receiveAddress = !_.isUndefined(receiveAssetToken.address) diff --git a/packages/website/ts/containers/portal_onboarding_flow.ts b/packages/website/ts/containers/portal_onboarding_flow.ts index 0ad9aef13..746adf0ba 100644 --- a/packages/website/ts/containers/portal_onboarding_flow.ts +++ b/packages/website/ts/containers/portal_onboarding_flow.ts @@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils'; import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; +import { Blockchain } from 'ts/blockchain'; import { ActionTypes, ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { PortalOnboardingFlow as PortalOnboardingFlowComponent } from 'ts/components/onboarding/portal_onboarding_flow'; @@ -9,6 +10,8 @@ import { State } from 'ts/redux/reducer'; interface PortalOnboardingFlowProps { trackedTokenStateByAddress: TokenStateByAddress; + blockchain: Blockchain; + refetchTokenStateAsync: (tokenAddress: string) => Promise<void>; } interface ConnectedState { @@ -28,7 +31,7 @@ interface ConnectedDispatch { updateOnboardingStep: (stepIndex: number) => void; } -const mapStateToProps = (state: State, ownProps: PortalOnboardingFlowProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: PortalOnboardingFlowProps): ConnectedState => ({ stepIndex: state.portalOnboardingStep, isRunning: state.isPortalOnboardingShowing, userAddress: state.userAddress, diff --git a/packages/website/ts/containers/smart_contracts_documentation.ts b/packages/website/ts/containers/smart_contracts_documentation.ts index 27328909c..c88c3b365 100644 --- a/packages/website/ts/containers/smart_contracts_documentation.ts +++ b/packages/website/ts/containers/smart_contracts_documentation.ts @@ -75,7 +75,7 @@ interface ConnectedDispatch { docsInfo: DocsInfo; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/sol_compiler_documentation.ts b/packages/website/ts/containers/sol_compiler_documentation.ts index 4ccc1850f..8720e2c1d 100644 --- a/packages/website/ts/containers/sol_compiler_documentation.ts +++ b/packages/website/ts/containers/sol_compiler_documentation.ts @@ -67,7 +67,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/sol_cov_documentation.ts b/packages/website/ts/containers/sol_cov_documentation.ts index 73cc99a8c..a8009071f 100644 --- a/packages/website/ts/containers/sol_cov_documentation.ts +++ b/packages/website/ts/containers/sol_cov_documentation.ts @@ -96,7 +96,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/subproviders_documentation.ts b/packages/website/ts/containers/subproviders_documentation.ts index 0c7e73f38..6d4230e53 100644 --- a/packages/website/ts/containers/subproviders_documentation.ts +++ b/packages/website/ts/containers/subproviders_documentation.ts @@ -128,7 +128,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/web3_wrapper_documentation.ts b/packages/website/ts/containers/web3_wrapper_documentation.ts index 34633f14d..b04a83ac4 100644 --- a/packages/website/ts/containers/web3_wrapper_documentation.ts +++ b/packages/website/ts/containers/web3_wrapper_documentation.ts @@ -105,7 +105,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, translate: state.translate, diff --git a/packages/website/ts/containers/wiki.ts b/packages/website/ts/containers/wiki.ts index af7228dbe..357f8bbf4 100644 --- a/packages/website/ts/containers/wiki.ts +++ b/packages/website/ts/containers/wiki.ts @@ -14,7 +14,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: WikiProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: WikiProps): ConnectedState => ({ translate: state.translate, }); diff --git a/packages/website/ts/containers/zero_ex_js_documentation.ts b/packages/website/ts/containers/zero_ex_js_documentation.ts index 9c3f447fb..bd0d1732a 100644 --- a/packages/website/ts/containers/zero_ex_js_documentation.ts +++ b/packages/website/ts/containers/zero_ex_js_documentation.ts @@ -173,6 +173,7 @@ const docsInfoConfig: DocsInfoConfig = { 'OrderStateInvalid', 'OrderState', 'OrderStateWatcherConfig', + 'OrderWatcherConfig', 'FilterObject', 'OrderRelevantState', 'JSONRPCRequestPayload', @@ -209,7 +210,7 @@ interface ConnectedDispatch { dispatcher: Dispatcher; } -const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ +const mapStateToProps = (state: State, _ownProps: DocPageProps): ConnectedState => ({ docsVersion: state.docsVersion, availableDocVersions: state.availableDocVersions, docsInfo, diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index 719604c02..249b4fdc9 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -4,9 +4,10 @@ import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom'; import * as injectTapEventPlugin from 'react-tap-event-plugin'; -import { Redirecter } from 'ts/components/redirecter'; +import { Redirector } from 'ts/components/redirector'; import { About } from 'ts/containers/about'; import { FAQ } from 'ts/containers/faq'; +import { Jobs } from 'ts/containers/jobs'; import { Landing } from 'ts/containers/landing'; import { NotFound } from 'ts/containers/not_found'; import { Wiki } from 'ts/containers/wiki'; @@ -86,8 +87,12 @@ render( <Switch> <Route exact={true} path="/" component={Landing as any} /> <Redirect from="/otc" to={`${WebsitePaths.Portal}`} /> - - <Route path={WebsitePaths.Jobs} component={Redirecter as any} /> + {/* TODO: Remove this once we ship the jobs page*/} + {utils.shouldShowJobsPage() ? ( + <Route path={WebsitePaths.Jobs} component={Jobs as any} /> + ) : ( + <Route path={WebsitePaths.Jobs} component={Redirector as any} /> + )} <Route path={WebsitePaths.Portal} component={LazyPortal} /> <Route path={WebsitePaths.FAQ} component={FAQ as any} /> <Route path={WebsitePaths.About} component={About as any} /> diff --git a/packages/website/ts/local_storage/trade_history_storage.tsx b/packages/website/ts/local_storage/trade_history_storage.tsx index cc764d98e..2e2f4e64e 100644 --- a/packages/website/ts/local_storage/trade_history_storage.tsx +++ b/packages/website/ts/local_storage/trade_history_storage.tsx @@ -57,7 +57,7 @@ export const tradeHistoryStorage = { return {}; } const userFillsByHash = JSON.parse(userFillsJSONString); - _.each(userFillsByHash, (fill, hash) => { + _.each(userFillsByHash, fill => { fill.paidMakerFee = new BigNumber(fill.paidMakerFee); fill.paidTakerFee = new BigNumber(fill.paidTakerFee); fill.filledTakerTokenAmount = new BigNumber(fill.filledTakerTokenAmount); diff --git a/packages/website/ts/pages/jobs/benefits.tsx b/packages/website/ts/pages/jobs/benefits.tsx new file mode 100644 index 000000000..006facc83 --- /dev/null +++ b/packages/website/ts/pages/jobs/benefits.tsx @@ -0,0 +1,109 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { FilledImage } from 'ts/components/ui/filled_image'; +import { HeaderItem } from 'ts/pages/jobs/list/header_item'; +import { ListItem } from 'ts/pages/jobs/list/list_item'; +import { colors } from 'ts/style/colors'; +import { ScreenWidths } from 'ts/types'; + +const IMAGE_PATHS = ['/images/jobs/location1.png', '/images/jobs/location2.png', '/images/jobs/location3.png']; +const BENEFIT_ITEM_PROPS_LIST: BenefitItemProps[] = [ + { + bulletColor: '#6FCF97', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, + { + bulletColor: '#56CCF2', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, + { + bulletColor: '#EB5757', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, + { + bulletColor: '#6FCF97', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, + { + bulletColor: '#56CCF2', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, +]; +const LARGE_LAYOUT_HEIGHT = 937; +const LARGE_LAYOUT_BENEFITS_LIST_PADDING_LEFT = 205; +const HEADER_TEXT = 'Benefits'; +const BENEFIT_ITEM_MIN_HEIGHT = 150; + +export interface BenefitsProps { + screenWidth: ScreenWidths; +} + +export const Benefits = (props: BenefitsProps) => ( + <div style={{ backgroundColor: colors.jobsPageBackground }}> + {props.screenWidth === ScreenWidths.Sm ? <SmallLayout /> : <LargeLayout />} + </div> +); + +const LargeLayout = () => ( + <div className="flex" style={{ height: LARGE_LAYOUT_HEIGHT }}> + <div style={{ width: '43%', height: '100%' }}> + <ImageGrid /> + </div> + <div + className="pr4" + style={{ paddingLeft: LARGE_LAYOUT_BENEFITS_LIST_PADDING_LEFT, width: '57%', height: '100%' }} + > + <BenefitsList /> + </div> + </div> +); + +const SmallLayout = () => ( + <div> + <FilledImage src={_.head(IMAGE_PATHS)} /> + <BenefitsList /> + </div> +); + +export const BenefitsList = () => { + return ( + <div> + <HeaderItem headerText={HEADER_TEXT} /> + {_.map(BENEFIT_ITEM_PROPS_LIST, valueItemProps => <BenefitItem {...valueItemProps} />)} + </div> + ); +}; +interface BenefitItemProps { + bulletColor: string; + description: string; +} + +const BenefitItem: React.StatelessComponent<BenefitItemProps> = ({ bulletColor, description }) => ( + <div style={{ minHeight: BENEFIT_ITEM_MIN_HEIGHT }}> + <ListItem bulletColor={bulletColor}> + <div style={{ fontSize: 16, lineHeight: 1.5 }}>{description}</div> + </ListItem> + </div> +); + +const ImageGrid = () => ( + <div style={{ width: '100%', height: '100%' }}> + <div className="flex" style={{ height: '67%' }}> + <FilledImage src={IMAGE_PATHS[0]} /> + </div> + <div className="clearfix" style={{ height: '33%' }}> + <div className="col lg-col-6 md-col-6 col-12" style={{ height: '100%' }}> + <FilledImage src={IMAGE_PATHS[1]} /> + </div> + <div className="col lg-col-6 md-col-6 col-12" style={{ height: '100%' }}> + <FilledImage src={IMAGE_PATHS[2]} /> + </div> + </div> + </div> +); diff --git a/packages/website/ts/pages/jobs/jobs.tsx b/packages/website/ts/pages/jobs/jobs.tsx new file mode 100644 index 000000000..314669ee9 --- /dev/null +++ b/packages/website/ts/pages/jobs/jobs.tsx @@ -0,0 +1,81 @@ +import { colors, utils as sharedUtils } from '@0xproject/react-shared'; +import * as _ from 'lodash'; +import * as React from 'react'; +import * as DocumentTitle from 'react-document-title'; + +import { Footer } from 'ts/components/footer'; +import { TopBar } from 'ts/components/top_bar/top_bar'; +import { FilledImage } from 'ts/components/ui/filled_image'; +import { Benefits } from 'ts/pages/jobs/benefits'; +import { Join0x } from 'ts/pages/jobs/join_0x'; +import { Mission } from 'ts/pages/jobs/mission'; +import { OpenPositions } from 'ts/pages/jobs/open_positions'; +import { PhotoRail } from 'ts/pages/jobs/photo_rail'; +import { Teams } from 'ts/pages/jobs/teams'; +import { Values } from 'ts/pages/jobs/values'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { ScreenWidths } from 'ts/types'; +import { Translate } from 'ts/utils/translate'; +import { utils } from 'ts/utils/utils'; + +const OPEN_POSITIONS_HASH = 'positions'; +const THROTTLE_TIMEOUT = 100; +const PHOTO_RAIL_IMAGES = ['/images/jobs/office1.png', '/images/jobs/office2.png', '/images/jobs/office3.png']; + +export interface JobsProps { + location: Location; + translate: Translate; + dispatcher: Dispatcher; + screenWidth: ScreenWidths; +} + +export interface JobsState {} + +export class Jobs extends React.Component<JobsProps, JobsState> { + // TODO: consolidate this small screen scaffolding into one place (its being used in portal and docs as well) + private _throttledScreenWidthUpdate: () => void; + public constructor(props: JobsProps) { + super(props); + this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); + } + public componentDidMount(): void { + window.addEventListener('resize', this._throttledScreenWidthUpdate); + window.scrollTo(0, 0); + } + public render(): React.ReactNode { + return ( + <div> + <DocumentTitle title="Jobs" /> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + style={{ backgroundColor: colors.white, position: 'relative' }} + translate={this.props.translate} + /> + <Join0x onCallToActionClick={this._onJoin0xCallToActionClick.bind(this)} /> + <Mission screenWidth={this.props.screenWidth} /> + {this._isSmallScreen() ? ( + <FilledImage src={_.head(PHOTO_RAIL_IMAGES)} /> + ) : ( + <PhotoRail images={PHOTO_RAIL_IMAGES} /> + )} + <Values /> + <Benefits screenWidth={this.props.screenWidth} /> + <Teams screenWidth={this.props.screenWidth} /> + <OpenPositions hash={OPEN_POSITIONS_HASH} screenWidth={this.props.screenWidth} /> + <Footer translate={this.props.translate} dispatcher={this.props.dispatcher} /> + </div> + ); + } + private _onJoin0xCallToActionClick(): void { + sharedUtils.setUrlHash(OPEN_POSITIONS_HASH); + } + private _updateScreenWidth(): void { + const newScreenWidth = utils.getScreenWidth(); + this.props.dispatcher.updateScreenWidth(newScreenWidth); + } + private _isSmallScreen(): boolean { + const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; + return isSmallScreen; + } +} diff --git a/packages/website/ts/pages/jobs/join_0x.tsx b/packages/website/ts/pages/jobs/join_0x.tsx new file mode 100644 index 000000000..72cce3b89 --- /dev/null +++ b/packages/website/ts/pages/jobs/join_0x.tsx @@ -0,0 +1,41 @@ +import { colors } from '@0xproject/react-shared'; + +import * as React from 'react'; + +import { Button } from 'ts/components/ui/button'; + +const BUTTON_TEXT = 'view open positions'; + +export interface Join0xProps { + onCallToActionClick: () => void; +} + +export const Join0x = (props: Join0xProps) => ( + <div className="clearfix center lg-py4 md-py4" style={{ backgroundColor: colors.white, color: colors.black }}> + <div className="mx-auto inline-block align-middle py4" style={{ lineHeight: '44px', textAlign: 'center' }}> + <div className="h2 sm-center sm-pt3" style={{ fontFamily: 'Roboto Mono' }}> + Join 0x + </div> + <div + className="pb2 lg-pt2 md-pt2 sm-pt3 sm-px3 h4 sm-center" + style={{ fontFamily: 'Roboto', lineHeight: 2, maxWidth: 537 }} + > + 0x is transforming the way that value is exchanged on a global scale. Come join us in San Francisco or + work remotely anywhere in the world to help create the infrastructure of a new tokenized economy. + </div> + <div className="py3"> + <Button + type="button" + backgroundColor={colors.black} + width="290px" + fontColor={colors.white} + fontSize="18px" + fontFamily="Roboto Mono" + onClick={props.onCallToActionClick} + > + {BUTTON_TEXT} + </Button> + </div> + </div> + </div> +); diff --git a/packages/website/ts/pages/jobs/list/header_item.tsx b/packages/website/ts/pages/jobs/list/header_item.tsx new file mode 100644 index 000000000..ac214904c --- /dev/null +++ b/packages/website/ts/pages/jobs/list/header_item.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; + +import { Text } from 'ts/components/ui/text'; +import { ListItem } from 'ts/pages/jobs/list/list_item'; +import { colors } from 'ts/style/colors'; + +export interface HeaderItemProps { + headerText?: string; +} +export const HeaderItem: React.StatelessComponent<HeaderItemProps> = ({ headerText }) => { + return ( + <div className="h2 lg-py4 md-py4 sm-py3"> + <ListItem> + <Text + fontFamily="Roboto Mono" + fontSize="24px" + lineHeight="1.25" + minHeight="1.25em" + fontColor={colors.black} + > + {headerText} + </Text> + </ListItem> + </div> + ); +}; diff --git a/packages/website/ts/pages/jobs/list/list_item.tsx b/packages/website/ts/pages/jobs/list/list_item.tsx new file mode 100644 index 000000000..d7838bc01 --- /dev/null +++ b/packages/website/ts/pages/jobs/list/list_item.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export interface ListItemProps { + bulletColor?: string; +} +export const ListItem: React.StatelessComponent<ListItemProps> = ({ bulletColor, children }) => { + return ( + <div className="flex items-center"> + <svg className="flex-none lg-px2 md-px2 sm-pl2" height="26" width="26"> + <circle cx="13" cy="13" r="13" fill={bulletColor || 'transparent'} /> + </svg> + <div className="flex-auto px2">{children}</div> + </div> + ); +}; diff --git a/packages/website/ts/pages/jobs/mission.tsx b/packages/website/ts/pages/jobs/mission.tsx new file mode 100644 index 000000000..f7f874e04 --- /dev/null +++ b/packages/website/ts/pages/jobs/mission.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; + +import { colors } from 'ts/style/colors'; +import { ScreenWidths } from 'ts/types'; + +export interface MissionProps { + screenWidth: ScreenWidths; +} +export const Mission = (props: MissionProps) => { + const isSmallScreen = props.screenWidth === ScreenWidths.Sm; + const image = ( + <div className="col lg-col-6 md-col-6 col-12 sm-py2 px2 center"> + <img src="/images/jobs/map.png" style={{ width: '100%' }} /> + </div> + ); + const missionStatementStyle = !isSmallScreen ? { height: 364, lineHeight: '364px' } : undefined; + const missionStatement = ( + <div className="col lg-col-6 md-col-6 col-12 center" style={missionStatementStyle}> + <div + className="mx-auto inline-block align-middle" + style={{ maxWidth: 385, lineHeight: '44px', textAlign: 'center' }} + > + <div className="h2 sm-center sm-pt3" style={{ fontFamily: 'Roboto Mono' }}> + Our Mission + </div> + <div + className="pb2 lg-pt2 md-pt2 sm-pt3 sm-px3 h4 sm-center" + style={{ fontFamily: 'Roboto', lineHeight: 2, maxWidth: 537 }} + > + We believe a system can exist in which all world value is accessible to anyone, anywhere, regardless + of where you happen to be born. + </div> + </div> + </div> + ); + return ( + <div + className="container lg-py4 md-py4" + style={{ backgroundColor: colors.jobsPageBackground, color: colors.black }} + > + <div className="mx-auto clearfix sm-py4"> + {isSmallScreen ? ( + <div> + {missionStatement} + {image} + </div> + ) : ( + <div> + {image} + {missionStatement} + </div> + )} + </div> + </div> + ); +}; diff --git a/packages/website/ts/pages/jobs/open_positions.tsx b/packages/website/ts/pages/jobs/open_positions.tsx new file mode 100644 index 000000000..f3d980315 --- /dev/null +++ b/packages/website/ts/pages/jobs/open_positions.tsx @@ -0,0 +1,192 @@ +import * as _ from 'lodash'; +import CircularProgress from 'material-ui/CircularProgress'; +import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; +import * as React from 'react'; + +import { Retry } from 'ts/components/ui/retry'; +import { Text } from 'ts/components/ui/text'; +import { HeaderItem } from 'ts/pages/jobs/list/header_item'; +import { ListItem } from 'ts/pages/jobs/list/list_item'; +import { colors } from 'ts/style/colors'; +import { styled } from 'ts/style/theme'; +import { ScreenWidths, WebsiteBackendJobInfo } from 'ts/types'; +import { backendClient } from 'ts/utils/backend_client'; + +const labelStyle = { fontFamily: 'Roboto Mono', fontSize: 18 }; +const HEADER_TEXT = 'Open Positions'; +const TABLE_ROW_MIN_HEIGHT = 100; + +export interface OpenPositionsProps { + hash: string; + screenWidth: ScreenWidths; +} +export interface OpenPositionsState { + jobInfos?: WebsiteBackendJobInfo[]; + error?: Error; +} + +export class OpenPositions extends React.Component<OpenPositionsProps, OpenPositionsState> { + private _isUnmounted: boolean; + constructor(props: OpenPositionsProps) { + super(props); + this._isUnmounted = false; + this.state = { + jobInfos: undefined, + error: undefined, + }; + } + public componentWillMount(): void { + // tslint:disable-next-line:no-floating-promises + this._fetchJobInfosAsync(); + } + public componentWillUnmount(): void { + this._isUnmounted = true; + } + public render(): React.ReactNode { + const isReadyToRender = _.isUndefined(this.state.error) && !_.isUndefined(this.state.jobInfos); + return ( + <div id={this.props.hash} className="mx-auto max-width-4"> + {isReadyToRender ? this._renderBody() : this._renderLoading()} + </div> + ); + } + private _renderBody(): React.ReactNode { + const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; + return isSmallScreen ? this._renderList() : this._renderTable(); + } + private _renderLoading(): React.ReactNode { + return ( + // TODO: consolidate this loading component with the one in portal and RelayerIndex + // 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} /> + ) : ( + <Retry onRetry={this._fetchJobInfosAsync.bind(this)} /> + )} + </div> + ); + } + private _renderList(): React.ReactNode { + return ( + <div style={{ backgroundColor: colors.jobsPageBackground }}> + <HeaderItem headerText={HEADER_TEXT} /> + {_.map(this.state.jobInfos, jobInfo => ( + <JobInfoListItem + key={jobInfo.id} + title={jobInfo.title} + description={jobInfo.department} + onClick={this._openJobInfoUrl.bind(this, jobInfo)} + /> + ))} + </div> + ); + } + private _renderTable(): React.ReactNode { + return ( + <div> + <HeaderItem headerText={HEADER_TEXT} /> + <Table selectable={false} onCellClick={this._onCellClick.bind(this)}> + <TableHeader displaySelectAll={false} adjustForCheckbox={false}> + <TableRow> + <TableHeaderColumn colSpan={5} style={labelStyle}> + Position + </TableHeaderColumn> + <TableHeaderColumn colSpan={3} style={labelStyle}> + Department + </TableHeaderColumn> + <TableHeaderColumn colSpan={4} style={labelStyle}> + Office + </TableHeaderColumn> + </TableRow> + </TableHeader> + <TableBody displayRowCheckbox={false} showRowHover={true}> + {_.map(this.state.jobInfos, jobInfo => { + return this._renderJobInfoTableRow(jobInfo); + })} + </TableBody> + </Table> + </div> + ); + } + private _renderJobInfoTableRow(jobInfo: WebsiteBackendJobInfo): React.ReactNode { + return ( + <TableRow + key={jobInfo.id} + hoverable={true} + displayBorder={false} + style={{ height: TABLE_ROW_MIN_HEIGHT, border: 2 }} + > + <TableRowColumn colSpan={5} style={labelStyle}> + {jobInfo.title} + </TableRowColumn> + <TableRowColumn colSpan={3} style={labelStyle}> + {jobInfo.department} + </TableRowColumn> + <TableRowColumn colSpan={4} style={labelStyle}> + {jobInfo.office} + </TableRowColumn> + </TableRow> + ); + } + private async _fetchJobInfosAsync(): Promise<void> { + try { + if (!this._isUnmounted) { + this.setState({ + jobInfos: undefined, + error: undefined, + }); + } + const jobInfos = await backendClient.getJobInfosAsync(); + if (!this._isUnmounted) { + this.setState({ + jobInfos, + }); + } + } catch (error) { + if (!this._isUnmounted) { + this.setState({ + error, + }); + } + } + } + private _onCellClick(rowNumber: number): void { + if (_.isUndefined(this.state.jobInfos)) { + return; + } + const jobInfo = this.state.jobInfos[rowNumber]; + this._openJobInfoUrl(jobInfo); + } + + private _openJobInfoUrl(jobInfo: WebsiteBackendJobInfo): void { + const url = jobInfo.url; + window.open(url, '_blank'); + } +} + +export interface JobInfoListItemProps { + title?: string; + description?: string; + onClick?: (event: React.MouseEvent<HTMLElement>) => void; +} + +const PlainJobInfoListItem: React.StatelessComponent<JobInfoListItemProps> = ({ title, description, onClick }) => ( + <div className="mb3" onClick={onClick}> + <ListItem> + <Text fontWeight="bold" fontSize="16px" fontColor={colors.mediumBlue}> + {title + ' ›'} + </Text> + <Text className="pt1" fontSize="16px" fontColor={colors.darkGrey}> + {description} + </Text> + </ListItem> + </div> +); + +export const JobInfoListItem = styled(PlainJobInfoListItem)` + cursor: pointer; + &:hover { + opacity: 0.5; + } +`; diff --git a/packages/website/ts/pages/jobs/photo_rail.tsx b/packages/website/ts/pages/jobs/photo_rail.tsx new file mode 100644 index 000000000..acc9dcb91 --- /dev/null +++ b/packages/website/ts/pages/jobs/photo_rail.tsx @@ -0,0 +1,22 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { FilledImage } from 'ts/components/ui/filled_image'; + +export interface PhotoRailProps { + images: string[]; +} + +export const PhotoRail = (props: PhotoRailProps) => { + return ( + <div className="clearfix" style={{ height: 490 }}> + {_.map(props.images, (image: string) => { + return ( + <div key={image} className="col lg-col-4 md-col-4 col-12 center" style={{ height: '100%' }}> + <FilledImage src={image} /> + </div> + ); + })} + </div> + ); +}; diff --git a/packages/website/ts/pages/jobs/teams.tsx b/packages/website/ts/pages/jobs/teams.tsx new file mode 100644 index 000000000..80b036396 --- /dev/null +++ b/packages/website/ts/pages/jobs/teams.tsx @@ -0,0 +1,90 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Text } from 'ts/components/ui/text'; +import { HeaderItem } from 'ts/pages/jobs/list/header_item'; +import { ListItem } from 'ts/pages/jobs/list/list_item'; +import { colors } from 'ts/style/colors'; +import { ScreenWidths } from 'ts/types'; + +const TEAM_ITEM_PROPS_COLUMN1: TeamItemProps[] = [ + { + bulletColor: '#EB5757', + title: 'User Growth', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, + { + bulletColor: '#EB5757', + title: 'Governance', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, +]; +const TEAM_ITEM_PROPS_COLUMN2: TeamItemProps[] = [ + { + bulletColor: '#EB5757', + title: 'Developer Tools', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, + { + bulletColor: '#EB5757', + title: 'Marketing', + description: + 'Donec eget auctor mauris, a imperdiet ante. Ut a tellus ullamcorper, pharetra nibh sed, dignissim mauris. Quisque vel magna vitae nisi scelerisque commodo sed eget dolor. Maecenas vehicula orci', + }, +]; +const HEADER_TEXT = 'Our Teams'; +const MINIMUM_ITEM_HEIGHT = 240; + +export interface TeamsProps { + screenWidth: ScreenWidths; +} + +export const Teams = (props: TeamsProps) => (props.screenWidth === ScreenWidths.Sm ? <SmallLayout /> : <LargeLayout />); + +const LargeLayout = () => ( + <div className="mx-auto max-width-4 clearfix pb4"> + <div className="col lg-col-6 md-col-6 col-12"> + <HeaderItem headerText={HEADER_TEXT} /> + {_.map(TEAM_ITEM_PROPS_COLUMN1, teamItemProps => <TeamItem {...teamItemProps} />)} + </div> + <div className="col lg-col-6 md-col-6 col-12"> + <HeaderItem headerText=" " /> + {_.map(TEAM_ITEM_PROPS_COLUMN2, teamItemProps => <TeamItem {...teamItemProps} />)} + </div> + </div> +); + +const SmallLayout = () => ( + <div> + <HeaderItem headerText={HEADER_TEXT} /> + {_.map(_.concat(TEAM_ITEM_PROPS_COLUMN1, TEAM_ITEM_PROPS_COLUMN2), teamItemProps => ( + <TeamItem {...teamItemProps} /> + ))} + </div> +); + +interface TeamItemProps { + bulletColor: string; + title: string; + description: string; +} + +export const TeamItem: React.StatelessComponent<TeamItemProps> = ({ bulletColor, title, description }) => { + return ( + <div style={{ minHeight: MINIMUM_ITEM_HEIGHT }}> + <ListItem bulletColor={bulletColor}> + <Text fontWeight="bold" fontSize="16px" fontColor={colors.black}> + {title} + </Text> + </ListItem> + <ListItem> + <Text className="pt1" fontSize="16px" lineHeight="2em" fontColor={colors.black}> + {description} + </Text> + </ListItem> + </div> + ); +}; diff --git a/packages/website/ts/pages/jobs/values.tsx b/packages/website/ts/pages/jobs/values.tsx new file mode 100644 index 000000000..c7c4d5726 --- /dev/null +++ b/packages/website/ts/pages/jobs/values.tsx @@ -0,0 +1,60 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Text } from 'ts/components/ui/text'; +import { HeaderItem } from 'ts/pages/jobs/list/header_item'; +import { ListItem } from 'ts/pages/jobs/list/list_item'; +import { colors } from 'ts/style/colors'; + +const VALUE_ITEM_PROPS_LIST: ValueItemProps[] = [ + { + bulletColor: '#6FCF97', + title: 'Ethics/Doing the right thing', + description: 'orem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + { + bulletColor: '#56CCF2', + title: 'Consistently ship', + description: 'orem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + { + bulletColor: '#EB5757', + title: 'Focus on long term impact', + description: 'orem ipsum dolor sit amet, consectetur adipiscing elit.', + }, +]; + +const HEADER_TEXT = 'Our Values'; +const VALUE_ITEM_MIN_HEIGHT = 150; + +export const Values = () => { + return ( + <div className="mx-auto max-width-4"> + <HeaderItem headerText={HEADER_TEXT} /> + {_.map(VALUE_ITEM_PROPS_LIST, valueItemProps => <ValueItem {...valueItemProps} />)} + </div> + ); +}; + +interface ValueItemProps { + bulletColor: string; + title: string; + description: string; +} + +export const ValueItem: React.StatelessComponent<ValueItemProps> = ({ bulletColor, title, description }) => { + return ( + <div style={{ minHeight: VALUE_ITEM_MIN_HEIGHT }}> + <ListItem bulletColor={bulletColor}> + <Text fontWeight="bold" fontSize="16x" fontColor={colors.black}> + {title} + </Text> + </ListItem> + <ListItem> + <Text className="pt1" fontSize="16x" lineHeight="2em" fontColor={colors.black}> + {description} + </Text> + </ListItem> + </div> + ); +}; diff --git a/packages/website/ts/pages/not_found.tsx b/packages/website/ts/pages/not_found.tsx index a94ba5863..2fe3b1f45 100644 --- a/packages/website/ts/pages/not_found.tsx +++ b/packages/website/ts/pages/not_found.tsx @@ -11,7 +11,7 @@ export interface NotFoundProps { dispatcher: Dispatcher; } -export const NotFound = (props: NotFoundProps) => { +export const NotFound = (_props: NotFoundProps) => { return ( <div> <TopBar blockchainIsLoaded={false} location={this.props.location} translate={this.props.translate} /> diff --git a/packages/website/ts/pages/wiki/wiki.tsx b/packages/website/ts/pages/wiki/wiki.tsx index bdefe0fda..9659900be 100644 --- a/packages/website/ts/pages/wiki/wiki.tsx +++ b/packages/website/ts/pages/wiki/wiki.tsx @@ -233,7 +233,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> { } return menuSubsectionsBySection; } - private _onSidebarHover(event: React.FormEvent<HTMLInputElement>): void { + private _onSidebarHover(_event: React.FormEvent<HTMLInputElement>): void { this.setState({ isHoveringSidebar: true, }); @@ -243,7 +243,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> { isHoveringSidebar: false, }); } - private _onHashChanged(event: any): void { + private _onHashChanged(_event: any): void { const hash = window.location.hash.slice(1); sharedUtils.scrollToHash(hash, sharedConstants.SCROLL_CONTAINER_ID); } diff --git a/packages/website/ts/style/colors.ts b/packages/website/ts/style/colors.ts index 5ffdd6ba7..b15000d7a 100644 --- a/packages/website/ts/style/colors.ts +++ b/packages/website/ts/style/colors.ts @@ -11,6 +11,8 @@ const appColors = { wrapEtherConfirmationButton: sharedColors.mediumBlue, drawerMenuBackground: '#4a4a4a', menuItemDefaultSelectedBackground: '#424242', + jobsPageBackground: sharedColors.grey50, + jobsPageOpenPositionRow: sharedColors.grey100, }; export const colors = { diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index 15444e517..d00154652 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -519,6 +519,8 @@ export interface WebsiteBackendRelayerInfo { url: string; appUrl?: string; headerImgUrl?: string; + logoImgUrl?: string; + primaryColor?: string; topTokens: WebsiteBackendTokenInfo[]; } @@ -536,4 +538,12 @@ export interface WebsiteBackendTokenInfo { export interface WebsiteBackendGasInfo { average: number; } + +export interface WebsiteBackendJobInfo { + id: number; + title: string; + department: string; + office: string; + url: string; +} // tslint:disable:max-file-line-count diff --git a/packages/website/ts/utils/backend_client.ts b/packages/website/ts/utils/backend_client.ts index 6b16aea6b..835a6ef4d 100644 --- a/packages/website/ts/utils/backend_client.ts +++ b/packages/website/ts/utils/backend_client.ts @@ -1,10 +1,17 @@ import * as _ from 'lodash'; -import { ArticlesBySection, WebsiteBackendGasInfo, WebsiteBackendPriceInfo, WebsiteBackendRelayerInfo } from 'ts/types'; +import { + ArticlesBySection, + WebsiteBackendGasInfo, + WebsiteBackendJobInfo, + WebsiteBackendPriceInfo, + WebsiteBackendRelayerInfo, +} from 'ts/types'; import { fetchUtils } from 'ts/utils/fetch_utils'; import { utils } from 'ts/utils/utils'; const ETH_GAS_STATION_ENDPOINT = '/eth_gas_station'; +const JOBS_ENDPOINT = '/jobs'; const PRICES_ENDPOINT = '/prices'; const RELAYERS_ENDPOINT = '/relayers'; const WIKI_ENDPOINT = '/wiki'; @@ -15,6 +22,10 @@ export const backendClient = { const result = await fetchUtils.requestAsync(utils.getBackendBaseUrl(), ETH_GAS_STATION_ENDPOINT); return result; }, + async getJobInfosAsync(): Promise<WebsiteBackendJobInfo[]> { + const result = await fetchUtils.requestAsync(utils.getBackendBaseUrl(), JOBS_ENDPOINT); + return result; + }, async getPriceInfoAsync(tokenSymbols: string[]): Promise<WebsiteBackendPriceInfo> { if (_.isEmpty(tokenSymbols)) { return {}; diff --git a/packages/website/ts/utils/constants.ts b/packages/website/ts/utils/constants.ts index d281c5738..25670ef27 100644 --- a/packages/website/ts/utils/constants.ts +++ b/packages/website/ts/utils/constants.ts @@ -6,6 +6,7 @@ export const constants = { ETHER_TOKEN_SYMBOL: 'WETH', ZRX_TOKEN_SYMBOL: 'ZRX', ETHER_SYMBOL: 'ETH', + TOKEN_AMOUNT_DISPLAY_PRECISION: 5, GENESIS_ORDER_BLOCK_BY_NETWORK_ID: { 1: 4145578, 42: 3117574, diff --git a/packages/website/ts/utils/error_reporter.ts b/packages/website/ts/utils/error_reporter.ts index 548d4d41d..f875141fe 100644 --- a/packages/website/ts/utils/error_reporter.ts +++ b/packages/website/ts/utils/error_reporter.ts @@ -37,7 +37,7 @@ export const errorReporter = { return; // Let's not log development errors to rollbar } - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { rollbar.error(err, (rollbarErr: Error) => { if (rollbarErr) { logUtils.log(`Error reporting to rollbar, ignoring: ${rollbarErr}`); diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index 6961784c6..414361c1b 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -3,6 +3,7 @@ import { OrderError } from '@0xproject/order-utils'; import { constants as sharedConstants, Networks } from '@0xproject/react-shared'; import { ECSignature, Provider } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; import deepEqual = require('deep-equal'); import * as _ from 'lodash'; import * as moment from 'moment'; @@ -17,6 +18,7 @@ import { SideToAssetToken, Token, TokenByAddress, + TokenState, } from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; @@ -152,7 +154,7 @@ export const utils = { const intervalId = setTimeout(() => { resolve(false); }, getApiVersionTimeoutMs); - u2f.getApiVersion((version: number) => { + u2f.getApiVersion((_version: number) => { clearTimeout(intervalId); resolve(true); }); @@ -279,7 +281,7 @@ export const utils = { if (document.readyState === 'complete') { return; // Already loaded } - return new Promise<void>((resolve, reject) => { + return new Promise<void>((resolve, _reject) => { window.onload = () => resolve(); }); }, @@ -318,9 +320,27 @@ export const utils = { shouldShowPortalV2(): boolean { return this.isDevelopment() || this.isStaging() || this.isDogfood(); }, + shouldShowJobsPage(): boolean { + return this.isDevelopment() || this.isStaging() || this.isDogfood(); + }, getEthToken(tokenByAddress: TokenByAddress): Token { + return utils.getTokenBySymbol(constants.ETHER_TOKEN_SYMBOL, tokenByAddress); + }, + getZrxToken(tokenByAddress: TokenByAddress): Token { + return utils.getTokenBySymbol(constants.ZRX_TOKEN_SYMBOL, tokenByAddress); + }, + getTokenBySymbol(symbol: string, tokenByAddress: TokenByAddress): Token { const tokens = _.values(tokenByAddress); - const etherToken = _.find(tokens, { symbol: constants.ETHER_TOKEN_SYMBOL }); - return etherToken; + const token = _.find(tokens, { symbol }); + return token; + }, + getFormattedAmountFromToken(token: Token, tokenState: TokenState): string { + return utils.getFormattedAmount(tokenState.balance, token.decimals, token.symbol); + }, + getFormattedAmount(amount: BigNumber, decimals: number, symbol: string): string { + const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); + const precision = Math.min(constants.TOKEN_AMOUNT_DISPLAY_PRECISION, unitAmount.decimalPlaces()); + const formattedAmount = unitAmount.toFixed(precision); + return `${formattedAmount} ${symbol}`; }, }; |