diff options
Diffstat (limited to 'packages/instant/src')
40 files changed, 761 insertions, 300 deletions
diff --git a/packages/instant/src/components/animations/position_animation.tsx b/packages/instant/src/components/animations/position_animation.tsx index 4bb21befb..8b3b294b7 100644 --- a/packages/instant/src/components/animations/position_animation.tsx +++ b/packages/instant/src/components/animations/position_animation.tsx @@ -1,5 +1,6 @@ -import { Keyframes } from 'styled-components'; +import { InterpolationValue } from 'styled-components'; +import { media, OptionallyScreenSpecific, stylesForMedia } from '../../style/media'; import { css, keyframes, styled } from '../../style/theme'; export interface TransitionInfo { @@ -51,30 +52,59 @@ export interface PositionAnimationSettings { right?: TransitionInfo; timingFunction: string; duration?: string; + position?: string; } -export interface PositionAnimationProps extends PositionAnimationSettings { - position: string; +const generatePositionAnimationCss = (positionSettings: PositionAnimationSettings) => { + return css` + animation-name: ${slideKeyframeGenerator( + positionSettings.position || 'relative', + positionSettings.top, + positionSettings.bottom, + positionSettings.left, + positionSettings.right, + )}; + animation-duration: ${positionSettings.duration || '0.3s'}; + animation-timing-function: ${positionSettings.timingFunction}; + animation-delay: 0s; + animation-iteration-count: 1; + animation-fill-mode: forwards; + position: ${positionSettings.position || 'relative'}; + width: 100%; + `; +}; + +export interface PositionAnimationProps { + positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>; + zIndex?: OptionallyScreenSpecific<number>; + height?: string; } +const defaultAnimation = (positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>) => { + const bestDefault = 'default' in positionSettings ? positionSettings.default : positionSettings; + return generatePositionAnimationCss(bestDefault); +}; +const animationForSize = ( + positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>, + sizeKey: 'sm' | 'md' | 'lg', + mediaFn: (...args: any[]) => InterpolationValue[], +) => { + // checking default makes sure we have a PositionAnimationSettings object + // and then we check to see if we have a setting for the specific `sizeKey` + const animationSettingsForSize = 'default' in positionSettings && positionSettings[sizeKey]; + return animationSettingsForSize && mediaFn`${generatePositionAnimationCss(animationSettingsForSize)}`; +}; + export const PositionAnimation = styled.div < PositionAnimationProps > ` - animation-name: ${props => - css` - ${slideKeyframeGenerator(props.position, props.top, props.bottom, props.left, props.right)}; - `}; - animation-duration: ${props => props.duration || '0.3s'}; - animation-timing-function: ${props => props.timingFunction}; - animation-delay: 0s; - animation-iteration-count: 1; - animation-fill-mode: forwards; - position: ${props => props.position}; - height: 100%; - width: 100%; + && { + ${props => props.zIndex && stylesForMedia<number>('z-index', props.zIndex)} + ${props => defaultAnimation(props.positionSettings)} + ${props => animationForSize(props.positionSettings, 'sm', media.small)} + ${props => animationForSize(props.positionSettings, 'md', media.medium)} + ${props => animationForSize(props.positionSettings, 'lg', media.large)} + ${props => (props.height ? `height: ${props.height};` : '')} + } `; - -PositionAnimation.defaultProps = { - position: 'relative', -}; diff --git a/packages/instant/src/components/animations/slide_animation.tsx b/packages/instant/src/components/animations/slide_animation.tsx index 66a314c7f..9adb1c674 100644 --- a/packages/instant/src/components/animations/slide_animation.tsx +++ b/packages/instant/src/components/animations/slide_animation.tsx @@ -1,22 +1,25 @@ import * as React from 'react'; +import { OptionallyScreenSpecific } from '../../style/media'; + import { PositionAnimation, PositionAnimationSettings } from './position_animation'; export type SlideAnimationState = 'slidIn' | 'slidOut' | 'none'; export interface SlideAnimationProps { - position: string; animationState: SlideAnimationState; - slideInSettings: PositionAnimationSettings; - slideOutSettings: PositionAnimationSettings; + slideInSettings: OptionallyScreenSpecific<PositionAnimationSettings>; + slideOutSettings: OptionallyScreenSpecific<PositionAnimationSettings>; + zIndex?: OptionallyScreenSpecific<number>; + height?: string; } export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = props => { if (props.animationState === 'none') { return <React.Fragment>{props.children}</React.Fragment>; } - const propsToUse = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings; + const positionSettings = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings; return ( - <PositionAnimation position={props.position} {...propsToUse}> + <PositionAnimation height={props.height} positionSettings={positionSettings} zIndex={props.zIndex}> {props.children} </PositionAnimation> ); diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 9a6e22ea9..877ab275c 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -12,7 +12,6 @@ import { gasPriceEstimator } from '../util/gas_price_estimator'; import { util } from '../util/util'; import { Button } from './ui/button'; -import { Text } from './ui/text'; export interface BuyButtonProps { accountAddress?: string; @@ -39,10 +38,14 @@ export class BuyButton extends React.Component<BuyButtonProps> { const { buyQuote, accountAddress } = this.props; const shouldDisableButton = _.isUndefined(buyQuote) || _.isUndefined(accountAddress); return ( - <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> - <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> - Buy - </Text> + <Button + width="100%" + onClick={this._handleClick} + isDisabled={shouldDisableButton} + fontColor={ColorOption.white} + fontSize="20px" + > + Buy </Button> ); } diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx index b46ff7714..6041bf4f5 100644 --- a/packages/instant/src/components/buy_order_state_buttons.tsx +++ b/packages/instant/src/components/buy_order_state_buttons.tsx @@ -12,7 +12,6 @@ import { SecondaryButton } from './secondary_button'; import { Button } from './ui/button'; import { Flex } from './ui/flex'; -import { Text } from './ui/text'; export interface BuyOrderStateButtonProps { accountAddress?: string; @@ -36,10 +35,8 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP if (props.buyOrderProcessingState === OrderProcessState.Failure) { return ( <Flex justify="space-between"> - <Button width="48%" onClick={props.onRetry}> - <Text fontColor={ColorOption.white} fontWeight={600} fontSize="16px"> - Back - </Text> + <Button width="48%" onClick={props.onRetry} fontColor={ColorOption.white} fontSize="16px"> + Back </Button> <SecondaryButton width="48%" onClick={props.onViewTransaction}> Details diff --git a/packages/instant/src/components/css_reset.tsx b/packages/instant/src/components/css_reset.tsx new file mode 100644 index 000000000..0bef85389 --- /dev/null +++ b/packages/instant/src/components/css_reset.tsx @@ -0,0 +1,32 @@ +import { INJECTED_DIV_CLASS } from '../constants'; +import { createGlobalStyle } from '../style/theme'; + +export interface CSSResetProps {} + +/* +* Derived from +* https://github.com/jtrost/Complete-CSS-Reset +*/ +export const CSSReset = createGlobalStyle` + .${INJECTED_DIV_CLASS} { + a, abbr, area, article, aside, audio, b, bdo, blockquote, body, button, + canvas, caption, cite, code, col, colgroup, command, datalist, dd, del, + details, dialog, dfn, div, dl, dt, em, embed, fieldset, figure, form, + h1, h2, h3, h4, h5, h6, head, header, hgroup, hr, html, i, iframe, img, + input, ins, keygen, kbd, label, legend, li, map, mark, menu, meter, nav, + noscript, object, ol, optgroup, option, output, p, param, pre, progress, + q, rp, rt, ruby, samp, section, select, small, span, strong, sub, sup, + table, tbody, td, textarea, tfoot, th, thead, time, tr, ul, var, video { + background: transparent; + border: 0; + font-size: 100%; + font: inherit; + margin: 0; + outline: none; + padding: 0; + text-align: left; + text-decoration: none; + vertical-align: baseline; + } + } +`; diff --git a/packages/instant/src/components/erc20_asset_amount_input.tsx b/packages/instant/src/components/erc20_asset_amount_input.tsx index f21c21b87..520ac33d5 100644 --- a/packages/instant/src/components/erc20_asset_amount_input.tsx +++ b/packages/instant/src/components/erc20_asset_amount_input.tsx @@ -113,7 +113,7 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput } return ( <Container marginLeft="5px"> - <Icon icon="chevron" width={12} onClick={this._handleSelectAssetClick} /> + <Icon icon="chevron" width={12} stroke={ColorOption.white} onClick={this._handleSelectAssetClick} /> </Container> ); }; diff --git a/packages/instant/src/components/erc20_token_selector.tsx b/packages/instant/src/components/erc20_token_selector.tsx index 76d5c66ff..3503ff31a 100644 --- a/packages/instant/src/components/erc20_token_selector.tsx +++ b/packages/instant/src/components/erc20_token_selector.tsx @@ -28,14 +28,14 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps> public render(): React.ReactNode { const { tokens, onTokenSelect } = this.props; return ( - <Container> + <Container height="100%"> <SearchInput placeholder="Search tokens..." width="100%" value={this.state.searchQuery} onChange={this._handleSearchInputChange} /> - <Container overflow="scroll" height={{ default: '275px', sm: '75vh' }} marginTop="10px"> + <Container overflow="scroll" height="calc(100% - 80px)" marginTop="10px"> {_.map(tokens, token => { if (!this._isTokenQueryMatch(token)) { return null; @@ -85,7 +85,7 @@ class TokenSelectorRow extends React.Component<TokenSelectorRowProps> { <Container marginLeft="5px"> <Flex justify="flex-start"> <Container marginRight="10px"> - <Circle diameter={30} fillColor={token.metaData.primaryColor}> + <Circle diameter={30} rawColor={token.metaData.primaryColor}> <Flex height="100%"> <Text fontColor={ColorOption.white} fontSize="8px"> {displaySymbol} diff --git a/packages/instant/src/components/payment_method.tsx b/packages/instant/src/components/payment_method.tsx new file mode 100644 index 000000000..8c0b47d72 --- /dev/null +++ b/packages/instant/src/components/payment_method.tsx @@ -0,0 +1,45 @@ +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; +import { Network } from '../types'; + +import { PaymentMethodDropdown } from './payment_method_dropdown'; +import { Circle } from './ui/circle'; +import { Container } from './ui/container'; +import { Flex } from './ui/flex'; +import { Text } from './ui/text'; + +export interface PaymentMethodProps {} + +export const PaymentMethod: React.StatelessComponent<PaymentMethodProps> = () => ( + <Container padding="20px" width="100%"> + <Container marginBottom="10px"> + <Flex justify="space-between"> + <Text + letterSpacing="1px" + fontColor={ColorOption.primaryColor} + fontWeight={600} + textTransform="uppercase" + fontSize="14px" + > + Payment Method + </Text> + <Flex> + <Circle color={ColorOption.green} diameter={8} /> + <Container marginLeft="3px"> + <Text fontColor={ColorOption.darkGrey} fontSize="12px"> + MetaMask + </Text> + </Container> + </Flex> + </Flex> + </Container> + <PaymentMethodDropdown + accountAddress="0xa1b2c3d4e5f6g7h8j9k10" + accountEthBalanceInWei={new BigNumber(10500000000000000000)} + network={Network.Mainnet} + /> + </Container> +); diff --git a/packages/instant/src/components/payment_method_dropdown.tsx b/packages/instant/src/components/payment_method_dropdown.tsx new file mode 100644 index 000000000..bdce2a49d --- /dev/null +++ b/packages/instant/src/components/payment_method_dropdown.tsx @@ -0,0 +1,44 @@ +import { BigNumber } from '@0x/utils'; +import copy from 'copy-to-clipboard'; +import * as React from 'react'; + +import { Network } from '../types'; +import { etherscanUtil } from '../util/etherscan'; +import { format } from '../util/format'; + +import { Dropdown, DropdownItemConfig } from './ui/dropdown'; + +export interface PaymentMethodDropdownProps { + accountAddress: string; + accountEthBalanceInWei?: BigNumber; + network: Network; +} + +export class PaymentMethodDropdown extends React.Component<PaymentMethodDropdownProps> { + public render(): React.ReactNode { + const { accountAddress, accountEthBalanceInWei } = this.props; + const value = format.ethAddress(accountAddress); + const label = format.ethBaseAmount(accountEthBalanceInWei, 4, '') as string; + return <Dropdown value={value} label={label} items={this._getDropdownItemConfigs()} />; + } + private readonly _getDropdownItemConfigs = (): DropdownItemConfig[] => { + const viewOnEtherscan = { + text: 'View on Etherscan', + onClick: this._handleEtherscanClick, + }; + const copyAddressToClipboard = { + text: 'Copy address to clipboard', + onClick: this._handleCopyToClipboardClick, + }; + return [viewOnEtherscan, copyAddressToClipboard]; + }; + private readonly _handleEtherscanClick = (): void => { + const { accountAddress, network } = this.props; + const etherscanUrl = etherscanUtil.getEtherScanEthAddressIfExists(accountAddress, network); + window.open(etherscanUrl, '_blank'); + }; + private readonly _handleCopyToClipboardClick = (): void => { + const { accountAddress } = this.props; + copy(accountAddress); + }; +} diff --git a/packages/instant/src/components/placing_order_button.tsx b/packages/instant/src/components/placing_order_button.tsx index 4232e6c22..d774d7d27 100644 --- a/packages/instant/src/components/placing_order_button.tsx +++ b/packages/instant/src/components/placing_order_button.tsx @@ -5,15 +5,12 @@ import { ColorOption } from '../style/theme'; import { Button } from './ui/button'; import { Container } from './ui/container'; import { Spinner } from './ui/spinner'; -import { Text } from './ui/text'; export const PlacingOrderButton: React.StatelessComponent<{}> = props => ( - <Button isDisabled={true} width="100%"> + <Button isDisabled={true} width="100%" fontColor={ColorOption.white} fontSize="20px"> <Container display="inline-block" position="relative" top="3px" marginRight="8px"> <Spinner widthPx={20} heightPx={20} /> </Container> - <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> - Placing Order… - </Text> + Placing Order… </Button> ); diff --git a/packages/instant/src/components/secondary_button.tsx b/packages/instant/src/components/secondary_button.tsx index 583058b5b..df0539606 100644 --- a/packages/instant/src/components/secondary_button.tsx +++ b/packages/instant/src/components/secondary_button.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { ColorOption } from '../style/theme'; import { Button, ButtonProps } from './ui/button'; -import { Text } from './ui/text'; export interface SecondaryButtonProps extends ButtonProps {} @@ -16,11 +15,11 @@ export const SecondaryButton: React.StatelessComponent<SecondaryButtonProps> = p borderColor={ColorOption.lightGrey} width={props.width} onClick={props.onClick} + fontColor={ColorOption.primaryColor} + fontSize="16px" {...buttonProps} > - <Text fontColor={ColorOption.primaryColor} fontWeight={600} fontSize="16px"> - {props.children} - </Text> + {props.children} </Button> ); }; diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx index a923b9932..a8d4e391c 100644 --- a/packages/instant/src/components/sliding_error.tsx +++ b/packages/instant/src/components/sliding_error.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; +import { ScreenSpecification } from '../style/media'; import { ColorOption } from '../style/theme'; +import { zIndex } from '../style/z_index'; import { PositionAnimationSettings } from './animations/position_animation'; import { SlideAnimation, SlideAnimationState } from './animations/slide_animation'; @@ -21,6 +23,7 @@ export const Error: React.StatelessComponent<ErrorProps> = props => ( backgroundColor={ColorOption.lightOrange} width="100%" borderRadius="6px" + marginTop="10px" marginBottom="10px" > <Flex justify="flex-start"> @@ -39,25 +42,51 @@ export interface SlidingErrorProps extends ErrorProps { } export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props => { const slideAmount = '120px'; - const slideUpSettings: PositionAnimationSettings = { + + const desktopSlideIn: PositionAnimationSettings = { timingFunction: 'ease-in', top: { from: slideAmount, to: '0px', }, + position: 'relative', }; - const slideDownSettings: PositionAnimationSettings = { + const desktopSlideOut: PositionAnimationSettings = { timingFunction: 'cubic-bezier(0.25, 0.1, 0.25, 1)', top: { from: '0px', to: slideAmount, }, + position: 'relative', + }; + + const mobileSlideIn: PositionAnimationSettings = { + duration: '0.5s', + timingFunction: 'ease-in', + top: { from: '-120px', to: '0px' }, + position: 'fixed', + }; + const moblieSlideOut: PositionAnimationSettings = { + duration: '0.5s', + timingFunction: 'ease-in', + top: { from: '0px', to: '-120px' }, + position: 'fixed', + }; + + const slideUpSettings: ScreenSpecification<PositionAnimationSettings> = { + default: desktopSlideIn, + sm: mobileSlideIn, }; + const slideOutSettings: ScreenSpecification<PositionAnimationSettings> = { + default: desktopSlideOut, + sm: moblieSlideOut, + }; + return ( <SlideAnimation - position="relative" slideInSettings={slideUpSettings} - slideOutSettings={slideDownSettings} + slideOutSettings={slideOutSettings} + zIndex={{ sm: zIndex.errorPopup, default: zIndex.errorPopBehind }} animationState={props.animationState} > <Error icon={props.icon} message={props.message} /> diff --git a/packages/instant/src/components/sliding_panel.tsx b/packages/instant/src/components/sliding_panel.tsx index 7ef28e09c..9d16f9560 100644 --- a/packages/instant/src/components/sliding_panel.tsx +++ b/packages/instant/src/components/sliding_panel.tsx @@ -30,7 +30,9 @@ export const Panel: React.StatelessComponent<PanelProps> = ({ title, children, o <Icon width={12} color={ColorOption.lightGrey} icon="closeX" onClick={onClose} /> </Container> </Flex> - <Container marginTop="10px">{children}</Container> + <Container marginTop="10px" height="100%"> + {children} + </Container> </Container> ); @@ -51,6 +53,7 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props = from: slideAmount, to: '0px', }, + position: 'absolute', }; const slideDownSettings: PositionAnimationSettings = { duration: '0.3s', @@ -59,13 +62,14 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props = from: '0px', to: slideAmount, }, + position: 'absolute', }; return ( <SlideAnimation - position="absolute" slideInSettings={slideUpSettings} slideOutSettings={slideDownSettings} animationState={animationState} + height="100%" > <Panel {...rest} /> </SlideAnimation> diff --git a/packages/instant/src/components/timed_progress_bar.tsx b/packages/instant/src/components/timed_progress_bar.tsx index f2a6f5745..59aaa33a1 100644 --- a/packages/instant/src/components/timed_progress_bar.tsx +++ b/packages/instant/src/components/timed_progress_bar.tsx @@ -70,9 +70,11 @@ export const TimedProgress = styled.div < TimedProgressProps > ` - background-color: ${props => props.theme[ColorOption.primaryColor]}; - border-radius: 6px; - height: 6px; - animation: ${props => expandingWidthKeyframes(props.fromWidth, props.toWidth)} - ${props => props.timeMs}ms linear 1 forwards; - `; + && { + background-color: ${props => props.theme[ColorOption.primaryColor]}; + border-radius: 6px; + height: 6px; + animation: ${props => expandingWidthKeyframes(props.fromWidth, props.toWidth)} + ${props => props.timeMs}ms linear 1 forwards; + } +`; diff --git a/packages/instant/src/components/ui/button.tsx b/packages/instant/src/components/ui/button.tsx index 5274d835b..b90221bf4 100644 --- a/packages/instant/src/components/ui/button.tsx +++ b/packages/instant/src/components/ui/button.tsx @@ -6,6 +6,8 @@ import { ColorOption, styled } from '../../style/theme'; export interface ButtonProps { backgroundColor?: ColorOption; borderColor?: ColorOption; + fontColor?: ColorOption; + fontSize?: string; width?: string; padding?: string; type?: string; @@ -24,29 +26,39 @@ const darkenOnHoverAmount = 0.1; const darkenOnActiveAmount = 0.2; const saturateOnFocusAmount = 0.2; export const Button = styled(PlainButton)` - cursor: ${props => (props.isDisabled ? 'default' : 'pointer')}; - transition: background-color, opacity 0.5s ease; - padding: ${props => props.padding}; - border-radius: 3px; - outline: none; - width: ${props => props.width}; - background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; - border: ${props => (props.borderColor ? `1px solid ${props.theme[props.borderColor]}` : 'none')}; - &:hover { - background-color: ${props => - !props.isDisabled - ? darken(darkenOnHoverAmount, props.theme[props.backgroundColor || 'white']) - : ''} !important; - } - &:active { - background-color: ${props => - !props.isDisabled ? darken(darkenOnActiveAmount, props.theme[props.backgroundColor || 'white']) : ''}; - } - &:disabled { - opacity: 0.5; - } - &:focus { - background-color: ${props => saturate(saturateOnFocusAmount, props.theme[props.backgroundColor || 'white'])}; + && { + all: initial; + box-sizing: border-box; + font-size: ${props => props.fontSize}; + font-family: 'Inter UI', sans-serif; + font-weight: 600; + color: ${props => props.fontColor && props.theme[props.fontColor]}; + cursor: ${props => (props.isDisabled ? 'default' : 'pointer')}; + transition: background-color, opacity 0.5s ease; + padding: ${props => props.padding}; + border-radius: 3px; + text-align: center; + outline: none; + width: ${props => props.width}; + background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; + border: ${props => (props.borderColor ? `1px solid ${props.theme[props.borderColor]}` : 'none')}; + &:hover { + background-color: ${props => + !props.isDisabled + ? darken(darkenOnHoverAmount, props.theme[props.backgroundColor || 'white']) + : ''} !important; + } + &:active { + background-color: ${props => + !props.isDisabled ? darken(darkenOnActiveAmount, props.theme[props.backgroundColor || 'white']) : ''}; + } + &:disabled { + opacity: 0.5; + } + &:focus { + background-color: ${props => + saturate(saturateOnFocusAmount, props.theme[props.backgroundColor || 'white'])}; + } } `; @@ -55,7 +67,8 @@ Button.defaultProps = { borderColor: ColorOption.primaryColor, width: 'auto', isDisabled: false, - padding: '1em 2.2em', + padding: '.6em 1.2em', + fontSize: '15px', }; Button.displayName = 'Button'; diff --git a/packages/instant/src/components/ui/circle.tsx b/packages/instant/src/components/ui/circle.tsx index eec2777d2..4f9f56f12 100644 --- a/packages/instant/src/components/ui/circle.tsx +++ b/packages/instant/src/components/ui/circle.tsx @@ -1,22 +1,27 @@ -import { styled } from '../../style/theme'; +import { ColorOption, styled, Theme, withTheme } from '../../style/theme'; export interface CircleProps { diameter: number; - fillColor?: string; + rawColor?: string; + color?: ColorOption; + theme: Theme; } -export const Circle = +export const Circle = withTheme( styled.div < - CircleProps > - ` - width: ${props => props.diameter}px; - height: ${props => props.diameter}px; - background-color: ${props => props.fillColor}; - border-radius: 50%; -`; + CircleProps > + ` + && { + width: ${props => props.diameter}px; + height: ${props => props.diameter}px; + background-color: ${props => (props.rawColor ? props.rawColor : props.theme[props.color || ColorOption.white])}; + border-radius: 50%; + } +`, +); Circle.displayName = 'Circle'; Circle.defaultProps = { - fillColor: 'white', + color: ColorOption.white, }; diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index 403751210..8aa5db9e5 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -34,6 +34,7 @@ export interface ContainerProps { cursor?: string; overflow?: string; darkenOnHover?: boolean; + boxShadowOnHover?: boolean; flexGrow?: string | number; } @@ -41,42 +42,45 @@ export const Container = styled.div < ContainerProps > ` - box-sizing: border-box; - ${props => cssRuleIfExists(props, 'flex-grow')} - ${props => cssRuleIfExists(props, 'position')} - ${props => cssRuleIfExists(props, 'top')} - ${props => cssRuleIfExists(props, 'right')} - ${props => cssRuleIfExists(props, 'bottom')} - ${props => cssRuleIfExists(props, 'left')} - ${props => cssRuleIfExists(props, 'max-width')} - ${props => cssRuleIfExists(props, 'margin')} - ${props => cssRuleIfExists(props, 'margin-top')} - ${props => cssRuleIfExists(props, 'margin-right')} - ${props => cssRuleIfExists(props, 'margin-bottom')} - ${props => cssRuleIfExists(props, 'margin-left')} - ${props => cssRuleIfExists(props, 'padding')} - ${props => cssRuleIfExists(props, 'border-radius')} - ${props => cssRuleIfExists(props, 'border')} - ${props => cssRuleIfExists(props, 'border-top')} - ${props => cssRuleIfExists(props, 'border-bottom')} - ${props => cssRuleIfExists(props, 'z-index')} - ${props => cssRuleIfExists(props, 'white-space')} - ${props => cssRuleIfExists(props, 'opacity')} - ${props => cssRuleIfExists(props, 'cursor')} - ${props => cssRuleIfExists(props, 'overflow')} - ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; - ${props => props.display && stylesForMedia('display', props.display)} - ${props => stylesForMedia('width', props.width || 'auto')} - ${props => stylesForMedia('height', props.height || 'auto')} - background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; - border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')}; - &:hover { - ${props => - props.darkenOnHover - ? `background-color: ${ - props.backgroundColor ? darken(0.05, props.theme[props.backgroundColor]) : 'none' - }` - : ''}; + && { + box-sizing: border-box; + ${props => cssRuleIfExists(props, 'flex-grow')} + ${props => cssRuleIfExists(props, 'position')} + ${props => cssRuleIfExists(props, 'top')} + ${props => cssRuleIfExists(props, 'right')} + ${props => cssRuleIfExists(props, 'bottom')} + ${props => cssRuleIfExists(props, 'left')} + ${props => cssRuleIfExists(props, 'max-width')} + ${props => cssRuleIfExists(props, 'margin')} + ${props => cssRuleIfExists(props, 'margin-top')} + ${props => cssRuleIfExists(props, 'margin-right')} + ${props => cssRuleIfExists(props, 'margin-bottom')} + ${props => cssRuleIfExists(props, 'margin-left')} + ${props => cssRuleIfExists(props, 'padding')} + ${props => cssRuleIfExists(props, 'border-radius')} + ${props => cssRuleIfExists(props, 'border')} + ${props => cssRuleIfExists(props, 'border-top')} + ${props => cssRuleIfExists(props, 'border-bottom')} + ${props => cssRuleIfExists(props, 'z-index')} + ${props => cssRuleIfExists(props, 'white-space')} + ${props => cssRuleIfExists(props, 'opacity')} + ${props => cssRuleIfExists(props, 'cursor')} + ${props => cssRuleIfExists(props, 'overflow')} + ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; + ${props => props.display && stylesForMedia<string>('display', props.display)} + ${props => props.width && stylesForMedia<string>('width', props.width)} + ${props => props.height && stylesForMedia<string>('height', props.height)} + background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; + border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')}; + &:hover { + ${props => + props.darkenOnHover + ? `background-color: ${ + props.backgroundColor ? darken(0.05, props.theme[props.backgroundColor]) : 'none' + }` + : ''}; + ${props => (props.boxShadowOnHover ? 'box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)' : '')}; + } } `; diff --git a/packages/instant/src/components/ui/dropdown.tsx b/packages/instant/src/components/ui/dropdown.tsx new file mode 100644 index 000000000..3a23f456d --- /dev/null +++ b/packages/instant/src/components/ui/dropdown.tsx @@ -0,0 +1,134 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption, completelyTransparent } from '../../style/theme'; +import { zIndex } from '../../style/z_index'; + +import { Container } from './container'; +import { Flex } from './flex'; +import { Icon } from './icon'; +import { Overlay } from './overlay'; +import { Text } from './text'; + +export interface DropdownItemConfig { + text: string; + onClick?: () => void; +} + +export interface DropdownProps { + value: string; + label?: string; + items: DropdownItemConfig[]; +} + +export interface DropdownState { + isOpen: boolean; +} + +export class Dropdown extends React.Component<DropdownProps, DropdownState> { + public static defaultProps = { + items: [], + }; + public state: DropdownState = { + isOpen: false, + }; + public render(): React.ReactNode { + const { value, label, items } = this.props; + const { isOpen } = this.state; + const hasItems = !_.isEmpty(items); + const borderRadius = isOpen ? '4px 4px 0px 0px' : '4px'; + return ( + <React.Fragment> + {isOpen && ( + <Overlay + zIndex={zIndex.dropdownItems - 1} + backgroundColor={completelyTransparent} + onClick={this._closeDropdown} + /> + )} + <Container position="relative"> + <Container + cursor={hasItems ? 'pointer' : undefined} + onClick={this._handleDropdownClick} + hasBoxShadow={isOpen} + boxShadowOnHover={true} + borderRadius={borderRadius} + border="1px solid" + borderColor={ColorOption.feintGrey} + padding="0.8em" + > + <Flex justify="space-between"> + <Text fontSize="16px" fontColor={ColorOption.darkGrey}> + {value} + </Text> + <Container> + {label && ( + <Text fontSize="16px" fontColor={ColorOption.lightGrey}> + {label} + </Text> + )} + {hasItems && ( + <Container marginLeft="5px" display="inline-block" position="relative" bottom="2px"> + <Icon padding="3px" icon="chevron" width={12} stroke={ColorOption.grey} /> + </Container> + )} + </Container> + </Flex> + </Container> + {isOpen && ( + <Container + width="100%" + position="absolute" + onClick={this._closeDropdown} + backgroundColor={ColorOption.white} + hasBoxShadow={true} + zIndex={zIndex.dropdownItems} + > + {_.map(items, (item, index) => ( + <DropdownItem key={item.text} {...item} isLast={index === items.length - 1} /> + ))} + </Container> + )} + </Container> + </React.Fragment> + ); + } + private readonly _handleDropdownClick = (): void => { + if (_.isEmpty(this.props.items)) { + return; + } + this.setState({ + isOpen: !this.state.isOpen, + }); + }; + private readonly _closeDropdown = (): void => { + this.setState({ + isOpen: false, + }); + }; +} + +export interface DropdownItemProps extends DropdownItemConfig { + text: string; + onClick?: () => void; + isLast: boolean; +} + +export const DropdownItem: React.StatelessComponent<DropdownItemProps> = ({ text, onClick, isLast }) => ( + <Container + onClick={onClick} + cursor="pointer" + darkenOnHover={true} + backgroundColor={ColorOption.white} + padding="0.8em" + borderTop="0" + border="1px solid" + borderRadius={isLast ? '0px 0px 4px 4px' : undefined} + width="100%" + borderColor={ColorOption.feintGrey} + > + <Text fontSize="14px" fontColor={ColorOption.darkGrey}> + {text} + </Text> + </Container> +); diff --git a/packages/instant/src/components/ui/flex.tsx b/packages/instant/src/components/ui/flex.tsx index fd218b0cd..274c46b9e 100644 --- a/packages/instant/src/components/ui/flex.tsx +++ b/packages/instant/src/components/ui/flex.tsx @@ -18,15 +18,17 @@ export const Flex = styled.div < FlexProps > ` - display: ${props => (props.inline ? 'inline-flex' : 'flex')}; - flex-direction: ${props => props.direction}; - flex-wrap: ${props => props.flexWrap}; - ${props => cssRuleIfExists(props, 'flexGrow')} - justify-content: ${props => props.justify}; - align-items: ${props => props.align}; - background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; - ${props => stylesForMedia('width', props.width || 'auto')} - ${props => stylesForMedia('height', props.height || 'auto')} + && { + display: ${props => (props.inline ? 'inline-flex' : 'flex')}; + flex-direction: ${props => props.direction}; + flex-wrap: ${props => props.flexWrap}; + ${props => cssRuleIfExists(props, 'flexGrow')} + justify-content: ${props => props.justify}; + align-items: ${props => props.align}; + background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; + ${props => (props.width ? stylesForMedia('width', props.width) : '')} + ${props => (props.height ? stylesForMedia('height', props.height) : '')} + } `; Flex.defaultProps = { diff --git a/packages/instant/src/components/ui/icon.tsx b/packages/instant/src/components/ui/icon.tsx index 94ea26900..a88fa87dd 100644 --- a/packages/instant/src/components/ui/icon.tsx +++ b/packages/instant/src/components/ui/icon.tsx @@ -9,7 +9,6 @@ interface IconInfo { path: string; fillRule?: svgRule; clipRule?: svgRule; - stroke?: string; strokeOpacity?: number; strokeWidth?: number; strokeLinecap?: 'butt' | 'round' | 'square' | 'inherit'; @@ -47,7 +46,6 @@ const ICONS: IconInfoMapping = { chevron: { viewBox: '0 0 12 7', path: 'M11 1L6 6L1 1', - stroke: 'white', strokeOpacity: 0.5, strokeWidth: 1.5, strokeLinecap: 'round', @@ -67,6 +65,7 @@ export interface IconProps { width: number; height?: number; color?: ColorOption; + stroke?: ColorOption; icon: keyof IconInfoMapping; onClick?: (event: React.MouseEvent<HTMLElement>) => void; padding?: string; @@ -75,6 +74,7 @@ export interface IconProps { const PlainIcon: React.StatelessComponent<IconProps> = props => { const iconInfo = ICONS[props.icon]; const colorValue = _.isUndefined(props.color) ? undefined : props.theme[props.color]; + const strokeValue = _.isUndefined(props.stroke) ? undefined : props.theme[props.stroke]; return ( <div onClick={props.onClick} className={props.className}> <svg @@ -89,7 +89,7 @@ const PlainIcon: React.StatelessComponent<IconProps> = props => { fill={colorValue} fillRule={iconInfo.fillRule || 'nonzero'} clipRule={iconInfo.clipRule || 'nonzero'} - stroke={iconInfo.stroke} + stroke={strokeValue} strokeOpacity={iconInfo.strokeOpacity} strokeWidth={iconInfo.strokeWidth} strokeLinecap={iconInfo.strokeLinecap} @@ -101,15 +101,18 @@ const PlainIcon: React.StatelessComponent<IconProps> = props => { }; export const Icon = withTheme(styled(PlainIcon)` - cursor: ${props => (!_.isUndefined(props.onClick) ? 'pointer' : 'default')}; - transition: opacity 0.5s ease; - padding: ${props => props.padding}; - opacity: ${props => (!_.isUndefined(props.onClick) ? 0.7 : 1)}; - &:hover { - opacity: 1; - } - &:active { - opacity: 1; + && { + display: inline-block; + ${props => (!_.isUndefined(props.onClick) ? 'cursor: pointer' : '')}; + transition: opacity 0.5s ease; + padding: ${props => props.padding}; + opacity: ${props => (!_.isUndefined(props.onClick) ? 0.7 : 1)}; + &:hover { + opacity: 1; + } + &:active { + opacity: 1; + } } `); diff --git a/packages/instant/src/components/ui/input.tsx b/packages/instant/src/components/ui/input.tsx index a884ff7cb..2fb408db4 100644 --- a/packages/instant/src/components/ui/input.tsx +++ b/packages/instant/src/components/ui/input.tsx @@ -16,17 +16,20 @@ export const Input = styled.input < InputProps > ` - font-size: ${props => props.fontSize}; - width: ${props => props.width}; - padding: 0.1em 0em; - font-family: 'Inter UI'; - color: ${props => props.theme[props.fontColor || 'white']}; - background: transparent; - outline: none; - border: none; - &::placeholder { + && { + all: initial; + font-size: ${props => props.fontSize}; + width: ${props => props.width}; + padding: 0.1em 0em; + font-family: 'Inter UI'; color: ${props => props.theme[props.fontColor || 'white']}; - opacity: 0.5; + background: transparent; + outline: none; + border: none; + &::placeholder { + color: ${props => props.theme[props.fontColor || 'white']}; + opacity: 0.5; + } } `; diff --git a/packages/instant/src/components/ui/overlay.tsx b/packages/instant/src/components/ui/overlay.tsx index 7110ee70f..c5f55f9c0 100644 --- a/packages/instant/src/components/ui/overlay.tsx +++ b/packages/instant/src/components/ui/overlay.tsx @@ -1,40 +1,31 @@ import * as _ from 'lodash'; -import * as React from 'react'; -import { ColorOption, overlayBlack, styled } from '../../style/theme'; - -import { Container } from './container'; -import { Flex } from './flex'; -import { Icon } from './icon'; +import { overlayBlack, styled } from '../../style/theme'; +import { zIndex } from '../../style/z_index'; export interface OverlayProps { - className?: string; - onClose?: () => void; zIndex?: number; + backgroundColor?: string; } -const PlainOverlay: React.StatelessComponent<OverlayProps> = ({ children, className, onClose }) => ( - <Flex height="100vh" className={className}> - <Container position="absolute" top="0px" right="0px" display={{ default: 'initial', sm: 'none' }}> - <Icon height={18} width={18} color={ColorOption.white} icon="closeX" onClick={onClose} padding="2em 2em" /> - </Container> - <Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}> - {children} - </Container> - </Flex> -); -export const Overlay = styled(PlainOverlay)` - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: ${props => props.zIndex} - background-color: ${overlayBlack}; +export const Overlay = + styled.div < + OverlayProps > + ` + && { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: ${props => props.zIndex} + background-color: ${props => props.backgroundColor}; + } `; Overlay.defaultProps = { - zIndex: 100, + zIndex: zIndex.overlayDefault, + backgroundColor: overlayBlack, }; Overlay.displayName = 'Overlay'; diff --git a/packages/instant/src/components/ui/text.tsx b/packages/instant/src/components/ui/text.tsx index cba6e7b20..4fe429d25 100644 --- a/packages/instant/src/components/ui/text.tsx +++ b/packages/instant/src/components/ui/text.tsx @@ -27,25 +27,27 @@ export const Text = styled.div < TextProps > ` - font-family: ${props => props.fontFamily}; - font-style: ${props => props.fontStyle}; - font-weight: ${props => props.fontWeight}; - font-size: ${props => props.fontSize}; - opacity: ${props => props.opacity}; - text-decoration-line: ${props => props.textDecorationLine}; - ${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')}; - ${props => (props.center ? 'text-align: center' : '')}; - color: ${props => props.fontColor && props.theme[props.fontColor]}; - ${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')}; - ${props => (props.onClick ? 'cursor: pointer' : '')}; - transition: color 0.5s ease; - ${props => (props.noWrap ? 'white-space: nowrap' : '')}; - ${props => (props.display ? `display: ${props.display}` : '')}; - ${props => (props.letterSpacing ? `letter-spacing: ${props.letterSpacing}` : '')}; - ${props => (props.textTransform ? `text-transform: ${props.textTransform}` : '')}; - &:hover { - ${props => - props.onClick ? `color: ${darken(darkenOnHoverAmount, props.theme[props.fontColor || 'white'])}` : ''}; + && { + font-family: 'Inter UI', sans-serif; + font-style: ${props => props.fontStyle}; + font-weight: ${props => props.fontWeight}; + font-size: ${props => props.fontSize}; + opacity: ${props => props.opacity}; + text-decoration-line: ${props => props.textDecorationLine}; + ${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')}; + ${props => (props.center ? 'text-align: center' : '')}; + color: ${props => props.fontColor && props.theme[props.fontColor]}; + ${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')}; + ${props => (props.onClick ? 'cursor: pointer' : '')}; + transition: color 0.5s ease; + ${props => (props.noWrap ? 'white-space: nowrap' : '')}; + ${props => (props.display ? `display: ${props.display}` : '')}; + ${props => (props.letterSpacing ? `letter-spacing: ${props.letterSpacing}` : '')}; + ${props => (props.textTransform ? `text-transform: ${props.textTransform}` : '')}; + &:hover { + ${props => + props.onClick ? `color: ${darken(darkenOnHoverAmount, props.theme[props.fontColor || 'white'])}` : ''}; + } } `; @@ -61,14 +63,3 @@ Text.defaultProps = { }; Text.displayName = 'Text'; - -export const Title: React.StatelessComponent<TextProps> = props => <Text {...props} />; - -Title.defaultProps = { - fontSize: '20px', - fontWeight: 600, - opacity: 1, - fontColor: ColorOption.primaryColor, -}; - -Title.displayName = 'Title'; diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index 907c42e7a..b945f9908 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; +import { INJECTED_DIV_CLASS } from '../constants'; + import { ZeroExInstantContainer } from './zero_ex_instant_container'; import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider'; @@ -7,8 +9,10 @@ export type ZeroExInstantProps = ZeroExInstantProviderProps; export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = props => { return ( - <ZeroExInstantProvider {...props}> - <ZeroExInstantContainer /> - </ZeroExInstantProvider> + <div className={INJECTED_DIV_CLASS}> + <ZeroExInstantProvider {...props}> + <ZeroExInstantContainer /> + </ZeroExInstantProvider> + </div> ); }; diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index ef6adf384..5748e064e 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -3,15 +3,14 @@ import * as React from 'react'; import { AvailableERC20TokenSelector } from '../containers/available_erc20_token_selector'; import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details'; import { LatestError } from '../containers/latest_error'; +import { SelectedAssetBuyOrderProgress } from '../containers/selected_asset_buy_order_progress'; import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_buy_order_state_buttons'; import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; - -import { SelectedAssetBuyOrderProgress } from '../containers/selected_asset_buy_order_progress'; - import { ColorOption } from '../style/theme'; import { zIndex } from '../style/z_index'; import { SlideAnimationState } from './animations/slide_animation'; +import { CSSReset } from './css_reset'; import { SlidingPanel } from './sliding_panel'; import { Container } from './ui/container'; import { Flex } from './ui/flex'; @@ -27,40 +26,43 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain }; public render(): React.ReactNode { return ( - <Container - width={{ default: '350px', sm: '100%' }} - height={{ default: 'auto', sm: '100%' }} - position="relative" - > - <Container zIndex={zIndex.errorPopup} position="relative"> - <LatestError /> - </Container> + <React.Fragment> + <CSSReset /> <Container - zIndex={zIndex.mainContainer} + width={{ default: '350px', sm: '100%' }} + height={{ default: 'auto', sm: '100%' }} position="relative" - backgroundColor={ColorOption.white} - borderRadius="3px" - hasBoxShadow={true} - overflow="hidden" - height="100%" > - <Flex direction="column" height="100%" justify="flex-start"> - <SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} /> - <SelectedAssetBuyOrderProgress /> - <LatestBuyQuoteOrderDetails /> - <Container padding="20px" width="100%"> - <SelectedAssetBuyOrderStateButtons /> - </Container> - </Flex> - <SlidingPanel - title="Select Token" - animationState={this.state.tokenSelectionPanelAnimationState} - onClose={this._handlePanelClose} + <Container position="relative"> + <LatestError /> + </Container> + <Container + zIndex={zIndex.mainContainer} + position="relative" + backgroundColor={ColorOption.white} + borderRadius="3px" + hasBoxShadow={true} + overflow="hidden" + height="100%" > - <AvailableERC20TokenSelector onTokenSelect={this._handlePanelClose} /> - </SlidingPanel> + <Flex direction="column" justify="flex-start" height="100%"> + <SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} /> + <SelectedAssetBuyOrderProgress /> + <LatestBuyQuoteOrderDetails /> + <Container padding="20px" width="100%"> + <SelectedAssetBuyOrderStateButtons /> + </Container> + </Flex> + <SlidingPanel + title="Select Token" + animationState={this.state.tokenSelectionPanelAnimationState} + onClose={this._handlePanelClose} + > + <AvailableERC20TokenSelector onTokenSelect={this._handlePanelClose} /> + </SlidingPanel> + </Container> </Container> - </Container> + </React.Fragment> ); } private readonly _handleSymbolClick = (): void => { diff --git a/packages/instant/src/components/zero_ex_instant_overlay.tsx b/packages/instant/src/components/zero_ex_instant_overlay.tsx index 3461600e1..10438ab7a 100644 --- a/packages/instant/src/components/zero_ex_instant_overlay.tsx +++ b/packages/instant/src/components/zero_ex_instant_overlay.tsx @@ -1,5 +1,10 @@ import * as React from 'react'; +import { ColorOption } from '../style/theme'; + +import { Container } from './ui/container'; +import { Flex } from './ui/flex'; +import { Icon } from './ui/icon'; import { Overlay } from './ui/overlay'; import { ZeroExInstantContainer } from './zero_ex_instant_container'; import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider'; @@ -13,8 +18,22 @@ export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlay const { onClose, zIndex, ...rest } = props; return ( <ZeroExInstantProvider {...rest}> - <Overlay onClose={onClose} zIndex={zIndex}> - <ZeroExInstantContainer /> + <Overlay zIndex={zIndex}> + <Flex height="100vh"> + <Container position="absolute" top="0px" right="0px" display={{ default: 'initial', sm: 'none' }}> + <Icon + height={18} + width={18} + color={ColorOption.white} + icon="closeX" + onClick={onClose} + padding="2em 2em" + /> + </Container> + <Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}> + <ZeroExInstantContainer /> + </Container> + </Flex> </Overlay> </ZeroExInstantProvider> ); diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index dfbd0ccdf..37320e21d 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -5,6 +5,7 @@ import { AccountNotReady, AccountState, Network } from './types'; export const BIG_NUMBER_ZERO = new BigNumber(0); export const ETH_DECIMALS = 18; export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer'; +export const INJECTED_DIV_CLASS = 'zeroExInstantResetRoot'; export const INJECTED_DIV_ID = 'zeroExInstant'; export const WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX = 'Transaction failed'; export const GWEI_IN_WEI = new BigNumber(1000000000); diff --git a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts index 97fb073aa..74713327c 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -12,6 +12,7 @@ import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; import { AffiliateInfo, ERC20Asset, OrderProcessState } from '../types'; import { updateBuyQuoteOrFlashErrorAsync } from '../util/buy_quote_fetcher'; +import { buyQuoteUpdater } from '../util/buy_quote_updater'; export interface SelectedERC20AssetAmountInputProps { fontColor?: ColorOption; @@ -67,20 +68,10 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP }; }; -const updateBuyQuoteAsync = async ( - assetBuyer: AssetBuyer, - dispatch: Dispatch<Action>, - asset: ERC20Asset, - assetAmount: BigNumber, - affiliateInfo?: AffiliateInfo, -): Promise<void> => { - // mark quote as pending - dispatch(actions.setQuoteRequestStatePending()); - // kick of buy quote - updateBuyQuoteOrFlashErrorAsync(assetBuyer, asset, assetAmount, dispatch, affiliateInfo); -}; - -const debouncedUpdateBuyQuoteAsync = _.debounce(updateBuyQuoteAsync, 200, { trailing: true }); +// TODO: change to set pending to true +const debouncedUpdateBuyQuoteAsync = _.debounce(buyQuoteUpdater.updateBuyQuoteAsync.bind(buyQuoteUpdater), 200, { + trailing: true, +}); const mapDispatchToProps = ( dispatch: Dispatch<Action>, diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index 59d1e646f..0274db30c 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR, INJECTED_DIV_ID } from './constants'; +import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR, INJECTED_DIV_CLASS, INJECTED_DIV_ID } from './constants'; import { ZeroExInstantOverlay, ZeroExInstantOverlayProps } from './index'; import { assert } from './util/assert'; @@ -41,6 +41,7 @@ export const render = (props: ZeroExInstantOverlayProps, selector: string = DEFA const appendTo = appendToIfExists as Element; const injectedDiv = document.createElement('div'); injectedDiv.setAttribute('id', INJECTED_DIV_ID); + injectedDiv.setAttribute('class', INJECTED_DIV_CLASS); appendTo.appendChild(injectedDiv); const instantOverlayProps = { ...props, diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index 9b0d80152..fc89e3d0e 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -2,7 +2,7 @@ import { BuyQuote } from '@0x/asset-buyer'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { ActionsUnion, Asset } from '../types'; +import { ActionsUnion, AddressAndEthBalanceInWei, Asset } from '../types'; export interface PlainAction<T extends string> { type: T; @@ -49,7 +49,8 @@ export const actions = { setAccountStateLocked: () => createAction(ActionTypes.SET_ACCOUNT_STATE_LOCKED), setAccountStateError: () => createAction(ActionTypes.SET_ACCOUNT_STATE_ERROR), setAccountStateReady: (address: string) => createAction(ActionTypes.SET_ACCOUNT_STATE_READY, address), - updateAccountEthBalance: (balance: BigNumber) => createAction(ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE, balance), + updateAccountEthBalance: (addressAndBalance: AddressAndEthBalanceInWei) => + createAction(ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE, addressAndBalance), updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price), updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount), setBuyOrderStateNone: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_NONE), diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 055d3de20..a50f24cba 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -1,8 +1,10 @@ +import { AssetProxyId } from '@0x/types'; import * as _ from 'lodash'; import { BIG_NUMBER_ZERO } from '../constants'; -import { AccountState } from '../types'; +import { AccountState, ERC20Asset } from '../types'; import { assetUtils } from '../util/asset'; +import { buyQuoteUpdater } from '../util/buy_quote_updater'; import { coinbaseApi } from '../util/coinbase_api'; import { errorFlasher } from '../util/error_flasher'; @@ -50,7 +52,8 @@ export const asyncData = { if (!_.isEmpty(availableAddresses)) { const activeAddress = availableAddresses[0]; store.dispatch(actions.setAccountStateReady(activeAddress)); - await asyncData.fetchAccountBalanceAndDispatchToStore(store); + // tslint:disable-next-line:no-floating-promises + asyncData.fetchAccountBalanceAndDispatchToStore(store); } else { store.dispatch(actions.setAccountStateLocked()); } @@ -63,11 +66,29 @@ export const asyncData = { return; } try { - const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(account.address); - store.dispatch(actions.updateAccountEthBalance(ethBalanceInWei)); + const address = account.address; + const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address); + store.dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei })); } catch (e) { // leave balance as is return; } }, + fetchCurrentBuyQuoteAndDispatchToStore: async (store: Store) => { + const { providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = store.getState(); + const assetBuyer = providerState.assetBuyer; + if ( + !_.isUndefined(selectedAssetAmount) && + !_.isUndefined(selectedAsset) && + selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20 + ) { + await buyQuoteUpdater.updateBuyQuoteAsync( + assetBuyer, + store.dispatch, + selectedAsset as ERC20Asset, + selectedAssetAmount, + affiliateInfo, + ); + } + }, }; diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index 961e29619..a5a1b6f7d 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -75,13 +75,14 @@ export const createReducer = (initialState: State) => { return reduceStateWithAccount(state, account); } case ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE: { - const account = state.providerState.account; - if (account.state !== AccountState.Ready) { + const { address, ethBalanceInWei } = action.data; + const currentAccount = state.providerState.account; + if (currentAccount.state !== AccountState.Ready || currentAccount.address !== address) { return state; } else { const newAccount: AccountReady = { - ...account, - ethBalanceInWei: action.data, + ...currentAccount, + ethBalanceInWei, }; return reduceStateWithAccount(state, newAccount); } diff --git a/packages/instant/src/style/media.ts b/packages/instant/src/style/media.ts index beabbac46..5e7aaba37 100644 --- a/packages/instant/src/style/media.ts +++ b/packages/instant/src/style/media.ts @@ -14,30 +14,38 @@ const generateMediaWrapper = (screenWidth: ScreenWidths) => (...args: any[]) => } `; -const media = { +export const media = { small: generateMediaWrapper(ScreenWidths.Sm), medium: generateMediaWrapper(ScreenWidths.Md), large: generateMediaWrapper(ScreenWidths.Lg), }; -export interface ScreenSpecifications { - default: string; - sm?: string; - md?: string; - lg?: string; +export interface ScreenSpecification<T> { + default: T; + sm?: T; + md?: T; + lg?: T; } -export type MediaChoice = string | ScreenSpecifications; -export const stylesForMedia = (cssPropertyName: string, choice: MediaChoice): InterpolationValue[] => { - if (typeof choice === 'string') { +export type OptionallyScreenSpecific<T> = T | ScreenSpecification<T>; +export type MediaChoice = OptionallyScreenSpecific<string>; +/** + * Given a css property name and a OptionallyScreenSpecific value, + * generates css properties with screen-specific viewport styling + */ +export function stylesForMedia<T extends string | number>( + cssPropertyName: string, + choice: OptionallyScreenSpecific<T>, +): InterpolationValue[] { + if (typeof choice === 'object') { return css` - ${cssPropertyName}: ${choice}; - `; - } - - return css` ${cssPropertyName}: ${choice.default}; ${choice.lg && media.large`${cssPropertyName}: ${choice.lg}`} ${choice.md && media.medium`${cssPropertyName}: ${choice.md}`} ${choice.sm && media.small`${cssPropertyName}: ${choice.sm}`} `; -}; + } else { + return css` + ${cssPropertyName}: ${choice}; + `; + } +} diff --git a/packages/instant/src/style/theme.ts b/packages/instant/src/style/theme.ts index d10c9b72c..2653c38f7 100644 --- a/packages/instant/src/style/theme.ts +++ b/packages/instant/src/style/theme.ts @@ -1,6 +1,6 @@ import * as styledComponents from 'styled-components'; -const { default: styled, css, keyframes, withTheme, ThemeProvider } = styledComponents; +const { default: styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider } = styledComponents; export type Theme = { [key in ColorOption]: string }; @@ -15,6 +15,8 @@ export enum ColorOption { white = 'white', lightOrange = 'lightOrange', darkOrange = 'darkOrange', + green = 'green', + red = 'red', } export const theme: Theme = { @@ -28,9 +30,12 @@ export const theme: Theme = { white: 'white', lightOrange: '#F9F2ED', darkOrange: '#F2994C', + green: '#3CB34F', + red: '#D00000', }; export const transparentWhite = 'rgba(255,255,255,0.3)'; export const overlayBlack = 'rgba(0, 0, 0, 0.6)'; +export const completelyTransparent = 'rga(0, 0, 0, 0)'; -export { styled, css, keyframes, withTheme, ThemeProvider }; +export { styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider }; diff --git a/packages/instant/src/style/z_index.ts b/packages/instant/src/style/z_index.ts index 727a5189d..bd034182e 100644 --- a/packages/instant/src/style/z_index.ts +++ b/packages/instant/src/style/z_index.ts @@ -1,5 +1,8 @@ export const zIndex = { - errorPopup: 1, - mainContainer: 2, - panel: 3, + errorPopBehind: 10, + mainContainer: 20, + dropdownItems: 30, + panel: 40, + errorPopup: 50, + overlayDefault: 100, }; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index d65f70008..20ad2ed95 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -120,3 +120,8 @@ export interface AccountNotReady { export type Account = AccountReady | AccountNotReady; export type OrderSource = string | SignedOrder[]; + +export interface AddressAndEthBalanceInWei { + address: string; + ethBalanceInWei: BigNumber; +} diff --git a/packages/instant/src/util/buy_quote_fetcher.ts b/packages/instant/src/util/buy_quote_fetcher.ts index cc8aac0fa..22ce835e8 100644 --- a/packages/instant/src/util/buy_quote_fetcher.ts +++ b/packages/instant/src/util/buy_quote_fetcher.ts @@ -1,4 +1,6 @@ // TODO: rename file and export object +// TODO: delete this + import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; import { AssetProxyId } from '@0x/types'; import { BigNumber } from '@0x/utils'; diff --git a/packages/instant/src/util/buy_quote_updater.ts b/packages/instant/src/util/buy_quote_updater.ts new file mode 100644 index 000000000..e697d3ef7 --- /dev/null +++ b/packages/instant/src/util/buy_quote_updater.ts @@ -0,0 +1,56 @@ +import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as _ from 'lodash'; +import { Dispatch } from 'redux'; +import { oc } from 'ts-optchain'; + +import { Action, actions } from '../redux/actions'; +import { AffiliateInfo, ERC20Asset } from '../types'; +import { assetUtils } from '../util/asset'; +import { errorFlasher } from '../util/error_flasher'; + +export const buyQuoteUpdater = { + updateBuyQuoteAsync: async ( + assetBuyer: AssetBuyer, + dispatch: Dispatch<Action>, + asset: ERC20Asset, + assetAmount: BigNumber, + affiliateInfo?: AffiliateInfo, + ): Promise<void> => { + // get a new buy quote. + const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); + // mark quote as pending + dispatch(actions.setQuoteRequestStatePending()); + const feePercentage = oc(affiliateInfo).feePercentage(); + let newBuyQuote: BuyQuote | undefined; + try { + newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage }); + } catch (error) { + dispatch(actions.setQuoteRequestStateFailure()); + let errorMessage; + if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { + const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); + errorMessage = `Not enough ${assetName} available`; + } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { + errorMessage = 'Not enough ZRX available'; + } else if ( + error.message === AssetBuyerError.StandardRelayerApiError || + error.message.startsWith(AssetBuyerError.AssetUnavailable) + ) { + const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); + errorMessage = `${assetName} is currently unavailable`; + } + if (!_.isUndefined(errorMessage)) { + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); + } else { + throw error; + } + return; + } + // We have a successful new buy quote + errorFlasher.clearError(dispatch); + // invalidate the last buy quote. + dispatch(actions.updateLatestBuyQuote(newBuyQuote)); + }, +}; diff --git a/packages/instant/src/util/etherscan.ts b/packages/instant/src/util/etherscan.ts index cfc2578a3..4d62c4d9f 100644 --- a/packages/instant/src/util/etherscan.ts +++ b/packages/instant/src/util/etherscan.ts @@ -21,4 +21,11 @@ export const etherscanUtil = { } return `https://${prefix}etherscan.io/tx/${txHash}`; }, + getEtherScanEthAddressIfExists: (ethAddress: string, networkId: number) => { + const prefix = etherscanPrefix(networkId); + if (_.isUndefined(prefix)) { + return; + } + return `https://${prefix}etherscan.io/address/${ethAddress}`; + }, }; diff --git a/packages/instant/src/util/format.ts b/packages/instant/src/util/format.ts index 4a48dec9d..44661d697 100644 --- a/packages/instant/src/util/format.ts +++ b/packages/instant/src/util/format.ts @@ -50,4 +50,7 @@ export const format = { } return `$${ethUnitAmount.mul(ethUsdPrice).toFixed(decimalPlaces)}`; }, + ethAddress: (address: string): string => { + return `0x${address.slice(2, 7)}…${address.slice(-5)}`; + }, }; |