diff options
Diffstat (limited to 'packages/instant/src/components')
10 files changed, 300 insertions, 174 deletions
diff --git a/packages/instant/src/components/amount_input.tsx b/packages/instant/src/components/amount_input.tsx index 38810063d..7644f5f67 100644 --- a/packages/instant/src/components/amount_input.tsx +++ b/packages/instant/src/components/amount_input.tsx @@ -3,6 +3,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import { ColorOption } from '../style/theme'; +import { util } from '../util/util'; import { Container, Input } from './ui'; @@ -10,10 +11,13 @@ export interface AmountInputProps { fontColor?: ColorOption; fontSize?: string; value?: BigNumber; - onChange?: (value?: BigNumber) => void; + onChange: (value?: BigNumber) => void; } export class AmountInput extends React.Component<AmountInputProps> { + public static defaultProps = { + onChange: util.boundNoop, + }; public render(): React.ReactNode { const { fontColor, fontSize, value } = this.props; return ( @@ -24,7 +28,7 @@ export class AmountInput extends React.Component<AmountInputProps> { onChange={this._handleChange} value={!_.isUndefined(value) ? value.toString() : ''} placeholder="0.00" - width="2em" + width="2.2em" /> </Container> ); @@ -40,8 +44,6 @@ export class AmountInput extends React.Component<AmountInputProps> { return; } } - if (!_.isUndefined(this.props.onChange)) { - this.props.onChange(bigNumberValue); - } + this.props.onChange(bigNumberValue); }; } diff --git a/packages/instant/src/components/animations/slide_animations.tsx b/packages/instant/src/components/animations/slide_animations.tsx new file mode 100644 index 000000000..1f10a2ed6 --- /dev/null +++ b/packages/instant/src/components/animations/slide_animations.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; + +import { keyframes, styled } from '../../style/theme'; + +const slideKeyframeGenerator = (fromY: string, toY: string) => keyframes` + from { + position: relative; + top: ${fromY}; + } + + to { + position: relative; + top: ${toY}; + } +`; + +export interface SlideAnimationProps { + keyframes: string; + animationType: string; + animationDirection?: string; +} + +export const SlideAnimation = + styled.div < + SlideAnimationProps > + ` + animation-name: ${props => props.keyframes}; + animation-duration: 0.3s; + animation-timing-function: ${props => props.animationType}; + animation-delay: 0s; + animation-iteration-count: 1; + animation-fill-mode: ${props => props.animationDirection || 'none'}; + position: relative; +`; + +export interface SlideAnimationComponentProps { + downY: string; +} + +export const SlideUpAnimation: React.StatelessComponent<SlideAnimationComponentProps> = props => ( + <SlideAnimation animationType="ease-in" keyframes={slideKeyframeGenerator(props.downY, '0px')}> + {props.children} + </SlideAnimation> +); + +export const SlideDownAnimation: React.StatelessComponent<SlideAnimationComponentProps> = props => ( + <SlideAnimation + animationDirection="forwards" + animationType="cubic-bezier(0.25, 0.1, 0.25, 1)" + keyframes={slideKeyframeGenerator('0px', props.downY)} + > + {props.children} + </SlideAnimation> +); diff --git a/packages/instant/src/components/animations/slide_up_and_down_animation.tsx b/packages/instant/src/components/animations/slide_up_and_down_animation.tsx deleted file mode 100644 index 9c18e0933..000000000 --- a/packages/instant/src/components/animations/slide_up_and_down_animation.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import * as React from 'react'; - -import { keyframes, styled } from '../../style/theme'; - -const slideKeyframeGenerator = (fromY: string, toY: string) => keyframes` - from { - position: relative; - top: ${fromY}; - } - - to { - position: relative; - top: ${toY}; - } -`; - -export interface SlideAnimationProps { - keyframes: string; - animationType: string; - animationDirection?: string; -} - -export const SlideAnimation = - styled.div < - SlideAnimationProps > - ` - animation-name: ${props => props.keyframes}; - animation-duration: 0.3s; - animation-timing-function: ${props => props.animationType}; - animation-delay: 0s; - animation-iteration-count: 1; - animation-fill-mode: ${props => props.animationDirection || 'none'}; - position: relative; -`; - -export interface SlideAnimationComponentProps { - downY: string; -} - -export const SlideUpAnimationComponent: React.StatelessComponent<SlideAnimationComponentProps> = props => ( - <SlideAnimation animationType="ease-in" keyframes={slideKeyframeGenerator(props.downY, '0px')}> - {props.children} - </SlideAnimation> -); - -export const SlideDownAnimationComponent: React.StatelessComponent<SlideAnimationComponentProps> = props => ( - <SlideAnimation - animationDirection="forwards" - animationType="cubic-bezier(0.25, 0.1, 0.25, 1)" - keyframes={slideKeyframeGenerator('0px', props.downY)} - > - {props.children} - </SlideAnimation> -); - -export interface SlideUpAndDownAnimationProps extends SlideAnimationComponentProps { - delayMs: number; -} - -enum SlideState { - Up = 'up', - Down = 'down', -} -interface SlideUpAndDownState { - slideState: SlideState; -} - -export class SlideUpAndDownAnimation extends React.Component<SlideUpAndDownAnimationProps, SlideUpAndDownState> { - public state = { - slideState: SlideState.Up, - }; - - private _timeoutId?: number; - public render(): React.ReactNode { - return this._renderSlide(); - } - public componentDidMount(): void { - this._timeoutId = window.setTimeout(() => { - this.setState({ - slideState: SlideState.Down, - }); - }, this.props.delayMs); - - return; - } - public componentWillUnmount(): void { - if (this._timeoutId) { - window.clearTimeout(this._timeoutId); - } - } - private _renderSlide(): React.ReactNode { - const SlideComponent = this.state.slideState === 'up' ? SlideUpAnimationComponent : SlideDownAnimationComponent; - - return <SlideComponent downY={this.props.downY}>{this.props.children}</SlideComponent>; - } -} diff --git a/packages/instant/src/components/asset_amount_input.tsx b/packages/instant/src/components/asset_amount_input.tsx new file mode 100644 index 000000000..db3dbe7f3 --- /dev/null +++ b/packages/instant/src/components/asset_amount_input.tsx @@ -0,0 +1,38 @@ +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { assetDataUtil } from '../util/asset_data'; + +import { ColorOption } from '../style/theme'; +import { util } from '../util/util'; + +import { AmountInput, AmountInputProps } from './amount_input'; +import { Container, Text } from './ui'; + +export interface AssetAmountInputProps extends AmountInputProps { + assetData?: string; + onChange: (value?: BigNumber, assetData?: string) => void; +} + +export class AssetAmountInput extends React.Component<AssetAmountInputProps> { + public static defaultProps = { + onChange: util.boundNoop, + }; + public render(): React.ReactNode { + const { assetData, onChange, ...rest } = this.props; + return ( + <Container> + <AmountInput {...rest} onChange={this._handleChange} /> + <Container display="inline-block" marginLeft="10px"> + <Text fontSize={rest.fontSize} fontColor={ColorOption.white} textTransform="uppercase"> + {assetDataUtil.bestNameForAsset(this.props.assetData, '???')} + </Text> + </Container> + </Container> + ); + } + private readonly _handleChange = (value?: BigNumber): void => { + this.props.onChange(value, this.props.assetData); + }; +} diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 5a32b9575..0706817c9 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -1,19 +1,53 @@ +import { BuyQuote } from '@0xproject/asset-buyer'; +import * as _ from 'lodash'; import * as React from 'react'; import { ColorOption } from '../style/theme'; +import { assetBuyer } from '../util/asset_buyer'; +import { util } from '../util/util'; +import { web3Wrapper } from '../util/web3_wrapper'; import { Button, Container, Text } from './ui'; -export interface BuyButtonProps {} +export interface BuyButtonProps { + buyQuote?: BuyQuote; + onClick: (buyQuote: BuyQuote) => void; + onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void; + onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void; + text: string; +} -export const BuyButton: React.StatelessComponent<BuyButtonProps> = props => ( - <Container padding="20px" width="100%"> - <Button width="100%"> - <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> - Buy - </Text> - </Button> - </Container> -); - -BuyButton.displayName = 'BuyButton'; +export class BuyButton extends React.Component<BuyButtonProps> { + public static defaultProps = { + onClick: util.boundNoop, + onBuySuccess: util.boundNoop, + onBuyFailure: util.boundNoop, + }; + public render(): React.ReactNode { + const shouldDisableButton = _.isUndefined(this.props.buyQuote); + return ( + <Container padding="20px" width="100%"> + <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> + <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> + {this.props.text} + </Text> + </Button> + </Container> + ); + } + private readonly _handleClick = async () => { + // The button is disabled when there is no buy quote anyway. + if (_.isUndefined(this.props.buyQuote)) { + return; + } + this.props.onClick(this.props.buyQuote); + let txnHash; + try { + txnHash = await assetBuyer.executeBuyQuoteAsync(this.props.buyQuote); + await web3Wrapper.awaitTransactionSuccessAsync(txnHash); + this.props.onBuySuccess(this.props.buyQuote, txnHash); + } catch { + this.props.onBuyFailure(this.props.buyQuote, txnHash); + } + }; +} diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index be0414b8d..f57b59b73 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -1,11 +1,58 @@ +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; import * as React from 'react'; import { SelectedAssetAmountInput } from '../containers/selected_asset_amount_input'; import { ColorOption } from '../style/theme'; +import { AsyncProcessState } from '../types'; +import { format } from '../util/format'; import { Container, Flex, Text } from './ui'; -export interface InstantHeadingProps {} +export interface InstantHeadingProps { + selectedAssetAmount?: BigNumber; + totalEthBaseAmount?: BigNumber; + ethUsdPrice?: BigNumber; + quoteState: AsyncProcessState; +} + +const Placeholder = () => ( + <Text fontWeight="bold" fontColor={ColorOption.white}> + — + </Text> +); +const displaytotalEthBaseAmount = ({ + selectedAssetAmount, + totalEthBaseAmount, +}: InstantHeadingProps): React.ReactNode => { + if (_.isUndefined(selectedAssetAmount)) { + return '0 ETH'; + } + return format.ethBaseAmount(totalEthBaseAmount, 4, <Placeholder />); +}; + +const displayUsdAmount = ({ + totalEthBaseAmount, + selectedAssetAmount, + ethUsdPrice, +}: InstantHeadingProps): React.ReactNode => { + if (_.isUndefined(selectedAssetAmount)) { + return '$0.00'; + } + return format.ethBaseAmountInUsd(totalEthBaseAmount, ethUsdPrice, 2, <Placeholder />); +}; + +const loadingOrAmount = (quoteState: AsyncProcessState, amount: React.ReactNode): React.ReactNode => { + if (quoteState === AsyncProcessState.PENDING) { + return ( + <Text fontWeight="bold" fontColor={ColorOption.white}> + …loading + </Text> + ); + } else { + return amount; + } +}; export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = props => ( <Container backgroundColor={ColorOption.primaryColor} padding="20px" width="100%" borderRadius="3px 3px 0px 0px"> @@ -22,22 +69,15 @@ export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = pro </Text> </Container> <Flex direction="row" justify="space-between"> - <Container> - <SelectedAssetAmountInput fontSize="45px" /> - <Container display="inline-block" marginLeft="10px"> - <Text fontSize="45px" fontColor={ColorOption.white} textTransform="uppercase"> - rep - </Text> - </Container> - </Container> + <SelectedAssetAmountInput fontSize="45px" /> <Flex direction="column" justify="space-between"> <Container marginBottom="5px"> <Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}> - 0 ETH + {loadingOrAmount(props.quoteState, displaytotalEthBaseAmount(props))} </Text> </Container> <Text fontSize="16px" fontColor={ColorOption.white} opacity={0.7}> - $0.00 + {loadingOrAmount(props.quoteState, displayUsdAmount(props))} </Text> </Flex> </Flex> diff --git a/packages/instant/src/components/order_details.tsx b/packages/instant/src/components/order_details.tsx index dbf2c1f0b..a15ff411b 100644 --- a/packages/instant/src/components/order_details.tsx +++ b/packages/instant/src/components/order_details.tsx @@ -1,53 +1,90 @@ +import { BuyQuoteInfo } from '@0xproject/asset-buyer'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; import * as React from 'react'; +import { oc } from 'ts-optchain'; import { ColorOption } from '../style/theme'; +import { format } from '../util/format'; import { Container, Flex, Text } from './ui'; -export interface OrderDetailsProps {} - -export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = props => ( - <Container padding="20px" width="100%"> - <Container marginBottom="10px"> - <Text - letterSpacing="1px" - fontColor={ColorOption.primaryColor} - fontWeight={600} - textTransform="uppercase" - fontSize="14px" - > - Order Details - </Text> - </Container> - <OrderDetailsRow name="Token Price" primaryValue=".013 ETH" secondaryValue="$24.32" /> - <OrderDetailsRow name="Fee" primaryValue=".005 ETH" secondaryValue="$1.04" /> - <OrderDetailsRow name="Total Cost" primaryValue="1.66 ETH" secondaryValue="$589.56" shouldEmphasize={true} /> - </Container> -); - -OrderDetails.displayName = 'OrderDetails'; - -export interface OrderDetailsRowProps { - name: string; - primaryValue: string; - secondaryValue: string; +export interface OrderDetailsProps { + buyQuoteInfo?: BuyQuoteInfo; + ethUsdPrice?: BigNumber; +} + +export class OrderDetails extends React.Component<OrderDetailsProps> { + public render(): React.ReactNode { + const { buyQuoteInfo, ethUsdPrice } = this.props; + const buyQuoteAccessor = oc(buyQuoteInfo); + const ethAssetPrice = buyQuoteAccessor.ethPerAssetPrice(); + const ethTokenFee = buyQuoteAccessor.feeEthAmount(); + const totalEthAmount = buyQuoteAccessor.totalEthAmount(); + return ( + <Container padding="20px" width="100%"> + <Container marginBottom="10px"> + <Text + letterSpacing="1px" + fontColor={ColorOption.primaryColor} + fontWeight={600} + textTransform="uppercase" + fontSize="14px" + > + Order Details + </Text> + </Container> + <EthAmountRow + rowLabel="Token Price" + ethAmount={ethAssetPrice} + ethUsdPrice={ethUsdPrice} + isEthAmountInBaseUnits={false} + /> + <EthAmountRow rowLabel="Fee" ethAmount={ethTokenFee} ethUsdPrice={ethUsdPrice} /> + <EthAmountRow + rowLabel="Total Cost" + ethAmount={totalEthAmount} + ethUsdPrice={ethUsdPrice} + shouldEmphasize={true} + /> + </Container> + ); + } +} + +export interface EthAmountRowProps { + rowLabel: string; + ethAmount?: BigNumber; + isEthAmountInBaseUnits?: boolean; + ethUsdPrice?: BigNumber; shouldEmphasize?: boolean; } -export const OrderDetailsRow: React.StatelessComponent<OrderDetailsRowProps> = props => { - const fontWeight = props.shouldEmphasize ? 700 : 400; +export const EthAmountRow: React.StatelessComponent<EthAmountRowProps> = ({ + rowLabel, + ethAmount, + isEthAmountInBaseUnits, + ethUsdPrice, + shouldEmphasize, +}) => { + const fontWeight = shouldEmphasize ? 700 : 400; + const usdFormatter = isEthAmountInBaseUnits ? format.ethBaseAmountInUsd : format.ethUnitAmountInUsd; + const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseAmount : format.ethUnitAmount; + const usdPriceSection = _.isUndefined(ethUsdPrice) ? null : ( + <Container marginRight="3px" display="inline-block"> + <Text fontColor={ColorOption.lightGrey}>({usdFormatter(ethAmount, ethUsdPrice)})</Text> + </Container> + ); return ( <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}> <Flex justify="space-between"> <Text fontWeight={fontWeight} fontColor={ColorOption.grey}> - {props.name} + {rowLabel} </Text> <Container> - <Container marginRight="3px" display="inline-block"> - <Text fontColor={ColorOption.lightGrey}>({props.secondaryValue}) </Text> - </Container> + {usdPriceSection} <Text fontWeight={fontWeight} fontColor={ColorOption.grey}> - {props.primaryValue} + {ethFormatter(ethAmount)} </Text> </Container> </Flex> @@ -55,8 +92,9 @@ export const OrderDetailsRow: React.StatelessComponent<OrderDetailsRowProps> = p ); }; -OrderDetailsRow.defaultProps = { +EthAmountRow.defaultProps = { shouldEmphasize: false, + isEthAmountInBaseUnits: true, }; -OrderDetailsRow.displayName = 'OrderDetailsRow'; +EthAmountRow.displayName = 'EthAmountRow'; diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx index 0237fb7e9..3865a8797 100644 --- a/packages/instant/src/components/sliding_error.tsx +++ b/packages/instant/src/components/sliding_error.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ColorOption } from '../style/theme'; -import { SlideUpAndDownAnimation } from './animations/slide_up_and_down_animation'; +import { SlideDownAnimation, SlideUpAnimation } from './animations/slide_animations'; import { Container, Text } from './ui'; @@ -20,8 +20,8 @@ export const Error: React.StatelessComponent<ErrorProps> = props => ( borderRadius="6px" marginBottom="10px" > - <Container marginRight="5px" display="inline"> - {props.icon} + <Container marginRight="5px" display="inline" top="3px" position="relative"> + <Text fontSize="20px">{props.icon}</Text> </Container> <Text fontWeight="500" fontColor={ColorOption.darkOrange}> {props.message} @@ -29,8 +29,16 @@ export const Error: React.StatelessComponent<ErrorProps> = props => ( </Container> ); -export const SlidingError: React.StatelessComponent<ErrorProps> = props => ( - <SlideUpAndDownAnimation downY="120px" delayMs={5000}> - <Error icon={props.icon} message={props.message} /> - </SlideUpAndDownAnimation> -); +export type SlidingDirection = 'up' | 'down'; +export interface SlidingErrorProps extends ErrorProps { + direction: SlidingDirection; +} +export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props => { + const AnimationComponent = props.direction === 'up' ? SlideUpAnimation : SlideDownAnimation; + + return ( + <AnimationComponent downY="120px"> + <Error icon={props.icon} message={props.message} /> + </AnimationComponent> + ); +}; diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index 0e6230d1b..f6472e811 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Provider } from 'react-redux'; +import { asyncData } from '../redux/async_data'; import { store } from '../redux/store'; import { fonts } from '../style/fonts'; import { theme, ThemeProvider } from '../style/theme'; @@ -8,6 +9,8 @@ import { theme, ThemeProvider } from '../style/theme'; import { ZeroExInstantContainer } from './zero_ex_instant_container'; fonts.include(); +// tslint:disable-next-line:no-floating-promises +asyncData.fetchAndDispatchToStore(); export interface ZeroExInstantProps {} diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index a384c5f1b..cf918d890 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -1,16 +1,21 @@ import * as React from 'react'; +import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details'; +import { LatestError } from '../containers/latest_error'; +import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button'; +import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; + import { ColorOption } from '../style/theme'; -import { BuyButton } from './buy_button'; -import { InstantHeading } from './instant_heading'; -import { OrderDetails } from './order_details'; import { Container, Flex } from './ui'; export interface ZeroExInstantContainerProps {} export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantContainerProps> = props => ( <Container width="350px"> + <Container zIndex={1} position="relative"> + <LatestError /> + </Container> <Container zIndex={2} position="relative" @@ -19,9 +24,9 @@ export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantConta hasBoxShadow={true} > <Flex direction="column" justify="flex-start"> - <InstantHeading /> - <OrderDetails /> - <BuyButton /> + <SelectedAssetInstantHeading /> + <LatestBuyQuoteOrderDetails /> + <SelectedAssetBuyButton /> </Flex> </Container> </Container> |