diff options
Diffstat (limited to 'packages/website')
30 files changed, 845 insertions, 188 deletions
diff --git a/packages/website/package.json b/packages/website/package.json index a27dd8b79..7d49581cd 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -88,6 +88,7 @@ "tslint": "5.8.0", "tslint-config-0xproject": "^0.0.2", "typescript": "2.7.1", + "uglifyjs-webpack-plugin": "^1.2.5", "webpack": "^3.1.0", "webpack-dev-middleware": "^1.10.0", "webpack-dev-server": "^2.5.0" diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index d7f7ebc8f..32acb9d43 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -787,7 +787,9 @@ export class Blockchain { const provider = await Blockchain._getProviderAsync(injectedWeb3, networkIdIfExists); this.networkId = !_.isUndefined(networkIdIfExists) ? networkIdIfExists - : configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN; + : configs.IS_MAINNET_ENABLED + ? constants.NETWORK_ID_MAINNET + : constants.NETWORK_ID_KOVAN; this._dispatcher.updateNetworkId(this.networkId); const zeroExConfigs = { networkId: this.networkId, diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx index 48c7f9f57..d61dfa87d 100644 --- a/packages/website/ts/components/inputs/allowance_toggle.tsx +++ b/packages/website/ts/components/inputs/allowance_toggle.tsx @@ -1,4 +1,4 @@ -import { colors, constants as sharedConstants, Styles } from '@0xproject/react-shared'; +import { constants as sharedConstants, Styles } from '@0xproject/react-shared'; import { BigNumber, logUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import Toggle from 'material-ui/Toggle'; @@ -7,6 +7,7 @@ import { Blockchain } from 'ts/blockchain'; import { Dispatcher } from 'ts/redux/dispatcher'; 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/inputs/balance_bounded_input.tsx b/packages/website/ts/components/inputs/balance_bounded_input.tsx index 68b77cfc3..e5b502b25 100644 --- a/packages/website/ts/components/inputs/balance_bounded_input.tsx +++ b/packages/website/ts/components/inputs/balance_bounded_input.tsx @@ -5,7 +5,7 @@ import TextField from 'material-ui/TextField'; import * as React from 'react'; import { Link } from 'react-router-dom'; import { RequiredLabel } from 'ts/components/ui/required_label'; -import { InputErrMsg, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types'; +import { ValidatedBigNumberCallback, WebsitePaths } from 'ts/types'; import { utils } from 'ts/utils/utils'; interface BalanceBoundedInputProps { @@ -14,18 +14,21 @@ interface BalanceBoundedInputProps { amount?: BigNumber; hintText?: string; onChange: ValidatedBigNumberCallback; + onErrorMsgChange?: (errorMsg: React.ReactNode) => void; shouldShowIncompleteErrs?: boolean; shouldCheckBalance: boolean; - validate?: (amount: BigNumber) => InputErrMsg; + validate?: (amount: BigNumber) => React.ReactNode; onVisitBalancesPageClick?: () => void; shouldHideVisitBalancesLink?: boolean; isDisabled?: boolean; shouldShowErrs?: boolean; shouldShowUnderline?: boolean; + inputStyle?: React.CSSProperties; + inputHintStyle?: React.CSSProperties; } interface BalanceBoundedInputState { - errMsg: InputErrMsg; + errMsg: React.ReactNode; amountString: string; } @@ -36,6 +39,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp isDisabled: false, shouldShowErrs: true, hintText: 'amount', + onErrorMsgChange: _.noop, shouldShowUnderline: true, }; constructor(props: BalanceBoundedInputProps) { @@ -63,17 +67,11 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp } if (shouldResetState) { const amountString = nextProps.amount.toString(); - this.setState({ - errMsg: this._validate(amountString, nextProps.balance), - amountString, - }); + this._setAmountState(amountString, nextProps.balance); } } else if (isCurrentAmountNumeric) { const amountString = ''; - this.setState({ - errMsg: this._validate(amountString, nextProps.balance), - amountString, - }); + this._setAmountState(amountString, nextProps.balance); } } public render(): React.ReactNode { @@ -99,29 +97,25 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp hintText={<span style={{ textTransform: 'capitalize' }}>{this.props.hintText}</span>} onChange={this._onValueChange.bind(this)} underlineStyle={{ width: 'calc(100% + 50px)' }} + inputStyle={this.props.inputStyle} + hintStyle={this.props.inputHintStyle} underlineShow={this.props.shouldShowUnderline} disabled={this.props.isDisabled} /> ); } private _onValueChange(e: any, amountString: string): void { - const errMsg = this._validate(amountString, this.props.balance); - this.setState( - { - amountString, - errMsg, - }, - () => { - const isValid = _.isUndefined(errMsg); - if (utils.isNumeric(amountString) && !_.includes(amountString, '-')) { - this.props.onChange(isValid, new BigNumber(amountString)); - } else { - this.props.onChange(isValid); - } - }, - ); + this._setAmountState(amountString, this.props.balance, () => { + const isValid = _.isUndefined(this._validate(amountString, this.props.balance)); + const isPositiveNumber = utils.isNumeric(amountString) && !_.includes(amountString, '-'); + if (isPositiveNumber) { + this.props.onChange(isValid, new BigNumber(amountString)); + } else { + this.props.onChange(isValid); + } + }); } - private _validate(amountString: string, balance: BigNumber): InputErrMsg { + private _validate(amountString: string, balance: BigNumber): React.ReactNode { if (!utils.isNumeric(amountString)) { return amountString !== '' ? 'Must be a number' : ''; } @@ -161,4 +155,16 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp ); } } + + private _setAmountState(amount: string, balance: BigNumber, callback: () => void = _.noop): void { + const errorMsg = this._validate(amount, balance); + this.props.onErrorMsgChange(errorMsg); + this.setState( + { + amountString: amount, + errMsg: errorMsg, + }, + callback, + ); + } } diff --git a/packages/website/ts/components/inputs/eth_amount_input.tsx b/packages/website/ts/components/inputs/eth_amount_input.tsx index c3822a80b..fa684d85c 100644 --- a/packages/website/ts/components/inputs/eth_amount_input.tsx +++ b/packages/website/ts/components/inputs/eth_amount_input.tsx @@ -12,6 +12,7 @@ interface EthAmountInputProps { amount?: BigNumber; hintText?: string; onChange: ValidatedBigNumberCallback; + onErrorMsgChange?: (errorMsg: React.ReactNode) => void; shouldShowIncompleteErrs: boolean; onVisitBalancesPageClick?: () => void; shouldCheckBalance: boolean; @@ -19,6 +20,8 @@ interface EthAmountInputProps { shouldShowErrs?: boolean; shouldShowUnderline?: boolean; style?: React.CSSProperties; + labelStyle?: React.CSSProperties; + inputHintStyle?: React.CSSProperties; } interface EthAmountInputState {} @@ -40,6 +43,7 @@ export class EthAmountInput extends React.Component<EthAmountInputProps, EthAmou balance={this.props.balance} amount={amount} onChange={this._onChange.bind(this)} + onErrorMsgChange={this.props.onErrorMsgChange} shouldCheckBalance={this.props.shouldCheckBalance} shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs} onVisitBalancesPageClick={this.props.onVisitBalancesPageClick} @@ -47,8 +51,10 @@ export class EthAmountInput extends React.Component<EthAmountInputProps, EthAmou hintText={this.props.hintText} shouldShowErrs={this.props.shouldShowErrs} shouldShowUnderline={this.props.shouldShowUnderline} + inputStyle={this.props.style} + inputHintStyle={this.props.inputHintStyle} /> - <div style={{ paddingTop: _.isUndefined(this.props.label) ? 15 : 40 }}>ETH</div> + <div style={this._getLabelStyle()}>ETH</div> </div> ); } @@ -58,4 +64,7 @@ export class EthAmountInput extends React.Component<EthAmountInputProps, EthAmou : ZeroEx.toBaseUnitAmount(amount, constants.DECIMAL_PLACES_ETH); this.props.onChange(isValid, baseUnitAmountIfExists); } + private _getLabelStyle(): React.CSSProperties { + return this.props.labelStyle || { paddingTop: _.isUndefined(this.props.label) ? 15 : 40 }; + } } diff --git a/packages/website/ts/components/inputs/token_amount_input.tsx b/packages/website/ts/components/inputs/token_amount_input.tsx index 9a74bdd51..f040928f1 100644 --- a/packages/website/ts/components/inputs/token_amount_input.tsx +++ b/packages/website/ts/components/inputs/token_amount_input.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import { Blockchain } from 'ts/blockchain'; import { BalanceBoundedInput } from 'ts/components/inputs/balance_bounded_input'; -import { InputErrMsg, Token, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types'; +import { Token, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types'; interface TokenAmountInputProps { userAddress: string; @@ -20,11 +20,14 @@ interface TokenAmountInputProps { shouldCheckBalance: boolean; shouldCheckAllowance: boolean; onChange: ValidatedBigNumberCallback; + onErrorMsgChange?: (errorMsg: React.ReactNode) => void; onVisitBalancesPageClick?: () => void; lastForceTokenStateRefetch: number; shouldShowErrs?: boolean; shouldShowUnderline?: boolean; style?: React.CSSProperties; + labelStyle?: React.CSSProperties; + inputHintStyle?: React.CSSProperties; } interface TokenAmountInputState { @@ -74,17 +77,14 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok const amount = this.props.amount ? ZeroEx.toUnitAmount(this.props.amount, this.props.token.decimals) : undefined; - const hasLabel = !_.isUndefined(this.props.label); - const style = !_.isUndefined(this.props.style) - ? this.props.style - : { height: hasLabel ? HEIGHT_WITH_LABEL : HEIGHT_WITHOUT_LABEL }; return ( - <div className="flex overflow-hidden" style={style}> + <div className="flex overflow-hidden" style={this._getStyle()}> <BalanceBoundedInput label={this.props.label} amount={amount} balance={ZeroEx.toUnitAmount(this.state.balance, this.props.token.decimals)} onChange={this._onChange.bind(this)} + onErrorMsgChange={this.props.onErrorMsgChange} validate={this._validate.bind(this)} shouldCheckBalance={this.props.shouldCheckBalance} shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs} @@ -93,8 +93,10 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok hintText={this.props.hintText} shouldShowErrs={this.props.shouldShowErrs} shouldShowUnderline={this.props.shouldShowUnderline} + inputStyle={this.props.style} + inputHintStyle={this.props.inputHintStyle} /> - <div style={{ paddingTop: hasLabel ? 39 : 14 }}>{this.props.token.symbol}</div> + <div style={this._getLabelStyle()}>{this.props.token.symbol}</div> </div> ); } @@ -105,7 +107,7 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok } this.props.onChange(isValid, baseUnitAmount); } - private _validate(amount: BigNumber): InputErrMsg { + private _validate(amount: BigNumber): React.ReactNode { if (this.props.shouldCheckAllowance && amount.gt(this.state.allowance)) { return ( <span> @@ -139,4 +141,14 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok }); } } + private _getStyle(): React.CSSProperties { + const hasLabel = !_.isUndefined(this.props.label); + return !_.isUndefined(this.props.style) + ? this.props.style + : { height: hasLabel ? HEIGHT_WITH_LABEL : HEIGHT_WITHOUT_LABEL }; + } + private _getLabelStyle(): React.CSSProperties { + const hasLabel = !_.isUndefined(this.props.label); + return this.props.labelStyle || { paddingTop: hasLabel ? 39 : 14 }; + } } diff --git a/packages/website/ts/components/legacy_portal/legacy_portal.tsx b/packages/website/ts/components/legacy_portal/legacy_portal.tsx index 002b258fb..a5ea95629 100644 --- a/packages/website/ts/components/legacy_portal/legacy_portal.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal.tsx @@ -217,7 +217,7 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta /> <Route path={`${WebsitePaths.Portal}/trades`} - component={this._renderTradeHistory.bind(this)} + render={this._renderTradeHistory.bind(this)} /> <Route path={`${WebsitePaths.Home}`} diff --git a/packages/website/ts/components/portal/back_button.tsx b/packages/website/ts/components/portal/back_button.tsx new file mode 100644 index 000000000..48858613c --- /dev/null +++ b/packages/website/ts/components/portal/back_button.tsx @@ -0,0 +1,43 @@ +import { Styles } from '@0xproject/react-shared'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +import { colors } from 'ts/utils/colors'; + +export interface BackButtonProps { + to: string; + labelText: string; +} + +const BACK_BUTTON_HEIGHT = 28; + +const styles: Styles = { + backButton: { + height: BACK_BUTTON_HEIGHT, + paddingTop: 10, + backgroundColor: colors.white, + borderRadius: BACK_BUTTON_HEIGHT, + boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`, + }, + backButtonIcon: { + color: colors.mediumBlue, + fontSize: 20, + }, +}; + +export const BackButton = (props: BackButtonProps) => { + return ( + <div style={{ height: 65, paddingTop: 25 }}> + <Link to={props.to} style={{ textDecoration: 'none' }}> + <div className="flex right" style={styles.backButton}> + <div style={{ marginLeft: 12 }}> + <i style={styles.backButtonIcon} className={`zmdi zmdi-arrow-left`} /> + </div> + <div style={{ marginLeft: 12, marginRight: 12 }}> + <div style={{ fontSize: 16, color: colors.lightGrey }}>{props.labelText}</div> + </div> + </div> + </Link> + </div> + ); +}; diff --git a/packages/website/ts/components/portal/drawer_menu.tsx b/packages/website/ts/components/portal/drawer_menu.tsx new file mode 100644 index 000000000..75c8ac6c2 --- /dev/null +++ b/packages/website/ts/components/portal/drawer_menu.tsx @@ -0,0 +1,68 @@ +import { Styles } from '@0xproject/react-shared'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { defaultMenuItemEntries, Menu } from 'ts/components/portal/menu'; +import { Identicon } from 'ts/components/ui/identicon'; +import { WebsitePaths } from 'ts/types'; +import { colors } from 'ts/utils/colors'; +import { utils } from 'ts/utils/utils'; + +const IDENTICON_DIAMETER = 45; +const BORDER_RADIUS = '50%'; + +const styles: Styles = { + root: { + backgroundColor: colors.drawerMenuBackground, + width: '100%', + height: '100%', + }, + identicon: { + borderWidth: 3, + borderStyle: 'solid', + borderColor: colors.white, + borderRadius: BORDER_RADIUS, + MozBorderRadius: BORDER_RADIUS, + WebkitBorderRadius: BORDER_RADIUS, + }, + userAddress: { + color: colors.white, + }, +}; + +export interface DrawerMenuProps { + selectedPath?: string; + userAddress?: string; +} +export const DrawerMenu = (props: DrawerMenuProps) => { + const relayerItemEntry = { + to: `${WebsitePaths.Portal}/`, + labelText: 'Relayer ecosystem', + iconName: 'zmdi-portable-wifi', + }; + const menuItemEntries = _.concat(relayerItemEntry, defaultMenuItemEntries); + return ( + <div style={styles.root}> + <Header userAddress={props.userAddress} /> + <Menu selectedPath={props.selectedPath} menuItemEntries={menuItemEntries} /> + </div> + ); +}; + +interface HeaderProps { + userAddress?: string; +} +const Header = (props: HeaderProps) => { + return ( + <div className="flex flex-center py4"> + <div className="flex flex-column mx-auto"> + <Identicon address={props.userAddress} diameter={IDENTICON_DIAMETER} style={styles.identicon} /> + {!_.isUndefined(props.userAddress) && ( + <div className="pt2" style={styles.userAddress}> + {utils.getAddressBeginAndEnd(props.userAddress)} + </div> + )} + </div> + </div> + ); +}; diff --git a/packages/website/ts/components/portal/loading.tsx b/packages/website/ts/components/portal/loading.tsx new file mode 100644 index 000000000..d804dd1b8 --- /dev/null +++ b/packages/website/ts/components/portal/loading.tsx @@ -0,0 +1,21 @@ +import CircularProgress from 'material-ui/CircularProgress'; +import * as React from 'react'; + +const CIRCULAR_PROGRESS_SIZE = 40; +const CIRCULAR_PROGRESS_THICKNESS = 5; + +export interface LoadingProps { + isLoading: boolean; + content: React.ReactNode; +} +export const Loading = (props: LoadingProps) => { + if (props.isLoading) { + return ( + <div className="center"> + <CircularProgress size={CIRCULAR_PROGRESS_SIZE} thickness={CIRCULAR_PROGRESS_THICKNESS} /> + </div> + ); + } else { + return <div>{props.content}</div>; + } +}; diff --git a/packages/website/ts/components/portal/menu.tsx b/packages/website/ts/components/portal/menu.tsx new file mode 100644 index 000000000..6a3301549 --- /dev/null +++ b/packages/website/ts/components/portal/menu.tsx @@ -0,0 +1,116 @@ +import { Styles } from '@0xproject/react-shared'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { MenuItem } from 'ts/components/ui/menu_item'; +import { Environments, WebsitePaths } from 'ts/types'; +import { colors } from 'ts/utils/colors'; +import { configs } from 'ts/utils/configs'; + +export interface MenuTheme { + paddingLeft: number; + textColor: string; + iconColor: string; + selectedIconColor: string; + selectedBackgroundColor: string; +} + +export interface MenuItemEntry { + to: string; + labelText: string; + iconName: string; +} + +export interface MenuProps { + selectedPath?: string; + theme?: MenuTheme; + menuItemEntries?: MenuItemEntry[]; +} + +export const defaultMenuItemEntries: MenuItemEntry[] = [ + { + to: `${WebsitePaths.Portal}/account`, + labelText: 'Account overview', + iconName: 'zmdi-balance-wallet', + }, + { + to: `${WebsitePaths.Portal}/trades`, + labelText: 'Trade history', + iconName: 'zmdi-format-list-bulleted', + }, + { + to: `${WebsitePaths.Portal}/weth`, + labelText: 'Wrapped ETH', + iconName: 'zmdi-circle-o', + }, + { + to: `${WebsitePaths.Portal}/direct`, + labelText: 'Trade direct', + iconName: 'zmdi-swap', + }, +]; + +const DEFAULT_MENU_THEME: MenuTheme = { + paddingLeft: 30, + textColor: colors.white, + iconColor: colors.white, + selectedIconColor: colors.white, + selectedBackgroundColor: colors.menuItemDefaultSelectedBackground, +}; + +export const Menu: React.StatelessComponent<MenuProps> = (props: MenuProps) => { + return ( + <div> + {_.map(props.menuItemEntries, entry => { + const isSelected = entry.to === props.selectedPath; + return ( + <MenuItem key={entry.to} to={entry.to}> + <MenuItemLabel + title={entry.labelText} + iconName={entry.iconName} + selected={isSelected} + theme={props.theme} + /> + </MenuItem> + ); + })} + </div> + ); +}; +Menu.defaultProps = { + theme: DEFAULT_MENU_THEME, + menuItemEntries: defaultMenuItemEntries, +}; + +interface MenuItemLabelProps { + title: string; + iconName: string; + selected: boolean; + theme: MenuTheme; +} +const MenuItemLabel: React.StatelessComponent<MenuItemLabelProps> = (props: MenuItemLabelProps) => { + const styles: Styles = { + root: { + backgroundColor: props.selected ? props.theme.selectedBackgroundColor : undefined, + paddingLeft: props.theme.paddingLeft, + }, + icon: { + color: props.selected ? props.theme.selectedIconColor : props.theme.iconColor, + fontSize: 20, + }, + text: { + color: props.theme.textColor, + fontWeight: props.selected ? 'bold' : 'normal', + fontSize: 16, + }, + }; + return ( + <div className="flex py2" style={styles.root}> + <div className="pr1"> + <i style={styles.icon} className={`zmdi ${props.iconName}`} /> + </div> + <div className="pl1" style={styles.text}> + {props.title} + </div> + </div> + ); +}; diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index e572b7911..1bd318c28 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -3,20 +3,40 @@ import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import * as React from 'react'; import * as DocumentTitle from 'react-document-title'; +import { Link, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { Blockchain } from 'ts/blockchain'; import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog'; import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog'; import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog'; +import { EthWrappers } from 'ts/components/eth_wrappers'; import { AssetPicker } from 'ts/components/generate_order/asset_picker'; +import { BackButton } from 'ts/components/portal/back_button'; +import { Loading } from 'ts/components/portal/loading'; +import { Menu, MenuTheme } from 'ts/components/portal/menu'; +import { Section } from 'ts/components/portal/section'; +import { TextHeader } from 'ts/components/portal/text_header'; import { RelayerIndex } from 'ts/components/relayer_index/relayer_index'; +import { TokenBalances } from 'ts/components/token_balances'; import { TopBar, TopBarDisplayType } from 'ts/components/top_bar/top_bar'; +import { TradeHistory } from 'ts/components/trade_history/trade_history'; import { FlashMessage } from 'ts/components/ui/flash_message'; import { Wallet } from 'ts/components/wallet/wallet'; +import { GenerateOrderForm } from 'ts/containers/generate_order_form'; import { localStorage } from 'ts/local_storage/local_storage'; import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; +import { FullscreenMessage } from 'ts/pages/fullscreen_message'; import { Dispatcher } from 'ts/redux/dispatcher'; -import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, TokenVisibility } from 'ts/types'; +import { + BlockchainErrs, + HashData, + Order, + ProviderType, + ScreenWidths, + TokenByAddress, + TokenVisibility, + WebsitePaths, +} from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { Translate } from 'ts/utils/translate'; @@ -54,6 +74,12 @@ interface PortalState { tokenManagementState: TokenManagementState; } +interface AccountManagementItem { + pathName: string; + headerText: string; + render: () => React.ReactNode; +} + enum TokenManagementState { Add = 'Add', Remove = 'Remove', @@ -62,6 +88,8 @@ enum TokenManagementState { const THROTTLE_TIMEOUT = 100; const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded); +const LEFT_COLUMN_WIDTH = 346; +const MENU_PADDING_LEFT = 185; const styles: Styles = { root: { @@ -72,15 +100,15 @@ const styles: Styles = { body: { height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`, }, + leftColumn: { + width: LEFT_COLUMN_WIDTH, + height: '100%', + }, scrollContainer: { height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`, WebkitOverflowScrolling: 'touch', overflow: 'auto', }, - title: { - fontWeight: 'bold', - fontSize: 20, - }, }; export class Portal extends React.Component<PortalProps, PortalState> { @@ -148,8 +176,6 @@ export class Portal extends React.Component<PortalProps, PortalState> { const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind( this.props.dispatcher, ); - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); const isAssetPickerDialogOpen = this.state.tokenManagementState !== TokenManagementState.None; const tokenVisibility = this.state.tokenManagementState === TokenManagementState.Add @@ -173,36 +199,14 @@ export class Portal extends React.Component<PortalProps, PortalState> { style={{ backgroundColor: colors.lightestGrey }} /> <div id="portal" style={styles.body}> - <div className="sm-flex flex-center"> - <div className="flex-last px3"> - <div className="py3" style={styles.title}> - Your Account - </div> - <Wallet - userAddress={this.props.userAddress} - networkId={this.props.networkId} - blockchain={this._blockchain} - blockchainIsLoaded={this.props.blockchainIsLoaded} - blockchainErr={this.props.blockchainErr} - dispatcher={this.props.dispatcher} - tokenByAddress={this.props.tokenByAddress} - trackedTokens={trackedTokens} - userEtherBalanceInWei={this.props.userEtherBalanceInWei} - lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} - injectedProviderName={this.props.injectedProviderName} - providerType={this.props.providerType} - onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)} - onAddToken={this._onAddToken.bind(this)} - onRemoveToken={this._onRemoveToken.bind(this)} - /> - </div> - <div className="flex-auto px3" style={styles.scrollContainer}> - <div className="py3" style={styles.title}> - Explore 0x Ecosystem - </div> - <RelayerIndex networkId={this.props.networkId} /> - </div> - </div> + <Switch> + <Route path={`${WebsitePaths.Portal}/:route`} render={this._renderOtherRoutes.bind(this)} /> + <Route + exact={true} + path={`${WebsitePaths.Portal}/`} + render={this._renderMainRoute.bind(this)} + /> + </Switch> <BlockchainErrDialog blockchain={this._blockchain} blockchainErr={this.props.blockchainErr} @@ -241,6 +245,173 @@ export class Portal extends React.Component<PortalProps, PortalState> { </div> ); } + private _renderMainRoute(): React.ReactNode { + if (this._isSmallScreen()) { + return <SmallLayout content={this._renderRelayerIndexSection()} />; + } else { + return <LargeLayout left={this._renderWalletSection()} right={this._renderRelayerIndexSection()} />; + } + } + private _renderOtherRoutes(routeComponentProps: RouteComponentProps<any>): React.ReactNode { + if (this._isSmallScreen()) { + return <SmallLayout content={this._renderAccountManagement()} />; + } else { + return <LargeLayout left={this._renderMenu(routeComponentProps)} right={this._renderAccountManagement()} />; + } + } + private _renderMenu(routeComponentProps: RouteComponentProps<any>): React.ReactNode { + const menuTheme: MenuTheme = { + paddingLeft: MENU_PADDING_LEFT, + textColor: colors.darkerGrey, + iconColor: colors.darkerGrey, + selectedIconColor: colors.yellow800, + selectedBackgroundColor: 'transparent', + }; + return ( + <Section + header={<BackButton to={`${WebsitePaths.Portal}`} labelText="back to Relayers" />} + body={<Menu selectedPath={routeComponentProps.location.pathname} theme={menuTheme} />} + /> + ); + } + private _renderWallet(): React.ReactNode { + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); + return ( + <Wallet + userAddress={this.props.userAddress} + networkId={this.props.networkId} + blockchain={this._blockchain} + blockchainIsLoaded={this.props.blockchainIsLoaded} + blockchainErr={this.props.blockchainErr} + dispatcher={this.props.dispatcher} + tokenByAddress={this.props.tokenByAddress} + trackedTokens={trackedTokens} + userEtherBalanceInWei={this.props.userEtherBalanceInWei} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)} + onAddToken={this._onAddToken.bind(this)} + onRemoveToken={this._onRemoveToken.bind(this)} + /> + ); + } + private _renderWalletSection(): React.ReactNode { + return <Section header={<TextHeader labelText="Your Account" />} body={this._renderWallet()} />; + } + private _renderAccountManagement(): React.ReactNode { + const accountManagementItems: AccountManagementItem[] = [ + { + pathName: `${WebsitePaths.Portal}/weth`, + headerText: 'Wrapped ETH', + render: this._renderEthWrapper.bind(this), + }, + { + pathName: `${WebsitePaths.Portal}/account`, + headerText: 'Your Account', + render: this._isSmallScreen() ? this._renderWallet.bind(this) : this._renderTokenBalances.bind(this), + }, + { + pathName: `${WebsitePaths.Portal}/trades`, + headerText: 'Trade History', + render: this._renderTradeHistory.bind(this), + }, + { + pathName: `${WebsitePaths.Portal}/direct`, + headerText: 'Trade Direct', + render: this._renderTradeDirect.bind(this), + }, + ]; + 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> + ); + } + private _renderAccountManagementItem(item: AccountManagementItem): React.ReactNode { + return ( + <Section + header={<TextHeader labelText={item.headerText} />} + body={<Loading isLoading={!this.props.blockchainIsLoaded} content={item.render()} />} + /> + ); + } + private _renderEthWrapper(): React.ReactNode { + return ( + <EthWrappers + networkId={this.props.networkId} + blockchain={this._blockchain} + dispatcher={this.props.dispatcher} + tokenByAddress={this.props.tokenByAddress} + userAddress={this.props.userAddress} + userEtherBalanceInWei={this.props.userEtherBalanceInWei} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + /> + ); + } + private _renderTradeHistory(): React.ReactNode { + return ( + <TradeHistory + tokenByAddress={this.props.tokenByAddress} + userAddress={this.props.userAddress} + networkId={this.props.networkId} + /> + ); + } + private _renderTradeDirect(match: any, location: Location, history: History): React.ReactNode { + return ( + <GenerateOrderForm + blockchain={this._blockchain} + hashData={this.props.hashData} + dispatcher={this.props.dispatcher} + /> + ); + } + private _renderTokenBalances(): React.ReactNode { + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); + return ( + <TokenBalances + blockchain={this._blockchain} + blockchainErr={this.props.blockchainErr} + blockchainIsLoaded={this.props.blockchainIsLoaded} + dispatcher={this.props.dispatcher} + screenWidth={this.props.screenWidth} + tokenByAddress={this.props.tokenByAddress} + trackedTokens={trackedTokens} + userAddress={this.props.userAddress} + userEtherBalanceInWei={this.props.userEtherBalanceInWei} + networkId={this.props.networkId} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + /> + ); + } + private _renderRelayerIndexSection(): React.ReactNode { + return ( + <Section + header={<TextHeader labelText="Explore 0x Relayers" />} + body={<RelayerIndex networkId={this.props.networkId} screenWidth={this.props.screenWidth} />} + /> + ); + } + private _renderNotFoundMessage(): React.ReactNode { + return ( + <FullscreenMessage + headerText="404 Not Found" + bodyText="Hm... looks like we couldn't find what you are looking for." + /> + ); + } private _onTokenChosen(tokenAddress: string): void { if (_.isEmpty(tokenAddress)) { this.setState({ @@ -294,4 +465,38 @@ export class Portal extends React.Component<PortalProps, PortalState> { const newScreenWidth = utils.getScreenWidth(); this.props.dispatcher.updateScreenWidth(newScreenWidth); } + private _isSmallScreen(): boolean { + const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; + return isSmallScreen; + } +} + +interface LargeLayoutProps { + left: React.ReactNode; + right: React.ReactNode; } +const LargeLayout = (props: LargeLayoutProps) => { + return ( + <div className="sm-flex flex-center"> + <div className="flex-last px3"> + <div style={styles.leftColumn}>{props.left}</div> + </div> + <div className="flex-auto px3" style={styles.scrollContainer}> + {props.right} + </div> + </div> + ); +}; + +interface SmallLayoutProps { + content: React.ReactNode; +} +const SmallLayout = (props: SmallLayoutProps) => { + return ( + <div className="sm-flex flex-center"> + <div className="flex-auto px3" style={styles.scrollContainer}> + {props.content} + </div> + </div> + ); +}; // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/portal/section.tsx b/packages/website/ts/components/portal/section.tsx new file mode 100644 index 000000000..9b172aae0 --- /dev/null +++ b/packages/website/ts/components/portal/section.tsx @@ -0,0 +1,15 @@ +import { Styles } from '@0xproject/react-shared'; +import * as React from 'react'; + +export interface SectionProps { + header: React.ReactNode; + body: React.ReactNode; +} +export const Section = (props: SectionProps) => { + return ( + <div className="flex flex-column" style={{ height: '100%' }}> + {props.header} + <div className="flex-auto">{props.body}</div> + </div> + ); +}; diff --git a/packages/website/ts/components/portal/text_header.tsx b/packages/website/ts/components/portal/text_header.tsx new file mode 100644 index 000000000..4aabd47d0 --- /dev/null +++ b/packages/website/ts/components/portal/text_header.tsx @@ -0,0 +1,21 @@ +import { Styles } from '@0xproject/react-shared'; +import * as React from 'react'; + +export interface TextHeaderProps { + labelText: string; +} + +const styles: Styles = { + title: { + fontWeight: 'bold', + fontSize: 20, + }, +}; + +export const TextHeader = (props: TextHeaderProps) => { + return ( + <div className="py3" style={styles.title}> + {props.labelText} + </div> + ); +}; 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 5964dcd56..dc9eeb29d 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -1,4 +1,4 @@ -import { colors, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import { GridTile } from 'material-ui/GridList'; import * as React from 'react'; @@ -6,6 +6,7 @@ import * as React from 'react'; import { TopTokens } from 'ts/components/relayer_index/relayer_top_tokens'; import { TokenIcon } from 'ts/components/ui/token_icon'; 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 675e83c9f..9ef6eaf59 100644 --- a/packages/website/ts/components/relayer_index/relayer_index.tsx +++ b/packages/website/ts/components/relayer_index/relayer_index.tsx @@ -1,4 +1,4 @@ -import { colors, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import CircularProgress from 'material-ui/CircularProgress'; import FlatButton from 'material-ui/FlatButton'; @@ -6,11 +6,13 @@ import { GridList } from 'material-ui/GridList'; import * as React from 'react'; import { RelayerGridTile } from 'ts/components/relayer_index/relayer_grid_tile'; -import { WebsiteBackendRelayerInfo } from 'ts/types'; +import { ScreenWidths, WebsiteBackendRelayerInfo } from 'ts/types'; import { backendClient } from 'ts/utils/backend_client'; +import { colors } from 'ts/utils/colors'; export interface RelayerIndexProps { networkId: number; + screenWidth: ScreenWidths; } interface RelayerIndexState { @@ -35,7 +37,9 @@ const styles: Styles = { }; const CELL_HEIGHT = 290; -const NUMBER_OF_COLUMNS = 4; +const NUMBER_OF_COLUMNS_LARGE = 4; +const NUMBER_OF_COLUMNS_MEDIUM = 3; +const NUMBER_OF_COLUMNS_SMALL = 1; const GRID_PADDING = 20; export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerIndexState> { @@ -59,27 +63,22 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde const isReadyToRender = _.isUndefined(this.state.error) && !_.isUndefined(this.state.relayerInfos); if (!isReadyToRender) { return ( - <div className="col col-12" style={{ ...styles.root, height: '100%' }}> - <div - className="relative sm-px2 sm-pt2 sm-m1" - style={{ height: 122, top: '33%', transform: 'translateY(-50%)' }} - > - <div className="center pb2"> - {_.isUndefined(this.state.error) ? ( - <CircularProgress size={40} thickness={5} /> - ) : ( - <Retry onRetry={this._fetchRelayerInfosAsync.bind(this)} /> - )} - </div> - </div> + // TODO: consolidate this loading component with the one in portal + <div className="center"> + {_.isUndefined(this.state.error) ? ( + <CircularProgress size={40} thickness={5} /> + ) : ( + <Retry onRetry={this._fetchRelayerInfosAsync.bind(this)} /> + )} </div> ); } else { + const numberOfColumns = this._numberOfColumnsForScreenWidth(this.props.screenWidth); return ( <div style={styles.root}> <GridList cellHeight={CELL_HEIGHT} - cols={NUMBER_OF_COLUMNS} + cols={numberOfColumns} padding={GRID_PADDING} style={styles.gridList} > @@ -113,6 +112,17 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde } } } + private _numberOfColumnsForScreenWidth(screenWidth: ScreenWidths): number { + switch (screenWidth) { + case ScreenWidths.Md: + return NUMBER_OF_COLUMNS_MEDIUM; + case ScreenWidths.Sm: + return NUMBER_OF_COLUMNS_SMALL; + case ScreenWidths.Lg: + default: + return NUMBER_OF_COLUMNS_LARGE; + } + } } interface RetryProps { diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index 9d8bd6913..fc516882a 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -1,13 +1,15 @@ -import { colors, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import RaisedButton from 'material-ui/RaisedButton'; import * as React from 'react'; + import { Blockchain } from 'ts/blockchain'; 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 { ProviderType } from 'ts/types'; +import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; @@ -41,7 +43,9 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi this.props.providerType === ProviderType.Injected && this.props.injectedProviderName !== '0x Public'; const displayAddress = isAddressAvailable ? utils.getAddressBeginAndEnd(this.props.userAddress) - : isExternallyInjectedProvider ? 'Account locked' : '0x0000...0000'; + : isExternallyInjectedProvider + ? 'Account locked' + : '0x0000...0000'; // If the "injected" provider is our fallback public node, then we want to // show the "connect a wallet" message instead of the providerName const injectedProviderName = isExternallyInjectedProvider diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 3b39d789b..f2d0c6177 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -9,6 +9,7 @@ import { Link } from 'react-router-dom'; import ReactTooltip = require('react-tooltip'); import { Blockchain } from 'ts/blockchain'; import { LegacyPortalMenu } from 'ts/components/legacy_portal/legacy_portal_menu'; +import { DrawerMenu } from 'ts/components/portal/drawer_menu'; import { SidebarHeader } from 'ts/components/sidebar_header'; import { ProviderDisplay } from 'ts/components/top_bar/provider_display'; import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item'; @@ -18,6 +19,7 @@ import { Dispatcher } from 'ts/redux/dispatcher'; import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/types'; import { constants } from 'ts/utils/constants'; import { Translate } from 'ts/utils/translate'; +import { utils } from 'ts/utils/utils'; export enum TopBarDisplayType { Default, @@ -93,6 +95,13 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { isDrawerOpen: false, }; } + public componentWillReceiveProps(nextProps: TopBarProps): void { + if (nextProps.location.pathname !== this.props.location.pathname) { + this.setState({ + isDrawerOpen: false, + }); + } + } public render(): React.ReactNode { const isNightVersion = this.props.isNightVersion; const isExpandedDisplayType = this.props.displayType === TopBarDisplayType.Expanded; @@ -196,6 +205,8 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> ); const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>; + // TODO : Remove this once we ship portal v2 + const shouldShowPortalV2Drawer = this._isViewingPortal() && utils.shouldShowPortalV2(); return ( <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style, ...{ height } }} className="pb1"> <div className={parentClassNames}> @@ -268,10 +279,22 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> </div> </div> - {this._renderDrawer()} + {shouldShowPortalV2Drawer ? this._renderPortalV2Drawer() : this._renderDrawer()} </div> ); } + private _renderPortalV2Drawer(): React.ReactNode { + return ( + <Drawer + open={this.state.isDrawerOpen} + docked={false} + openSecondary={true} + onRequestChange={this._onMenuButtonClick.bind(this)} + > + <DrawerMenu selectedPath={this.props.location.pathname} userAddress={this.props.userAddress} /> + </Drawer> + ); + } private _renderDrawer(): React.ReactNode { return ( <Drawer diff --git a/packages/website/ts/components/ui/input_label.tsx b/packages/website/ts/components/ui/input_label.tsx index 8137c0db6..8eda45a5d 100644 --- a/packages/website/ts/components/ui/input_label.tsx +++ b/packages/website/ts/components/ui/input_label.tsx @@ -1,11 +1,11 @@ -import { colors } from '@0xproject/react-shared'; +import { colors, Styles } from '@0xproject/react-shared'; import * as React from 'react'; export interface InputLabelProps { text: string | Element | React.ReactNode; } -const styles = { +const styles: Styles = { label: { color: colors.grey, fontSize: 12, diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 95f31582e..dab8b7d2f 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -1,6 +1,5 @@ import { ZeroEx } from '0x.js'; import { - colors, constants as sharedConstants, EtherscanLinkSuffixes, Styles, @@ -18,6 +17,7 @@ import NavigationArrowDownward from 'material-ui/svg-icons/navigation/arrow-down import NavigationArrowUpward from 'material-ui/svg-icons/navigation/arrow-upward'; import Close from 'material-ui/svg-icons/navigation/close'; import * as React from 'react'; +import { Link } from 'react-router-dom'; import ReactTooltip = require('react-tooltip'); import firstBy = require('thenby'); @@ -38,8 +38,10 @@ import { TokenByAddress, TokenState, 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 { utils } from 'ts/utils/utils'; import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles'; @@ -80,7 +82,7 @@ interface AccessoryItemConfig { const styles: Styles = { root: { - width: 346, + width: '100%', backgroundColor: colors.white, borderBottomRightRadius: 10, borderBottomLeftRadius: 10, @@ -134,6 +136,10 @@ const styles: Styles = { overflow: 'auto', WebkitOverflowScrolling: 'touch', }, + manageYourWalletText: { + color: colors.mediumBlue, + fontWeight: 'bold', + }, }; const ETHER_ICON_PATH = '/images/ether.png'; @@ -237,13 +243,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> { const userAddress = this.props.userAddress; const primaryText = utils.getAddressBeginAndEnd(userAddress); return ( - <ListItem - key={HEADER_ITEM_KEY} - primaryText={primaryText} - leftIcon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />} - style={{ ...styles.paddedItem, ...styles.borderedItem }} - innerDivStyle={styles.headerItemInnerDiv} - /> + <Link key={HEADER_ITEM_KEY} to={`${WebsitePaths.Portal}/account`} style={{ textDecoration: 'none' }}> + <ListItem + primaryText={primaryText} + leftIcon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />} + style={{ ...styles.paddedItem, ...styles.borderedItem }} + innerDivStyle={styles.headerItemInnerDiv} + /> + </Link> ); } private _renderBody(): React.ReactElement<{}> { @@ -275,31 +282,50 @@ export class Wallet extends React.Component<WalletProps, WalletState> { } private _renderFooterRows(): React.ReactElement<{}> { return ( - <ListItem - key={FOOTER_ITEM_KEY} - primaryText={ - <div className="flex"> - <FloatingActionButton mini={true} zDepth={0} onClick={this.props.onAddToken}> - <ContentAdd /> - </FloatingActionButton> - <FloatingActionButton mini={true} zDepth={0} className="px1" onClick={this.props.onRemoveToken}> - <ContentRemove /> - </FloatingActionButton> - <div - style={{ - paddingLeft: 10, - position: 'relative', - top: '50%', - transform: 'translateY(33%)', - }} - > - add/remove tokens + <div key={FOOTER_ITEM_KEY}> + <ListItem + primaryText={ + <div className="flex"> + <FloatingActionButton mini={true} zDepth={0} onClick={this.props.onAddToken}> + <ContentAdd /> + </FloatingActionButton> + <FloatingActionButton + mini={true} + zDepth={0} + className="px1" + onClick={this.props.onRemoveToken} + > + <ContentRemove /> + </FloatingActionButton> + <div + style={{ + paddingLeft: 10, + position: 'relative', + top: '50%', + transform: 'translateY(33%)', + }} + > + add/remove tokens + </div> </div> - </div> - } - disabled={true} - innerDivStyle={styles.footerItemInnerDiv} - /> + } + disabled={true} + innerDivStyle={styles.footerItemInnerDiv} + style={styles.borderedItem} + /> + <Link to={`${WebsitePaths.Portal}/account`} style={{ textDecoration: 'none' }}> + <ListItem + primaryText={ + <div className="flex right" style={styles.manageYourWalletText}> + {'manage your wallet'} + </div> + // https://github.com/palantir/tslint-react/issues/140 + // tslint:disable-next-line:jsx-curly-spacing + } + style={{ ...styles.paddedItem, ...styles.borderedItem }} + /> + </Link> + </div> ); } private _renderEthRows(): React.ReactNode { diff --git a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx index 89e32f7be..d334f1748 100644 --- a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx +++ b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx @@ -1,9 +1,10 @@ -import { colors, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; import FlatButton from 'material-ui/FlatButton'; import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; import * as React from 'react'; import { ProviderType } from 'ts/types'; +import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; export interface WalletDisconnectedItemProps { diff --git a/packages/website/ts/components/wallet/wrap_ether_item.tsx b/packages/website/ts/components/wallet/wrap_ether_item.tsx index 2ed924bcd..98b28b3ad 100644 --- a/packages/website/ts/components/wallet/wrap_ether_item.tsx +++ b/packages/website/ts/components/wallet/wrap_ether_item.tsx @@ -1,5 +1,5 @@ import { ZeroEx } from '0x.js'; -import { colors, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; import { BigNumber, logUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import FlatButton from 'material-ui/FlatButton'; @@ -11,6 +11,7 @@ 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 { 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'; @@ -31,8 +32,8 @@ export interface WrapEtherItemProps { interface WrapEtherItemState { currentInputAmount?: BigNumber; - currentInputHasErrors: boolean; isEthConversionHappening: boolean; + errorMsg: React.ReactNode; } const styles: Styles = { @@ -46,13 +47,29 @@ const styles: Styles = { padding: 4, width: 125, }, - ethAmountInput: { height: 32 }, - innerDiv: { paddingLeft: 60, paddingTop: 0 }, - wrapEtherConfirmationButtonContainer: { width: 128, top: 16 }, + amountInput: { height: 34 }, + amountInputLabel: { + paddingTop: 10, + paddingRight: 10, + paddingLeft: 5, + color: colors.grey, + fontSize: 14, + }, + amountInputHint: { + bottom: 18, + }, + innerDiv: { paddingLeft: 60, paddingTop: 0, paddingBottom: 10 }, + wrapEtherConfirmationButtonContainer: { width: 128, top: 19 }, wrapEtherConfirmationButtonLabel: { - fontSize: 10, + fontSize: 12, color: colors.white, }, + errorMsg: { + fontSize: 12, + marginTop: 4, + color: colors.red, + minHeight: 20, + }, }; export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEtherItemState> { @@ -60,8 +77,8 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther super(props); this.state = { currentInputAmount: undefined, - currentInputHasErrors: false, isEthConversionHappening: false, + errorMsg: null, }; } public render(): React.ReactNode { @@ -84,7 +101,10 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther shouldShowIncompleteErrs={false} shouldShowErrs={false} shouldShowUnderline={false} - style={styles.ethAmountInput} + style={styles.amountInput} + labelStyle={styles.amountInputLabel} + inputHintStyle={styles.amountInputHint} + onErrorMsgChange={this._onErrorMsgChange.bind(this)} /> ) : ( <TokenAmountInput @@ -99,12 +119,16 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther onChange={this._onValueChange.bind(this)} amount={this.state.currentInputAmount} hintText="0.00" - shouldShowErrs={false} // TODO: error handling + shouldShowErrs={false} shouldShowUnderline={false} - style={styles.ethAmountInput} + style={styles.amountInput} + labelStyle={styles.amountInputLabel} + inputHintStyle={styles.amountInputHint} + onErrorMsgChange={this._onErrorMsgChange.bind(this)} /> )} </div> + {this._renderErrorMsg()} </div> } secondaryTextLines={2} @@ -119,7 +143,11 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther private _onValueChange(isValid: boolean, amount?: BigNumber): void { this.setState({ currentInputAmount: amount, - currentInputHasErrors: !isValid, + }); + } + private _onErrorMsgChange(errorMsg: React.ReactNode): void { + this.setState({ + errorMsg, }); } private _renderIsEthConversionHappeningSpinner(): React.ReactElement<{}> { @@ -144,6 +172,9 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther </div> ); } + private _renderErrorMsg(): React.ReactNode { + return <div style={styles.errorMsg}>{this.state.errorMsg}</div>; + } private async _wrapEtherConfirmationActionAsync(): Promise<void> { this.setState({ isEthConversionHappening: true, diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index f255f81e7..7fc180449 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -34,14 +34,15 @@ import 'less/all.less'; // cause we only want to import the module when the user navigates to the page. // At the same time webpack statically parses for System.import() to determine bundle chunk split points // so each lazy import needs it's own `System.import()` declaration. -const LazyPortal = - utils.isDevelopment() || utils.isStaging() || utils.isDogfood() - ? createLazyComponent('Portal', async () => - System.import<any>(/* webpackChunkName: "portal" */ 'ts/containers/portal'), - ) - : createLazyComponent('LegacyPortal', async () => - System.import<any>(/* webpackChunkName: "legacyPortal" */ 'ts/containers/legacy_portal'), - ); + +// TODO: Remove this once we ship V2 +const LazyPortal = utils.shouldShowPortalV2() + ? createLazyComponent('Portal', async () => + System.import<any>(/* webpackChunkName: "portal" */ 'ts/containers/portal'), + ) + : createLazyComponent('LegacyPortal', async () => + System.import<any>(/* webpackChunkName: "legacyPortal" */ 'ts/containers/legacy_portal'), + ); const LazyZeroExJSDocumentation = createLazyComponent('Documentation', async () => System.import<any>(/* webpackChunkName: "zeroExDocs" */ 'ts/containers/zero_ex_js_documentation'), ); diff --git a/packages/website/ts/pages/fullscreen_message.tsx b/packages/website/ts/pages/fullscreen_message.tsx new file mode 100644 index 000000000..6fcf7b32c --- /dev/null +++ b/packages/website/ts/pages/fullscreen_message.tsx @@ -0,0 +1,30 @@ +import { Styles } from '@0xproject/react-shared'; +import * as React from 'react'; + +export interface FullscreenMessageProps { + headerText: string; + bodyText: string; +} + +const styles: Styles = { + thin: { + fontWeight: 100, + }, +}; + +export const FullscreenMessage = (props: FullscreenMessageProps) => { + return ( + <div className="mx-auto max-width-4 py4"> + <div className="center py4"> + <div className="py4"> + <div className="py4"> + <h1 style={styles.thin}>{props.headerText}</h1> + <div className="py1"> + <div className="py3">{props.bodyText}</div> + </div> + </div> + </div> + </div> + </div> + ); +}; diff --git a/packages/website/ts/pages/not_found.tsx b/packages/website/ts/pages/not_found.tsx index 96c73d4ec..674271636 100644 --- a/packages/website/ts/pages/not_found.tsx +++ b/packages/website/ts/pages/not_found.tsx @@ -3,6 +3,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import { Footer } from 'ts/components/footer'; import { TopBar } from 'ts/components/top_bar/top_bar'; +import { FullscreenMessage } from 'ts/pages/fullscreen_message'; import { Dispatcher } from 'ts/redux/dispatcher'; import { Translate } from 'ts/utils/translate'; @@ -12,35 +13,15 @@ export interface NotFoundProps { dispatcher: Dispatcher; } -interface NotFoundState {} - -const styles: Styles = { - thin: { - fontWeight: 100, - }, +export const NotFound = (props: NotFoundProps) => { + return ( + <div> + <TopBar blockchainIsLoaded={false} location={this.props.location} translate={this.props.translate} /> + <FullscreenMessage + headerText={'404 Not Found'} + bodyText={"Hm... looks like we couldn't find what you are looking for."} + /> + <Footer translate={this.props.translate} dispatcher={this.props.dispatcher} /> + </div> + ); }; - -export class NotFound extends React.Component<NotFoundProps, NotFoundState> { - public render(): React.ReactNode { - return ( - <div> - <TopBar blockchainIsLoaded={false} location={this.props.location} translate={this.props.translate} /> - <div className="mx-auto max-width-4 py4"> - <div className="center py4"> - <div className="py4"> - <div className="py4"> - <h1 style={{ ...styles.thin }}>404 Not Found</h1> - <div className="py1"> - <div className="py3"> - Hm... looks like we couldn't find what you are looking for. - </div> - </div> - </div> - </div> - </div> - </div> - <Footer translate={this.props.translate} dispatcher={this.props.dispatcher} /> - </div> - ); - } -} diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index 5caf5e73c..33e4d6c30 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -207,7 +207,6 @@ export interface ContractEvent { args: any; } -export type InputErrMsg = React.ReactNode | string | undefined; export type ValidatedBigNumberCallback = (isValid: boolean, amount?: BigNumber) => void; export enum ScreenWidths { Sm = 'SM', diff --git a/packages/website/ts/utils/colors.ts b/packages/website/ts/utils/colors.ts new file mode 100644 index 000000000..5ffdd6ba7 --- /dev/null +++ b/packages/website/ts/utils/colors.ts @@ -0,0 +1,19 @@ +import { colors as sharedColors } from '@0xproject/react-shared'; + +const appColors = { + walletBoxShadow: 'rgba(56, 59, 137, 0.2)', + walletBorder: '#ededee', + walletDefaultItemBackground: '#fbfbfc', + walletFocusedItemBackground: '#f0f1f4', + allowanceToggleShadow: 'rgba(0, 0, 0, 0)', + allowanceToggleOffTrack: '#adadad', + allowanceToggleOnTrack: sharedColors.mediumBlue, + wrapEtherConfirmationButton: sharedColors.mediumBlue, + drawerMenuBackground: '#4a4a4a', + menuItemDefaultSelectedBackground: '#424242', +}; + +export const colors = { + ...sharedColors, + ...appColors, +}; diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index 3c99bd2fe..c370ac90c 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -314,4 +314,7 @@ export const utils = { return _.includes(window.location.href, configs.DOMAIN_STAGING); }, isDogfood, + shouldShowPortalV2(): boolean { + return this.isDevelopment() || this.isStaging() || this.isDogfood(); + }, }; diff --git a/packages/website/ts/utils/wallet_item_styles.ts b/packages/website/ts/utils/wallet_item_styles.ts index 1ad304ce1..6b038efd2 100644 --- a/packages/website/ts/utils/wallet_item_styles.ts +++ b/packages/website/ts/utils/wallet_item_styles.ts @@ -1,4 +1,6 @@ -import { colors, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; + +import { colors } from 'ts/utils/colors'; export const styles: Styles = { focusedItem: { diff --git a/packages/website/webpack.config.js b/packages/website/webpack.config.js index e28e9e064..f9abeb27c 100644 --- a/packages/website/webpack.config.js +++ b/packages/website/webpack.config.js @@ -1,5 +1,6 @@ const path = require('path'); const webpack = require('webpack'); +const UglifyJsPlugin = require('uglifyjs-webpack-plugin') module.exports = { entry: ['./ts/index.tsx'], @@ -76,9 +77,14 @@ module.exports = { NODE_ENV: JSON.stringify(process.env.NODE_ENV), }, }), - new webpack.optimize.UglifyJsPlugin({ - mangle: { - except: ['BigNumber'], + // TODO: Revert to webpack bundled version with webpack v4. + // The v3 series bundled version does not support ES6 and + // fails to build. + new UglifyJsPlugin({ + uglifyOptions: { + mangle: { + reserved: ['BigNumber'], + }, }, }), ] |