diff options
Diffstat (limited to 'packages/website')
32 files changed, 564 insertions, 205 deletions
diff --git a/packages/website/package.json b/packages/website/package.json index a17964f2b..5287414c7 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -10,7 +10,7 @@ "build": "NODE_ENV=production webpack; exit 0;", "clean": "shx rm -f public/bundle*", "lint": "tslint --project . 'ts/**/*.ts' 'ts/**/*.tsx'", - "watch": "webpack-dev-server --content-base public --https", + "watch_without_deps": "webpack-dev-server --content-base public --https", "deploy_dogfood": "npm run build; aws s3 sync ./public/. s3://dogfood.0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", "deploy_staging": "npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", "deploy_live": "npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers" @@ -37,6 +37,7 @@ "lodash": "^4.17.4", "material-ui": "^0.17.1", "moment": "2.21.0", + "polished": "^1.9.2", "query-string": "^6.0.0", "react": "15.6.1", "react-copy-to-clipboard": "^4.2.3", @@ -52,6 +53,7 @@ "redux": "^3.6.0", "redux-devtools-extension": "^2.13.2", "semver-sort": "0.0.4", + "styled-components": "^3.3.0", "thenby": "^1.2.3", "truffle-contract": "2.0.1", "web3": "^0.20.0", diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 212e829dd..6e4d03e27 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -506,7 +506,7 @@ export class Blockchain { public async getTokenBalanceAndAllowanceAsync( ownerAddressIfExists: string, tokenAddress: string, - ): Promise<BigNumber[]> { + ): Promise<[BigNumber, BigNumber]> { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); if (_.isUndefined(ownerAddressIfExists)) { @@ -516,8 +516,10 @@ export class Blockchain { let balance = new BigNumber(0); let allowance = new BigNumber(0); if (this._doesUserAddressExist()) { - balance = await this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists); - allowance = await this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists); + [balance, allowance] = await Promise.all([ + this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists), + this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists), + ]); } return [balance, allowance]; } diff --git a/packages/website/ts/components/forms/subscribe_form.tsx b/packages/website/ts/components/forms/subscribe_form.tsx new file mode 100644 index 000000000..04ef28e70 --- /dev/null +++ b/packages/website/ts/components/forms/subscribe_form.tsx @@ -0,0 +1,124 @@ +import { colors } from '@0xproject/react-shared'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Button } from 'ts/components/ui/button'; +import { Container } from 'ts/components/ui/container'; +import { Input } from 'ts/components/ui/input'; +import { Text } from 'ts/components/ui/text'; +import { styled } from 'ts/style/theme'; +import { backendClient } from 'ts/utils/backend_client'; + +export interface SubscribeFormProps {} + +export enum SubscribeFormStatus { + None, + Error, + Success, + Loading, + Other, +} + +export interface SubscribeFormState { + emailText: string; + lastSubmittedEmail: string; + status: SubscribeFormStatus; +} + +const FORM_FONT_SIZE = '15px'; + +// TODO: Translate visible strings. https://app.asana.com/0/628666249318202/697485674422001 +export class SubscribeForm extends React.Component<SubscribeFormProps, SubscribeFormState> { + public state = { + emailText: '', + lastSubmittedEmail: '', + status: SubscribeFormStatus.None, + }; + public render(): React.ReactNode { + return ( + <Container className="flex flex-column items-center justify-between md-mx2 sm-mx2"> + <Container marginBottom="15px"> + <Text fontFamily="Roboto Mono" fontColor={colors.grey} center={true}> + Subscribe to our newsletter for 0x relayer and dApp updates + </Text> + </Container> + <form onSubmit={this._handleFormSubmitAsync.bind(this)}> + <Container className="flex flex-wrap justify-center items-center"> + <Container marginTop="15px"> + <Input + placeholder="you@email.com" + value={this.state.emailText} + fontColor={colors.white} + fontSize={FORM_FONT_SIZE} + backgroundColor={colors.projectsGrey} + width="300px" + onChange={this._handleEmailInputChange.bind(this)} + /> + </Container> + <Container marginLeft="15px" marginTop="15px"> + <Button + type="submit" + backgroundColor={colors.darkestGrey} + fontColor={colors.white} + fontSize={FORM_FONT_SIZE} + > + Subscribe + </Button> + </Container> + </Container> + </form> + {this._renderMessage()} + </Container> + ); + } + private _renderMessage(): React.ReactNode { + let message = null; + switch (this.state.status) { + case SubscribeFormStatus.Error: + message = 'Sorry, something went wrong. Try again later.'; + break; + case SubscribeFormStatus.Loading: + message = 'One second...'; + break; + case SubscribeFormStatus.Success: + message = `Thanks! ${this.state.lastSubmittedEmail} is now on the mailing list.`; + break; + case SubscribeFormStatus.None: + break; + default: + throw new Error( + 'The SubscribeFormStatus switch statement is not exhaustive when choosing an error message.', + ); + } + return ( + <Container isHidden={!message} marginTop="30px"> + <Text center={true} fontFamily="Roboto Mono" fontColor={colors.grey}> + {message || 'spacer text (never shown to user)'} + </Text> + </Container> + ); + } + private _handleEmailInputChange(event: React.ChangeEvent<HTMLInputElement>): void { + this.setState({ emailText: event.target.value }); + } + private async _handleFormSubmitAsync(event: React.FormEvent<HTMLInputElement>): Promise<void> { + event.preventDefault(); + if (_.isUndefined(this.state.emailText) || _.isEmpty(this.state.emailText)) { + return; + } + this.setState({ + status: SubscribeFormStatus.Loading, + lastSubmittedEmail: this.state.emailText, + }); + try { + const response = await backendClient.subscribeToNewsletterAsync(this.state.emailText); + const status = response.status === 200 ? SubscribeFormStatus.Success : SubscribeFormStatus.Error; + this.setState({ status, emailText: '' }); + } catch (error) { + this._setStatus(SubscribeFormStatus.Error); + } + } + private _setStatus(status: SubscribeFormStatus): void { + this.setState({ status }); + } +} diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx index d61dfa87d..a8df4935a 100644 --- a/packages/website/ts/components/inputs/allowance_toggle.tsx +++ b/packages/website/ts/components/inputs/allowance_toggle.tsx @@ -5,9 +5,9 @@ import Toggle from 'material-ui/Toggle'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { colors } from 'ts/style/colors'; import { BalanceErrs, Token, TokenState } from 'ts/types'; import { analytics } from 'ts/utils/analytics'; -import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; diff --git a/packages/website/ts/components/onboarding/onboarding_flow.tsx b/packages/website/ts/components/onboarding/onboarding_flow.tsx index 4066babaf..158ac23ec 100644 --- a/packages/website/ts/components/onboarding/onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/onboarding_flow.tsx @@ -5,7 +5,7 @@ import { Placement, Popper, PopperChildrenProps } from 'react-popper'; import { ContinueButtonDisplay, OnboardingTooltip } from 'ts/components/onboarding/onboarding_tooltip'; import { Container } from 'ts/components/ui/container'; import { Overlay } from 'ts/components/ui/overlay'; -import { zIndex } from 'ts/utils/style'; +import { zIndex } from 'ts/style/z_index'; export interface Step { target: string; diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index 2014dd7b0..bf52684d7 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { BigNumber } from '@0xproject/utils'; import { OnboardingFlow, Step } from 'ts/components/onboarding/onboarding_flow'; -import { ProviderType, TokenByAddress } from 'ts/types'; +import { ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { utils } from 'ts/utils/utils'; export interface PortalOnboardingFlowProps { @@ -16,6 +16,7 @@ export interface PortalOnboardingFlowProps { blockchainIsLoaded: boolean; userEtherBalanceInWei?: BigNumber; tokenByAddress: TokenByAddress; + trackedTokenStateByAddress: TokenStateByAddress; updateIsRunning: (isRunning: boolean) => void; updateOnboardingStep: (stepIndex: number) => void; } @@ -68,13 +69,13 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr target: '.eth-row', content: 'Before you begin you will need to send some ETH to your metamask wallet', placement: 'right', - continueButtonDisplay: this._userHasEth() ? 'enabled' : 'disabled', + continueButtonDisplay: this._userHasVisibleEth() ? 'enabled' : 'disabled', }, { target: '.weth-row', content: 'You need to convert some of your ETH into tradeable Wrapped ETH (WETH)', placement: 'right', - continueButtonDisplay: this._userHasWeth() ? 'enabled' : 'disabled', + continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled', }, ]; return steps; @@ -84,13 +85,17 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr return !_.isEmpty(this.props.userAddress); } - private _userHasEth(): boolean { + private _userHasVisibleEth(): boolean { return this.props.userEtherBalanceInWei > new BigNumber(0); } - private _userHasWeth(): boolean { - // TODO: https://app.asana.com/0/681385331277907/690722374136933 - return false; + private _userHasVisibleWeth(): boolean { + const ethToken = utils.getEthToken(this.props.tokenByAddress); + if (!ethToken) { + return false; + } + const wethTokenState = this.props.trackedTokenStateByAddress[ethToken.address]; + return wethTokenState.balance > new BigNumber(0); } private _overrideOnboardingStateIfShould(): void { diff --git a/packages/website/ts/components/portal/back_button.tsx b/packages/website/ts/components/portal/back_button.tsx index 48858613c..2d0bbefc3 100644 --- a/packages/website/ts/components/portal/back_button.tsx +++ b/packages/website/ts/components/portal/back_button.tsx @@ -2,7 +2,7 @@ import { Styles } from '@0xproject/react-shared'; import * as React from 'react'; import { Link } from 'react-router-dom'; -import { colors } from 'ts/utils/colors'; +import { colors } from 'ts/style/colors'; export interface BackButtonProps { to: string; diff --git a/packages/website/ts/components/portal/drawer_menu.tsx b/packages/website/ts/components/portal/drawer_menu.tsx index ace11639a..8ac2b9091 100644 --- a/packages/website/ts/components/portal/drawer_menu.tsx +++ b/packages/website/ts/components/portal/drawer_menu.tsx @@ -4,8 +4,8 @@ import * as React from 'react'; import { defaultMenuItemEntries, Menu } from 'ts/components/portal/menu'; import { Identicon } from 'ts/components/ui/identicon'; +import { colors } from 'ts/style/colors'; import { WebsitePaths } from 'ts/types'; -import { colors } from 'ts/utils/colors'; import { utils } from 'ts/utils/utils'; const IDENTICON_DIAMETER = 45; diff --git a/packages/website/ts/components/portal/menu.tsx b/packages/website/ts/components/portal/menu.tsx index 6e97ee37e..f98346101 100644 --- a/packages/website/ts/components/portal/menu.tsx +++ b/packages/website/ts/components/portal/menu.tsx @@ -2,8 +2,8 @@ import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; import { MenuItem } from 'ts/components/ui/menu_item'; +import { colors } from 'ts/style/colors'; import { Environments, WebsitePaths } from 'ts/types'; -import { colors } from 'ts/utils/colors'; import { configs } from 'ts/utils/configs'; export interface MenuTheme { diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 589ad00ad..9420395cf 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -34,13 +34,17 @@ import { Dispatcher } from 'ts/redux/dispatcher'; import { BlockchainErrs, HashData, + ItemByAddress, Order, ProviderType, ScreenWidths, + Token, TokenByAddress, + TokenStateByAddress, TokenVisibility, WebsitePaths, } from 'ts/types'; +import { backendClient } from 'ts/utils/backend_client'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { orderParser } from 'ts/utils/order_parser'; @@ -77,6 +81,7 @@ interface PortalState { isDisclaimerDialogOpen: boolean; isLedgerDialogOpen: boolean; tokenManagementState: TokenManagementState; + trackedTokenStateByAddress: TokenStateByAddress; } interface AccountManagementItem { @@ -127,6 +132,9 @@ export class Portal extends React.Component<PortalProps, PortalState> { const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER); const hasAcceptedDisclaimer = !_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer); + const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress( + this._getCurrentTrackedTokens(), + ); this.state = { prevNetworkId: this.props.networkId, prevNodeVersion: this.props.nodeVersion, @@ -135,6 +143,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { isDisclaimerDialogOpen: !hasAcceptedDisclaimer, tokenManagementState: TokenManagementState.None, isLedgerDialogOpen: false, + trackedTokenStateByAddress: initialTrackedTokenStateByAddress, }; } public componentDidMount(): void { @@ -143,6 +152,9 @@ export class Portal extends React.Component<PortalProps, PortalState> { } public componentWillMount(): void { this._blockchain = new Blockchain(this.props.dispatcher); + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); } public componentWillUnmount(): void { this._blockchain.destroy(); @@ -178,6 +190,39 @@ export class Portal extends React.Component<PortalProps, PortalState> { prevPathname: nextProps.location.pathname, }); } + if ( + nextProps.userAddress !== this.props.userAddress || + nextProps.networkId !== this.props.networkId || + nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch + ) { + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + } + + const nextTrackedTokens = this._getTrackedTokens(nextProps.tokenByAddress); + const trackedTokens = this._getCurrentTrackedTokens(); + + if (!_.isEqual(nextTrackedTokens, trackedTokens)) { + const newTokens = _.difference(nextTrackedTokens, trackedTokens); + const newTokenAddresses = _.map(newTokens, token => token.address); + // Add placeholder entry for this token to the state, since fetching the + // balance/allowance is asynchronous + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + for (const tokenAddress of newTokenAddresses) { + trackedTokenStateByAddress[tokenAddress] = { + balance: new BigNumber(0), + allowance: new BigNumber(0), + isLoaded: false, + }; + } + this.setState({ + trackedTokenStateByAddress, + }); + // Fetch the actual balance/allowance. + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(newTokenAddresses); + } } public render(): React.ReactNode { const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind( @@ -190,7 +235,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { : TokenVisibility.TRACKED; return ( <div style={styles.root}> - <PortalOnboardingFlow /> + <PortalOnboardingFlow trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} /> <DocumentTitle title="0x Portal DApp" /> <TopBar userAddress={this.props.userAddress} @@ -223,10 +268,6 @@ export class Portal extends React.Component<PortalProps, PortalState> { toggleDialogFn={updateShouldBlockchainErrDialogBeOpen} networkId={this.props.networkId} /> - <PortalDisclaimerDialog - isOpen={this.state.isDisclaimerDialogOpen} - onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)} - /> <FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} /> {this.props.blockchainIsLoaded && ( <LedgerConfigDialog @@ -283,8 +324,6 @@ export class Portal extends React.Component<PortalProps, PortalState> { ); } private _renderWallet(): React.ReactNode { - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); return ( <div> <Wallet @@ -295,16 +334,18 @@ export class Portal extends React.Component<PortalProps, PortalState> { blockchainErr={this.props.blockchainErr} dispatcher={this.props.dispatcher} tokenByAddress={this.props.tokenByAddress} - trackedTokens={trackedTokens} + trackedTokens={this._getCurrentTrackedTokens()} userEtherBalanceInWei={this.props.userEtherBalanceInWei} lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} injectedProviderName={this.props.injectedProviderName} providerType={this.props.providerType} screenWidth={this.props.screenWidth} location={this.props.location} + trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)} onAddToken={this._onAddToken.bind(this)} onRemoveToken={this._onRemoveToken.bind(this)} + refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)} /> <Container marginTop="15px"> <Island> @@ -351,18 +392,24 @@ export class Portal extends React.Component<PortalProps, PortalState> { }, ]; return ( - <Switch> - {_.map(accountManagementItems, item => { - return ( - <Route - key={item.pathName} - path={item.pathName} - render={this._renderAccountManagementItem.bind(this, item)} - /> - ); - })}} - <Route render={this._renderNotFoundMessage.bind(this)} /> - </Switch> + <div> + <Switch> + {_.map(accountManagementItems, item => { + return ( + <Route + key={item.pathName} + path={item.pathName} + render={this._renderAccountManagementItem.bind(this, item)} + /> + ); + })}} + <Route render={this._renderNotFoundMessage.bind(this)} /> + </Switch> + <PortalDisclaimerDialog + isOpen={this.state.isDisclaimerDialogOpen} + onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)} + /> + </div> ); } private _renderAccountManagementItem(item: AccountManagementItem): React.ReactNode { @@ -424,8 +471,6 @@ export class Portal extends React.Component<PortalProps, PortalState> { ); } private _renderTokenBalances(): React.ReactNode { - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); return ( <TokenBalances blockchain={this._blockchain} @@ -434,7 +479,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { dispatcher={this.props.dispatcher} screenWidth={this.props.screenWidth} tokenByAddress={this.props.tokenByAddress} - trackedTokens={trackedTokens} + trackedTokens={this._getCurrentTrackedTokens()} userAddress={this.props.userAddress} userEtherBalanceInWei={this.props.userEtherBalanceInWei} networkId={this.props.networkId} @@ -515,6 +560,85 @@ export class Portal extends React.Component<PortalProps, PortalState> { const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; return isSmallScreen; } + + private _getCurrentTrackedTokens(): Token[] { + return this._getTrackedTokens(this.props.tokenByAddress); + } + + private _getTrackedTokens(tokenByAddress: TokenByAddress): Token[] { + const allTokens = _.values(tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); + return trackedTokens; + } + + private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]): TokenStateByAddress { + const trackedTokenStateByAddress: TokenStateByAddress = {}; + _.each(trackedTokens, token => { + trackedTokenStateByAddress[token.address] = { + balance: new BigNumber(0), + allowance: new BigNumber(0), + isLoaded: false, + }; + }); + return trackedTokenStateByAddress; + } + + private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> { + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; + const balancesAndAllowances = await Promise.all( + tokenAddresses.map(async tokenAddress => { + return this._blockchain.getTokenBalanceAndAllowanceAsync(userAddressIfExists, tokenAddress); + }), + ); + const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses); + for (let i = 0; i < tokenAddresses.length; i++) { + // Order is preserved in Promise.all + const [balance, allowance] = balancesAndAllowances[i]; + const tokenAddress = tokenAddresses[i]; + trackedTokenStateByAddress[tokenAddress] = { + balance, + allowance, + isLoaded: true, + price: priceByAddress[tokenAddress], + }; + } + this.setState({ + trackedTokenStateByAddress, + }); + } + + private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> { + if (_.isEmpty(tokenAddresses)) { + return {}; + } + // for each input token address, search for the corresponding symbol in this.props.tokenByAddress, if it exists + // create a mapping from existing symbols -> address + const tokenAddressBySymbol: { [symbol: string]: string } = {}; + _.each(tokenAddresses, address => { + const tokenIfExists = _.get(this.props.tokenByAddress, address); + if (!_.isUndefined(tokenIfExists)) { + const symbol = tokenIfExists.symbol; + tokenAddressBySymbol[symbol] = address; + } + }); + const tokenSymbols = _.keys(tokenAddressBySymbol); + try { + const priceBySymbol = await backendClient.getPriceInfoAsync(tokenSymbols); + const priceByAddress = _.mapKeys(priceBySymbol, (value, symbol) => _.get(tokenAddressBySymbol, symbol)); + const result = _.mapValues(priceByAddress, price => { + const priceBigNumber = new BigNumber(price); + return priceBigNumber; + }); + return result; + } catch (err) { + return {}; + } + } + + private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> { + await this._fetchBalancesAndAllowancesAsync([tokenAddress]); + } } interface LargeLayoutProps { 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 ad6ab3de1..41febebe9 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -7,8 +7,8 @@ import { TopTokens } from 'ts/components/relayer_index/relayer_top_tokens'; import { Container } from 'ts/components/ui/container'; import { Island } from 'ts/components/ui/island'; import { TokenIcon } from 'ts/components/ui/token_icon'; +import { colors } from 'ts/style/colors'; import { Token, WebsiteBackendRelayerInfo } from 'ts/types'; -import { colors } from 'ts/utils/colors'; export interface RelayerGridTileProps { relayerInfo: WebsiteBackendRelayerInfo; diff --git a/packages/website/ts/components/relayer_index/relayer_index.tsx b/packages/website/ts/components/relayer_index/relayer_index.tsx index 9ef6eaf59..683f7084b 100644 --- a/packages/website/ts/components/relayer_index/relayer_index.tsx +++ b/packages/website/ts/components/relayer_index/relayer_index.tsx @@ -6,9 +6,9 @@ import { GridList } from 'material-ui/GridList'; import * as React from 'react'; import { RelayerGridTile } from 'ts/components/relayer_index/relayer_grid_tile'; +import { colors } from 'ts/style/colors'; import { ScreenWidths, WebsiteBackendRelayerInfo } from 'ts/types'; import { backendClient } from 'ts/utils/backend_client'; -import { colors } from 'ts/utils/colors'; export interface RelayerIndexProps { networkId: number; diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 7a0742bbe..2bc065b0f 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -77,11 +77,11 @@ interface TokenBalancesProps { interface TokenBalancesState { errorType: BalanceErrs; + trackedTokenStateByAddress: TokenStateByAddress; isBalanceSpinnerVisible: boolean; isZRXSpinnerVisible: boolean; isTokenPickerOpen: boolean; isAddingToken: boolean; - trackedTokenStateByAddress: TokenStateByAddress; } export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> { diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index 8a337119a..dba08f85c 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -9,10 +9,10 @@ import { ProviderPicker } from 'ts/components/top_bar/provider_picker'; import { DropDown } from 'ts/components/ui/drop_down'; import { Identicon } from 'ts/components/ui/identicon'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { colors } from 'ts/style/colors'; +import { zIndex } from 'ts/style/z_index'; import { ProviderType } from 'ts/types'; -import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; -import { zIndex } from 'ts/utils/style'; import { utils } from 'ts/utils/utils'; const ROOT_HEIGHT = 24; diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 2561da4e6..81ab2c06c 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -16,9 +16,9 @@ import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item'; import { DropDown } from 'ts/components/ui/drop_down'; import { Identicon } from 'ts/components/ui/identicon'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { zIndex } from 'ts/style/z_index'; import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/types'; import { constants } from 'ts/utils/constants'; -import { zIndex } from 'ts/utils/style'; import { Translate } from 'ts/utils/translate'; import { utils } from 'ts/utils/utils'; diff --git a/packages/website/ts/components/ui/button.tsx b/packages/website/ts/components/ui/button.tsx new file mode 100644 index 000000000..4c7d59839 --- /dev/null +++ b/packages/website/ts/components/ui/button.tsx @@ -0,0 +1,79 @@ +import { colors } from '@0xproject/react-shared'; +import { darken } from 'polished'; +import * as React from 'react'; +import { styled } from 'ts/style/theme'; + +export interface ButtonProps { + className?: string; + fontSize?: string; + fontColor?: string; + backgroundColor?: string; + borderColor?: string; + width?: string; + type?: string; + onClick?: (event: React.MouseEvent<HTMLElement>) => void; +} + +const PlainButton: React.StatelessComponent<ButtonProps> = ({ children, onClick, type, className }) => ( + <button type={type} className={className} onClick={onClick}> + {children} + </button> +); + +export const Button = styled(PlainButton)` + cursor: pointer; + font-size: ${props => props.fontSize}; + color: ${props => props.fontColor}; + 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'; + width: ${props => props.width}; + background-color: ${props => props.backgroundColor}; + border: ${props => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')}; + &:hover { + background-color: ${props => darken(0.1, props.backgroundColor)}; + } + &:active { + background-color: ${props => darken(0.2, props.backgroundColor)}; + } +`; + +Button.defaultProps = { + fontSize: '12px', + backgroundColor: colors.white, + width: 'auto', +}; + +Button.displayName = 'Button'; + +type CallToActionType = 'light' | 'dark'; + +export interface CallToActionProps { + type?: CallToActionType; + fontSize?: string; + width?: string; +} + +export const CallToAction: React.StatelessComponent<CallToActionProps> = ({ children, type, fontSize, width }) => { + const isLight = type === 'light'; + const backgroundColor = isLight ? colors.white : colors.heroGrey; + const fontColor = isLight ? colors.heroGrey : colors.white; + const borderColor = isLight ? undefined : colors.white; + return ( + <Button + fontSize={fontSize} + backgroundColor={backgroundColor} + fontColor={fontColor} + width={width} + borderColor={borderColor} + > + {children} + </Button> + ); +}; + +CallToAction.defaultProps = { + type: 'dark', +}; diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index d577447b0..c6a78e181 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -11,13 +11,20 @@ export interface ContainerProps { paddingBottom?: StringOrNum; paddingRight?: StringOrNum; paddingLeft?: StringOrNum; + backgroundColor?: string; + borderRadius?: StringOrNum; maxWidth?: StringOrNum; - children?: React.ReactNode; + isHidden?: boolean; + className?: string; } -export const Container: React.StatelessComponent<ContainerProps> = (props: ContainerProps) => { - const { children, ...style } = props; - return <div style={style}>{children}</div>; +export const Container: React.StatelessComponent<ContainerProps> = ({ children, className, isHidden, ...style }) => { + const visibility = isHidden ? 'hidden' : undefined; + return ( + <div style={{ ...style, visibility }} className={className}> + {children} + </div> + ); }; Container.displayName = 'Container'; diff --git a/packages/website/ts/components/ui/input.tsx b/packages/website/ts/components/ui/input.tsx new file mode 100644 index 000000000..e01a71a53 --- /dev/null +++ b/packages/website/ts/components/ui/input.tsx @@ -0,0 +1,43 @@ +import { colors } from '@0xproject/react-shared'; +import * as React from 'react'; +import { styled } from 'ts/style/theme'; + +export interface InputProps { + className?: string; + value?: string; + width?: string; + fontSize?: string; + fontColor?: string; + placeholderColor?: string; + placeholder?: string; + backgroundColor?: string; + onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; +} + +const PlainInput: React.StatelessComponent<InputProps> = ({ value, className, placeholder, onChange }) => ( + <input className={className} value={value} onChange={onChange} placeholder={placeholder} /> +); + +export const Input = styled(PlainInput)` + font-size: ${props => props.fontSize}; + width: ${props => props.width}; + padding: 0.8em 1.2em; + border-radius: 3px; + font-family: 'Roboto Mono'; + color: ${props => props.fontColor}; + border: none; + background-color: ${props => props.backgroundColor}; + &::placeholder { + color: ${props => props.placeholderColor}; + } +`; + +Input.defaultProps = { + width: 'auto', + backgroundColor: colors.white, + fontColor: colors.darkestGrey, + placeholderColor: colors.darkGrey, + fontSize: '12px', +}; + +Input.displayName = 'Input'; diff --git a/packages/website/ts/components/ui/island.tsx b/packages/website/ts/components/ui/island.tsx index f5480c9c9..802a7830a 100644 --- a/packages/website/ts/components/ui/island.tsx +++ b/packages/website/ts/components/ui/island.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; +import { colors } from 'ts/style/colors'; import { Styleable } from 'ts/types'; -import { colors } from 'ts/utils/colors'; export interface IslandProps { style?: React.CSSProperties; diff --git a/packages/website/ts/components/ui/overlay.tsx b/packages/website/ts/components/ui/overlay.tsx index bb2ed8e59..acca8194f 100644 --- a/packages/website/ts/components/ui/overlay.tsx +++ b/packages/website/ts/components/ui/overlay.tsx @@ -2,7 +2,7 @@ import { colors } from '@0xproject/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; -import { zIndex } from 'ts/utils/style'; +import { zIndex } from 'ts/style/z_index'; export interface OverlayProps { children?: React.ReactNode; diff --git a/packages/website/ts/components/ui/text.tsx b/packages/website/ts/components/ui/text.tsx new file mode 100644 index 000000000..99bf89966 --- /dev/null +++ b/packages/website/ts/components/ui/text.tsx @@ -0,0 +1,41 @@ +import { colors } from '@0xproject/react-shared'; +import * as React from 'react'; +import { styled } from 'ts/style/theme'; +import { Deco, Key } from 'ts/types'; +import { Translate } from 'ts/utils/translate'; + +export type TextTag = 'p' | 'div' | 'span' | 'label'; + +export interface TextProps { + className?: string; + Tag?: TextTag; + fontSize?: string; + fontFamily?: string; + fontColor?: string; + lineHeight?: string; + center?: boolean; + fontWeight?: number; +} + +const PlainText: React.StatelessComponent<TextProps> = ({ children, className, Tag }) => ( + <Tag className={className}>{children}</Tag> +); + +export const Text = styled(PlainText)` + font-family: ${props => props.fontFamily}; + font-weight: ${props => props.fontWeight}; + font-size: ${props => props.fontSize}; + ${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')}; + ${props => (props.center ? 'text-align: center' : '')}; + color: ${props => props.fontColor}; +`; + +Text.defaultProps = { + fontFamily: 'Roboto', + fontWeight: 400, + fontColor: colors.white, + fontSize: '14px', + Tag: 'div', +}; + +Text.displayName = 'Text'; diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 18dada22f..37233930e 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -32,10 +32,11 @@ 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 { Dispatcher } from 'ts/redux/dispatcher'; +import { colors } from 'ts/style/colors'; +import { zIndex } from 'ts/style/z_index'; import { BalanceErrs, BlockchainErrs, - ItemByAddress, ProviderType, ScreenWidths, Side, @@ -45,10 +46,7 @@ import { TokenStateByAddress, WebsitePaths, } from 'ts/types'; -import { backendClient } from 'ts/utils/backend_client'; -import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; -import { zIndex } from 'ts/utils/style'; import { utils } from 'ts/utils/utils'; import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles'; @@ -67,13 +65,14 @@ export interface WalletProps { providerType: ProviderType; screenWidth: ScreenWidths; location: Location; + trackedTokenStateByAddress: TokenStateByAddress; onToggleLedgerDialog: () => void; onAddToken: () => void; onRemoveToken: () => void; + refetchTokenStateAsync: (tokenAddress: string) => Promise<void>; } interface WalletState { - trackedTokenStateByAddress: TokenStateByAddress; wrappedEtherDirection?: Side; isHoveringSidebar: boolean; } @@ -151,48 +150,12 @@ export class Wallet extends React.Component<WalletProps, WalletState> { constructor(props: WalletProps) { super(props); this._isUnmounted = false; - const trackedTokenAddresses = _.map(props.trackedTokens, token => token.address); - const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(trackedTokenAddresses); this.state = { - trackedTokenStateByAddress: initialTrackedTokenStateByAddress, wrappedEtherDirection: undefined, isHoveringSidebar: false, }; } - public componentWillMount(): void { - const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); - // tslint:disable-next-line:no-floating-promises - this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); - } - public componentWillUnmount(): void { - this._isUnmounted = true; - } - public componentWillReceiveProps(nextProps: WalletProps): void { - if ( - nextProps.userAddress !== this.props.userAddress || - nextProps.networkId !== this.props.networkId || - nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch - ) { - const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); - // tslint:disable-next-line:no-floating-promises - this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); - } - if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) { - const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens); - const newTokenAddresses = _.map(newTokens, token => token.address); - // Add placeholder entry for this token to the state, since fetching the - // balance/allowance is asynchronous - const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; - const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(newTokenAddresses); - _.assign(trackedTokenStateByAddress, initialTrackedTokenStateByAddress); - this.setState({ - trackedTokenStateByAddress, - }); - // Fetch the actual balance/allowance. - // tslint:disable-next-line:no-floating-promises - this._fetchBalancesAndAllowancesAsync(newTokenAddresses); - } - } + public render(): React.ReactNode { const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError; return ( @@ -342,7 +305,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { _.isUndefined(this.props.userEtherBalanceInWei), ); const etherToken = this._getEthToken(); - const etherTokenState = this.state.trackedTokenStateByAddress[etherToken.address]; + const etherTokenState = this.props.trackedTokenStateByAddress[etherToken.address]; const etherPrice = etherTokenState.price; const secondaryText = this._renderValue( this.props.userEtherBalanceInWei || new BigNumber(0), @@ -366,7 +329,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); } private _renderTokenRow(token: Token, index: number): React.ReactNode { - const tokenState = this.state.trackedTokenStateByAddress[token.address]; + const tokenState = this.props.trackedTokenStateByAddress[token.address]; const tokenLink = sharedUtils.getEtherScanLinkIfExists( token.address, this.props.networkId, @@ -438,7 +401,8 @@ export class Wallet extends React.Component<WalletProps, WalletState> { etherToken={etherToken} lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} onConversionSuccessful={this._closeWrappedEtherActionRow.bind(this)} - refetchEthTokenStateAsync={this._refetchTokenStateAsync.bind(this, etherToken.address)} + // tslint:disable:jsx-no-lambda + refetchEthTokenStateAsync={async () => this.props.refetchTokenStateAsync(etherToken.address)} /> )} </div> @@ -474,7 +438,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { onErrorOccurred={_.noop} // TODO: Error handling userAddress={this.props.userAddress} isDisabled={!config.tokenState.isLoaded} - refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, config.token.address)} + refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)} /> ); } @@ -557,69 +521,6 @@ export class Wallet extends React.Component<WalletProps, WalletState> { }); return trackedTokenStateByAddress; } - private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> { - const balanceAndAllowanceTupleByAddress: ItemByAddress<BigNumber[]> = {}; - const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; - for (const tokenAddress of tokenAddresses) { - const balanceAndAllowanceTuple = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( - userAddressIfExists, - tokenAddress, - ); - balanceAndAllowanceTupleByAddress[tokenAddress] = balanceAndAllowanceTuple; - } - const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses); - const trackedTokenStateByAddress = _.reduce( - tokenAddresses, - (acc, address) => { - const [balance, allowance] = balanceAndAllowanceTupleByAddress[address]; - const priceIfExists = _.get(priceByAddress, address); - acc[address] = { - balance, - allowance, - price: priceIfExists, - isLoaded: true, - }; - return acc; - }, - this.state.trackedTokenStateByAddress, - ); - - if (!this._isUnmounted) { - this.setState({ - trackedTokenStateByAddress, - }); - } - } - private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> { - await this._fetchBalancesAndAllowancesAsync([tokenAddress]); - } - private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> { - if (_.isEmpty(tokenAddresses)) { - return {}; - } - // for each input token address, search for the corresponding symbol in this.props.tokenByAddress, if it exists - // create a mapping from existing symbols -> address - const tokenAddressBySymbol: { [symbol: string]: string } = {}; - _.each(tokenAddresses, address => { - const tokenIfExists = _.get(this.props.tokenByAddress, address); - if (!_.isUndefined(tokenIfExists)) { - const symbol = tokenIfExists.symbol; - tokenAddressBySymbol[symbol] = address; - } - }); - const tokenSymbols = _.keys(tokenAddressBySymbol); - try { - const priceBySymbol = await backendClient.getPriceInfoAsync(tokenSymbols); - const priceByAddress = _.mapKeys(priceBySymbol, (value, symbol) => _.get(tokenAddressBySymbol, symbol)); - const result = _.mapValues(priceByAddress, price => { - const priceBigNumber = new BigNumber(price); - return priceBigNumber; - }); - return result; - } catch (err) { - return {}; - } - } private _openWrappedEtherActionRow(wrappedEtherDirection: Side): void { this.setState({ wrappedEtherDirection, diff --git a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx index 17fd8a19e..e3b88dc54 100644 --- a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx +++ b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx @@ -3,8 +3,8 @@ import FlatButton from 'material-ui/FlatButton'; import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; import * as React from 'react'; +import { colors } from 'ts/style/colors'; import { ProviderType } from 'ts/types'; -import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; diff --git a/packages/website/ts/components/wallet/wrap_ether_item.tsx b/packages/website/ts/components/wallet/wrap_ether_item.tsx index 1dfcffadf..50b64d05c 100644 --- a/packages/website/ts/components/wallet/wrap_ether_item.tsx +++ b/packages/website/ts/components/wallet/wrap_ether_item.tsx @@ -10,8 +10,8 @@ import { Blockchain } from 'ts/blockchain'; import { EthAmountInput } from 'ts/components/inputs/eth_amount_input'; import { TokenAmountInput } from 'ts/components/inputs/token_amount_input'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { colors } from 'ts/style/colors'; import { BlockchainCallErrs, Side, Token } from 'ts/types'; -import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; diff --git a/packages/website/ts/containers/portal_onboarding_flow.ts b/packages/website/ts/containers/portal_onboarding_flow.ts index 8202fb2ae..0ad9aef13 100644 --- a/packages/website/ts/containers/portal_onboarding_flow.ts +++ b/packages/website/ts/containers/portal_onboarding_flow.ts @@ -2,12 +2,14 @@ import { BigNumber } from '@0xproject/utils'; import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -import { ActionTypes, ProviderType, TokenByAddress } from 'ts/types'; +import { ActionTypes, ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { PortalOnboardingFlow as PortalOnboardingFlowComponent } from 'ts/components/onboarding/portal_onboarding_flow'; import { State } from 'ts/redux/reducer'; -interface PortalOnboardingFlowProps {} +interface PortalOnboardingFlowProps { + trackedTokenStateByAddress: TokenStateByAddress; +} interface ConnectedState { stepIndex: number; @@ -26,7 +28,7 @@ interface ConnectedDispatch { updateOnboardingStep: (stepIndex: number) => void; } -const mapStateToProps = (state: State): ConnectedState => ({ +const mapStateToProps = (state: State, ownProps: PortalOnboardingFlowProps): ConnectedState => ({ stepIndex: state.portalOnboardingStep, isRunning: state.isPortalOnboardingShowing, userAddress: state.userAddress, diff --git a/packages/website/ts/pages/landing/landing.tsx b/packages/website/ts/pages/landing/landing.tsx index 02ecfa117..ed6ec4ec6 100644 --- a/packages/website/ts/pages/landing/landing.tsx +++ b/packages/website/ts/pages/landing/landing.tsx @@ -1,11 +1,13 @@ import { colors } from '@0xproject/react-shared'; import * as _ from 'lodash'; -import RaisedButton from 'material-ui/RaisedButton'; import * as React from 'react'; import DocumentTitle = require('react-document-title'); import { Link } from 'react-router-dom'; import { Footer } from 'ts/components/footer'; +import { SubscribeForm } from 'ts/components/forms/subscribe_form'; import { TopBar } from 'ts/components/top_bar/top_bar'; +import { CallToAction } from 'ts/components/ui/button'; +import { Container } from 'ts/components/ui/container'; import { Dispatcher } from 'ts/redux/dispatcher'; import { Deco, Key, Language, ScreenWidths, WebsitePaths } from 'ts/types'; import { constants } from 'ts/utils/constants'; @@ -236,7 +238,7 @@ export class Landing extends React.Component<LandingProps, LandingState> { <div className="clearfix py4" style={{ backgroundColor: colors.heroGrey }}> <div className="mx-auto max-width-4 clearfix"> {this._renderWhatsNew()} - <div className="lg-pt4 md-pt4 sm-pt2 lg-pb4 md-pb4 lg-my4 md-my4 sm-mt2 sm-mb4 clearfix"> + <div className="lg-pt4 md-pt4 sm-pt2 lg-pb4 md-pb4 lg-mt4 md-mt4 sm-mt2 sm-mb4 clearfix"> <div className="col lg-col-5 md-col-5 col-12 sm-center"> <img src="/images/landing/hero_chip_image.png" height={isSmallScreen ? 300 : 395} /> </div> @@ -268,40 +270,31 @@ export class Landing extends React.Component<LandingProps, LandingState> { > {this.props.translate.get(Key.TopTagline)} </div> - <div className="pt3 clearfix sm-mx-auto" style={{ maxWidth: 389 }}> - <div className="lg-pr2 md-pr2 col col-6 sm-center"> + <Container className="pt3 clearfix sm-mx-auto" maxWidth="390px"> + <div className="lg-pr2 md-pr2 lg-col lg-col-6 sm-center sm-col sm-col-12 mb2"> <Link to={WebsitePaths.ZeroExJs} className="text-decoration-none"> - <RaisedButton - style={{ borderRadius: 6, minWidth: 157.36 }} - buttonStyle={{ borderRadius: 6 }} - labelStyle={buttonLabelStyle} - label={this.props.translate.get(Key.BuildCallToAction, Deco.Cap)} - onClick={_.noop} - /> + <CallToAction width="175px" type="light"> + {this.props.translate.get(Key.BuildCallToAction, Deco.Cap)} + </CallToAction> </Link> </div> - <div className="col col-6 sm-center"> + <div className="lg-col lg-col-6 sm-center sm-col sm-col-12"> <a href={constants.URL_ZEROEX_CHAT} target="_blank" className="text-decoration-none" > - <RaisedButton - style={{ borderRadius: 6, minWidth: 150 }} - buttonStyle={lightButtonStyle} - labelColor="white" - backgroundColor={colors.heroGrey} - labelStyle={buttonLabelStyle} - label={this.props.translate.get(Key.CommunityCallToAction, Deco.Cap)} - onClick={_.noop} - /> + <CallToAction width="175px"> + {this.props.translate.get(Key.CommunityCallToAction, Deco.Cap)} + </CallToAction> </a> </div> - </div> + </Container> </div> </div> </div> </div> + {this.props.translate.getLanguage() === Language.English && <SubscribeForm />} </div> ); } @@ -349,6 +342,9 @@ export class Landing extends React.Component<LandingProps, LandingState> { case ScreenWidths.Lg: colWidth = isRelayersOnly ? 2 : 2 - i % 2; break; + + default: + throw new Error(`Encountered unknown ScreenWidths value: ${this.state.screenWidth}`); } return ( <div key={`project-${project.logoFileName}`} className={`col col-${colWidth} center`}> @@ -782,15 +778,9 @@ export class Landing extends React.Component<LandingProps, LandingState> { </div> <div className="sm-center sm-pt2 lg-table-cell md-table-cell"> <Link to={WebsitePaths.ZeroExJs} className="text-decoration-none"> - <RaisedButton - style={{ borderRadius: 6, minWidth: 150 }} - buttonStyle={lightButtonStyle} - labelColor={colors.white} - backgroundColor={colors.heroGrey} - labelStyle={buttonLabelStyle} - label={this.props.translate.get(Key.BuildCallToAction, Deco.Cap)} - onClick={_.noop} - /> + <CallToAction fontSize="15px"> + {this.props.translate.get(Key.BuildCallToAction, Deco.Cap)} + </CallToAction> </Link> </div> </div> diff --git a/packages/website/ts/utils/colors.ts b/packages/website/ts/style/colors.ts index 5ffdd6ba7..5ffdd6ba7 100644 --- a/packages/website/ts/utils/colors.ts +++ b/packages/website/ts/style/colors.ts diff --git a/packages/website/ts/style/theme.ts b/packages/website/ts/style/theme.ts new file mode 100644 index 000000000..9e447e7ee --- /dev/null +++ b/packages/website/ts/style/theme.ts @@ -0,0 +1,15 @@ +import * as styledComponents from 'styled-components'; + +const { + default: styled, + css, + injectGlobal, + keyframes, + ThemeProvider, +} = styledComponents as styledComponents.ThemedStyledComponentsModule<IThemeInterface>; + +export interface IThemeInterface {} + +export const theme = {}; + +export { styled, css, injectGlobal, keyframes, ThemeProvider }; diff --git a/packages/website/ts/utils/style.ts b/packages/website/ts/style/z_index.ts index 0411cdd91..0411cdd91 100644 --- a/packages/website/ts/utils/style.ts +++ b/packages/website/ts/style/z_index.ts diff --git a/packages/website/ts/utils/backend_client.ts b/packages/website/ts/utils/backend_client.ts index c440b1604..6b16aea6b 100644 --- a/packages/website/ts/utils/backend_client.ts +++ b/packages/website/ts/utils/backend_client.ts @@ -8,6 +8,7 @@ const ETH_GAS_STATION_ENDPOINT = '/eth_gas_station'; const PRICES_ENDPOINT = '/prices'; const RELAYERS_ENDPOINT = '/relayers'; const WIKI_ENDPOINT = '/wiki'; +const SUBSCRIBE_SUBSTACK_NEWSLETTER_ENDPOINT = '/newsletter_subscriber/substack'; export const backendClient = { async getGasInfoAsync(): Promise<WebsiteBackendGasInfo> { @@ -33,4 +34,11 @@ export const backendClient = { const result = await fetchUtils.requestAsync(utils.getBackendBaseUrl(), WIKI_ENDPOINT); return result; }, + async subscribeToNewsletterAsync(email: string): Promise<Response> { + const result = await fetchUtils.postAsync(utils.getBackendBaseUrl(), SUBSCRIBE_SUBSTACK_NEWSLETTER_ENDPOINT, { + email, + referrer: window.location.href, + }); + return result; + }, }; diff --git a/packages/website/ts/utils/fetch_utils.ts b/packages/website/ts/utils/fetch_utils.ts index d2e902db5..513f7e479 100644 --- a/packages/website/ts/utils/fetch_utils.ts +++ b/packages/website/ts/utils/fetch_utils.ts @@ -4,22 +4,38 @@ import * as queryString from 'query-string'; import { errorReporter } from 'ts/utils/error_reporter'; +const logErrorIfPresent = (response: Response, requestedURL: string) => { + if (response.status !== 200) { + const errorText = `Error requesting url: ${requestedURL}, ${response.status}: ${response.statusText}`; + logUtils.log(errorText); + const error = Error(errorText); + // tslint:disable-next-line:no-floating-promises + errorReporter.reportAsync(error); + throw error; + } +}; + export const fetchUtils = { async requestAsync(baseUrl: string, path: string, queryParams?: object): Promise<any> { const query = queryStringFromQueryParams(queryParams); const url = `${baseUrl}${path}${query}`; const response = await fetch(url); - if (response.status !== 200) { - const errorText = `Error requesting url: ${url}, ${response.status}: ${response.statusText}`; - logUtils.log(errorText); - const error = Error(errorText); - // tslint:disable-next-line:no-floating-promises - errorReporter.reportAsync(error); - throw error; - } + logErrorIfPresent(response, url); const result = await response.json(); return result; }, + async postAsync(baseUrl: string, path: string, body: object): Promise<Response> { + const url = `${baseUrl}${path}`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + logErrorIfPresent(response, url); + return response; + }, }; function queryStringFromQueryParams(queryParams?: object): string { diff --git a/packages/website/ts/utils/wallet_item_styles.ts b/packages/website/ts/utils/wallet_item_styles.ts index 6b038efd2..9d6033d74 100644 --- a/packages/website/ts/utils/wallet_item_styles.ts +++ b/packages/website/ts/utils/wallet_item_styles.ts @@ -1,6 +1,6 @@ import { Styles } from '@0xproject/react-shared'; -import { colors } from 'ts/utils/colors'; +import { colors } from 'ts/style/colors'; export const styles: Styles = { focusedItem: { |