diff options
Diffstat (limited to 'packages/website/ts/components/top_bar')
4 files changed, 657 insertions, 0 deletions
diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx new file mode 100644 index 000000000..39e7f2a8c --- /dev/null +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -0,0 +1,148 @@ +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'; + +const IDENTICON_DIAMETER = 32; + +interface ProviderDisplayProps { + dispatcher: Dispatcher; + userAddress: string; + networkId: number; + injectedProviderName: string; + providerType: ProviderType; + onToggleLedgerDialog: () => void; + blockchain: Blockchain; +} + +interface ProviderDisplayState {} + +export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> { + public render() { + const isAddressAvailable = !_.isEmpty(this.props.userAddress); + const isExternallyInjectedProvider = ProviderType.Injected && this.props.injectedProviderName !== '0x Public'; + const displayAddress = isAddressAvailable + ? utils.getAddressBeginAndEnd(this.props.userAddress) + : 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 + ? this.props.injectedProviderName + : 'Connect a wallet'; + const providerTitle = + this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S'; + const hoverActiveNode = ( + <div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}> + <div> + <Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} /> + </div> + <div style={{ marginLeft: 12, paddingTop: 1 }}> + <div style={{ fontSize: 12, color: colors.amber800 }}>{providerTitle}</div> + <div style={{ fontSize: 14 }}>{displayAddress}</div> + </div> + <div + style={{ borderLeft: `1px solid ${colors.grey300}`, marginLeft: 17, paddingTop: 1 }} + className="px2" + > + <i style={{ fontSize: 30, color: colors.grey300 }} className="zmdi zmdi zmdi-chevron-down" /> + </div> + </div> + ); + const hasInjectedProvider = + this.props.injectedProviderName !== '0x Public' && this.props.providerType === ProviderType.Injected; + const hasLedgerProvider = this.props.providerType === ProviderType.Ledger; + const horizontalPosition = hasInjectedProvider || hasLedgerProvider ? 'left' : 'middle'; + return ( + <div style={{ width: 'fit-content', height: 48, float: 'right' }}> + <DropDown + hoverActiveNode={hoverActiveNode} + popoverContent={this.renderPopoverContent(hasInjectedProvider, hasLedgerProvider)} + anchorOrigin={{ horizontal: horizontalPosition, vertical: 'bottom' }} + targetOrigin={{ horizontal: horizontalPosition, vertical: 'top' }} + zDepth={1} + /> + </div> + ); + } + public renderPopoverContent(hasInjectedProvider: boolean, hasLedgerProvider: boolean) { + if (hasInjectedProvider || hasLedgerProvider) { + return ( + <ProviderPicker + dispatcher={this.props.dispatcher} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this.props.onToggleLedgerDialog} + blockchain={this.props.blockchain} + /> + ); + } else { + // Nothing to connect to, show install/info popover + return ( + <div className="px2" style={{ maxWidth: 420 }}> + <div className="center h4 py2" style={{ color: colors.grey700 }}> + Choose a wallet: + </div> + <div className="flex pb3"> + <div className="center px2"> + <div style={{ color: colors.darkGrey }}>Install a browser wallet</div> + <div className="py2"> + <img src="/images/metamask_or_parity.png" width="135" /> + </div> + <div> + Use{' '} + <a + href={constants.URL_METAMASK_CHROME_STORE} + target="_blank" + style={{ color: colors.lightBlueA700 }} + > + Metamask + </a>{' '} + or{' '} + <a + href={constants.URL_PARITY_CHROME_STORE} + target="_blank" + style={{ color: colors.lightBlueA700 }} + > + Parity Signer + </a> + </div> + </div> + <div> + <div + className="pl1 ml1" + style={{ borderLeft: `1px solid ${colors.grey300}`, height: 65 }} + /> + <div className="py1">or</div> + <div + className="pl1 ml1" + style={{ borderLeft: `1px solid ${colors.grey300}`, height: 68 }} + /> + </div> + <div className="px2 center"> + <div style={{ color: colors.darkGrey }}>Connect to a ledger hardware wallet</div> + <div style={{ paddingTop: 21, paddingBottom: 29 }}> + <img src="/images/ledger_icon.png" style={{ width: 80 }} /> + </div> + <div> + <RaisedButton + style={{ width: '100%' }} + label="Use Ledger" + onClick={this.props.onToggleLedgerDialog} + /> + </div> + </div> + </div> + </div> + ); + } + } +} diff --git a/packages/website/ts/components/top_bar/provider_picker.tsx b/packages/website/ts/components/top_bar/provider_picker.tsx new file mode 100644 index 000000000..be7e57d6f --- /dev/null +++ b/packages/website/ts/components/top_bar/provider_picker.tsx @@ -0,0 +1,81 @@ +import * as _ from 'lodash'; +import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; +import * as React from 'react'; +import { Blockchain } from 'ts/blockchain'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { ProviderType } from 'ts/types'; +import { colors } from 'ts/utils/colors'; +import { constants } from 'ts/utils/constants'; + +interface ProviderPickerProps { + networkId: number; + injectedProviderName: string; + providerType: ProviderType; + onToggleLedgerDialog: () => void; + dispatcher: Dispatcher; + blockchain: Blockchain; +} + +interface ProviderPickerState {} + +export class ProviderPicker extends React.Component<ProviderPickerProps, ProviderPickerState> { + public render() { + const isLedgerSelected = this.props.providerType === ProviderType.Ledger; + const menuStyle = { + padding: 10, + paddingTop: 15, + paddingBottom: 15, + }; + // Show dropdown with two options + return ( + <div style={{ width: 225, overflow: 'hidden' }}> + <RadioButtonGroup name="provider" defaultSelected={this.props.providerType}> + <RadioButton + onClick={this._onProviderRadioChanged.bind(this, ProviderType.Injected)} + style={{ ...menuStyle, backgroundColor: !isLedgerSelected && colors.grey50 }} + value={ProviderType.Injected} + label={this._renderLabel(this.props.injectedProviderName, !isLedgerSelected)} + /> + <RadioButton + onClick={this._onProviderRadioChanged.bind(this, ProviderType.Ledger)} + style={{ ...menuStyle, backgroundColor: isLedgerSelected && colors.grey50 }} + value={ProviderType.Ledger} + label={this._renderLabel('Ledger Nano S', isLedgerSelected)} + /> + </RadioButtonGroup> + </div> + ); + } + private _renderLabel(title: string, shouldShowNetwork: boolean) { + const label = ( + <div className="flex"> + <div style={{ fontSize: 14 }}>{title}</div> + {shouldShowNetwork && this._renderNetwork()} + </div> + ); + return label; + } + private _renderNetwork() { + const networkName = constants.NETWORK_NAME_BY_ID[this.props.networkId]; + return ( + <div className="flex" style={{ marginTop: 1 }}> + <div className="relative" style={{ width: 14, paddingLeft: 14 }}> + <img + src={`/images/network_icons/${networkName.toLowerCase()}.png`} + className="absolute" + style={{ top: 6, width: 10 }} + /> + </div> + <div style={{ color: colors.lightGrey, fontSize: 11 }}>{networkName}</div> + </div> + ); + } + private _onProviderRadioChanged(value: string) { + if (value === ProviderType.Ledger) { + this.props.onToggleLedgerDialog(); + } else { + // tslint:disable-next-line:no-floating-promises + this.props.blockchain.updateProviderToInjectedAsync(); + } + } +} diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx new file mode 100644 index 000000000..1a0691e83 --- /dev/null +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -0,0 +1,376 @@ +import * as _ from 'lodash'; +import Drawer from 'material-ui/Drawer'; +import Menu from 'material-ui/Menu'; +import MenuItem from 'material-ui/MenuItem'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import ReactTooltip = require('react-tooltip'); +import { Blockchain } from 'ts/blockchain'; +import { PortalMenu } from 'ts/components/portal_menu'; +import { ProviderDisplay } from 'ts/components/top_bar/provider_display'; +import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item'; +import { DropDown } from 'ts/components/ui/drop_down'; +import { Identicon } from 'ts/components/ui/identicon'; +import { DocsInfo } from 'ts/pages/documentation/docs_info'; +import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { DocsMenu, MenuSubsectionsBySection, ProviderType, Styles, WebsitePaths } from 'ts/types'; +import { colors } from 'ts/utils/colors'; +import { constants } from 'ts/utils/constants'; + +interface TopBarProps { + userAddress?: string; + networkId?: number; + injectedProviderName?: string; + providerType?: ProviderType; + onToggleLedgerDialog?: () => void; + blockchain?: Blockchain; + dispatcher?: Dispatcher; + blockchainIsLoaded: boolean; + location: Location; + docsVersion?: string; + availableDocVersions?: string[]; + menu?: DocsMenu; + menuSubsectionsBySection?: MenuSubsectionsBySection; + shouldFullWidth?: boolean; + docsInfo?: DocsInfo; + style?: React.CSSProperties; + isNightVersion?: boolean; +} + +interface TopBarState { + isDrawerOpen: boolean; +} + +const styles: Styles = { + address: { + marginRight: 12, + overflow: 'hidden', + paddingTop: 4, + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + width: 70, + }, + topBar: { + backgroundcolor: colors.white, + height: 59, + width: '100%', + position: 'relative', + top: 0, + zIndex: 1100, + paddingBottom: 1, + }, + bottomBar: { + boxShadow: 'rgba(0, 0, 0, 0.187647) 0px 1px 3px', + }, + menuItem: { + fontSize: 14, + color: colors.darkestGrey, + paddingTop: 6, + paddingBottom: 6, + marginTop: 17, + cursor: 'pointer', + fontWeight: 400, + }, +}; + +export class TopBar extends React.Component<TopBarProps, TopBarState> { + public static defaultProps: Partial<TopBarProps> = { + shouldFullWidth: false, + style: {}, + isNightVersion: false, + }; + constructor(props: TopBarProps) { + super(props); + this.state = { + isDrawerOpen: false, + }; + } + public render() { + const isNightVersion = this.props.isNightVersion; + const isFullWidthPage = this.props.shouldFullWidth; + const parentClassNames = `flex mx-auto ${isFullWidthPage ? 'pl2' : 'max-width-4'}`; + const developerSectionMenuItems = [ + <Link key="subMenuItem-zeroEx" to={WebsitePaths.ZeroExJs} className="text-decoration-none"> + <MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="0x.js" /> + </Link>, + <Link key="subMenuItem-smartContracts" to={WebsitePaths.SmartContracts} className="text-decoration-none"> + <MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="Smart Contracts" /> + </Link>, + <Link key="subMenuItem-0xconnect" to={WebsitePaths.Connect} className="text-decoration-none"> + <MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="0x Connect" /> + </Link>, + <a + key="subMenuItem-standard-relayer-api" + target="_blank" + className="text-decoration-none" + href={constants.URL_STANDARD_RELAYER_API_GITHUB} + > + <MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="Standard Relayer API" /> + </a>, + <a + key="subMenuItem-github" + target="_blank" + className="text-decoration-none" + href={constants.URL_GITHUB_ORG} + > + <MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="GitHub" /> + </a>, + <a + key="subMenuItem-whitePaper" + target="_blank" + className="text-decoration-none" + href={`${WebsitePaths.Whitepaper}`} + > + <MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="Whitepaper" /> + </a>, + ]; + const bottomBorderStyle = this._shouldDisplayBottomBar() ? styles.bottomBar : {}; + const fullWidthClasses = isFullWidthPage ? 'pr4' : ''; + const logoUrl = isNightVersion ? '/images/protocol_logo_white.png' : '/images/protocol_logo_black.png'; + const menuClasses = `col col-${isFullWidthPage ? '4' : '5'} ${fullWidthClasses} lg-pr0 md-pr2 sm-hide xs-hide`; + const menuIconStyle = { + fontSize: 25, + color: isNightVersion ? 'white' : 'black', + cursor: 'pointer', + paddingTop: 16, + }; + const hoverActiveNode = ( + <div className="flex relative" style={{ color: menuIconStyle.color }}> + <div style={{ paddingRight: 10 }}>Developers</div> + <div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}> + <i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} /> + </div> + </div> + ); + const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>; + return ( + <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1"> + <div className={parentClassNames}> + <div className="col col-2 sm-pl2 md-pl2 lg-pl0" style={{ paddingTop: 15 }}> + <Link to={`${WebsitePaths.Home}`} className="text-decoration-none"> + <img src={logoUrl} height="30" /> + </Link> + </div> + <div className={`col col-${isFullWidthPage ? '8' : '9'} lg-hide md-hide`} /> + <div className={`col col-${isFullWidthPage ? '6' : '5'} sm-hide xs-hide`} /> + {!this._isViewingPortal() && ( + <div className={menuClasses}> + <div className="flex justify-between"> + <DropDown + hoverActiveNode={hoverActiveNode} + popoverContent={popoverContent} + anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }} + targetOrigin={{ horizontal: 'middle', vertical: 'top' }} + style={styles.menuItem} + /> + <TopBarMenuItem + title="Wiki" + path={`${WebsitePaths.Wiki}`} + style={styles.menuItem} + isNightVersion={isNightVersion} + /> + <TopBarMenuItem + title="About" + path={`${WebsitePaths.About}`} + style={styles.menuItem} + isNightVersion={isNightVersion} + /> + <TopBarMenuItem + title="Portal DApp" + path={`${WebsitePaths.Portal}`} + isPrimary={true} + style={styles.menuItem} + className={`${isFullWidthPage && 'md-hide'}`} + isNightVersion={isNightVersion} + /> + </div> + </div> + )} + {this.props.blockchainIsLoaded && ( + <div className="sm-hide xs-hide col col-5"> + <ProviderDisplay + dispatcher={this.props.dispatcher} + userAddress={this.props.userAddress} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this.props.onToggleLedgerDialog} + blockchain={this.props.blockchain} + /> + </div> + )} + <div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}> + <div style={menuIconStyle}> + <i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} /> + </div> + </div> + </div> + {this._renderDrawer()} + </div> + ); + } + private _renderDrawer() { + return ( + <Drawer + open={this.state.isDrawerOpen} + docked={false} + openSecondary={true} + onRequestChange={this._onMenuButtonClick.bind(this)} + > + {this._renderPortalMenu()} + {this._renderDocsMenu()} + {this._renderWiki()} + <div className="pl1 py1 mt3" style={{ backgroundColor: colors.lightGrey }}> + Website + </div> + <Link to={WebsitePaths.Home} className="text-decoration-none"> + <MenuItem className="py2">Home</MenuItem> + </Link> + <Link to={`${WebsitePaths.Wiki}`} className="text-decoration-none"> + <MenuItem className="py2">Wiki</MenuItem> + </Link> + {!this._isViewing0xjsDocs() && ( + <Link to={WebsitePaths.ZeroExJs} className="text-decoration-none"> + <MenuItem className="py2">0x.js Docs</MenuItem> + </Link> + )} + {!this._isViewingConnectDocs() && ( + <Link to={WebsitePaths.Connect} className="text-decoration-none"> + <MenuItem className="py2">0x Connect Docs</MenuItem> + </Link> + )} + {!this._isViewingSmartContractsDocs() && ( + <Link to={WebsitePaths.SmartContracts} className="text-decoration-none"> + <MenuItem className="py2">Smart Contract Docs</MenuItem> + </Link> + )} + {!this._isViewingPortal() && ( + <Link to={`${WebsitePaths.Portal}`} className="text-decoration-none"> + <MenuItem className="py2">Portal DApp</MenuItem> + </Link> + )} + <a className="text-decoration-none" target="_blank" href={`${WebsitePaths.Whitepaper}`}> + <MenuItem className="py2">Whitepaper</MenuItem> + </a> + <Link to={`${WebsitePaths.About}`} className="text-decoration-none"> + <MenuItem className="py2">About</MenuItem> + </Link> + <a className="text-decoration-none" target="_blank" href={constants.URL_BLOG}> + <MenuItem className="py2">Blog</MenuItem> + </a> + <Link to={`${WebsitePaths.FAQ}`} className="text-decoration-none"> + <MenuItem className="py2" onTouchTap={this._onMenuButtonClick.bind(this)}> + FAQ + </MenuItem> + </Link> + </Drawer> + ); + } + private _renderDocsMenu(): React.ReactNode { + if ( + (!this._isViewing0xjsDocs() && !this._isViewingSmartContractsDocs() && !this._isViewingConnectDocs()) || + _.isUndefined(this.props.menu) + ) { + return undefined; + } + + const sectionTitle = `${this.props.docsInfo.displayName} Docs`; + return ( + <div className="lg-hide md-hide"> + <div className="pl1 py1" style={{ backgroundColor: colors.lightGrey }}> + {sectionTitle} + </div> + <NestedSidebarMenu + topLevelMenu={this.props.menu} + menuSubsectionsBySection={this.props.menuSubsectionsBySection} + shouldDisplaySectionHeaders={false} + onMenuItemClick={this._onMenuButtonClick.bind(this)} + selectedVersion={this.props.docsVersion} + docPath={this.props.docsInfo.websitePath} + versions={this.props.availableDocVersions} + /> + </div> + ); + } + private _renderWiki(): React.ReactNode { + if (!this._isViewingWiki()) { + return undefined; + } + + return ( + <div className="lg-hide md-hide"> + <div className="pl1 py1" style={{ backgroundColor: colors.lightGrey }}> + 0x Protocol Wiki + </div> + <NestedSidebarMenu + topLevelMenu={this.props.menuSubsectionsBySection} + menuSubsectionsBySection={this.props.menuSubsectionsBySection} + shouldDisplaySectionHeaders={false} + onMenuItemClick={this._onMenuButtonClick.bind(this)} + /> + </div> + ); + } + private _renderPortalMenu(): React.ReactNode { + if (!this._isViewingPortal()) { + return undefined; + } + + return ( + <div className="lg-hide md-hide"> + <div className="pl1 py1" style={{ backgroundColor: colors.lightGrey }}> + Portal DApp + </div> + <PortalMenu menuItemStyle={{ color: 'black' }} onClick={this._onMenuButtonClick.bind(this)} /> + </div> + ); + } + private _renderUser() { + const userAddress = this.props.userAddress; + const identiconDiameter = 26; + return ( + <div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}> + <div style={styles.address} data-tip={true} data-for="userAddressTooltip"> + {!_.isEmpty(userAddress) ? userAddress : ''} + </div> + <ReactTooltip id="userAddressTooltip">{userAddress}</ReactTooltip> + <div> + <Identicon address={userAddress} diameter={identiconDiameter} /> + </div> + </div> + ); + } + private _onMenuButtonClick() { + this.setState({ + isDrawerOpen: !this.state.isDrawerOpen, + }); + } + private _isViewingPortal() { + return _.includes(this.props.location.pathname, WebsitePaths.Portal); + } + private _isViewingFAQ() { + return _.includes(this.props.location.pathname, WebsitePaths.FAQ); + } + private _isViewing0xjsDocs() { + return _.includes(this.props.location.pathname, WebsitePaths.ZeroExJs); + } + private _isViewingConnectDocs() { + return _.includes(this.props.location.pathname, WebsitePaths.Connect); + } + private _isViewingSmartContractsDocs() { + return _.includes(this.props.location.pathname, WebsitePaths.SmartContracts); + } + private _isViewingWiki() { + return _.includes(this.props.location.pathname, WebsitePaths.Wiki); + } + private _shouldDisplayBottomBar() { + return ( + this._isViewingWiki() || + this._isViewing0xjsDocs() || + this._isViewingFAQ() || + this._isViewingSmartContractsDocs() || + this._isViewingConnectDocs() + ); + } +} diff --git a/packages/website/ts/components/top_bar/top_bar_menu_item.tsx b/packages/website/ts/components/top_bar/top_bar_menu_item.tsx new file mode 100644 index 000000000..96ee86142 --- /dev/null +++ b/packages/website/ts/components/top_bar/top_bar_menu_item.tsx @@ -0,0 +1,52 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { colors } from 'ts/utils/colors'; + +const DEFAULT_STYLE = { + color: colors.darkestGrey, +}; + +interface TopBarMenuItemProps { + title: string; + path?: string; + isPrimary?: boolean; + style?: React.CSSProperties; + className?: string; + isNightVersion?: boolean; +} + +interface TopBarMenuItemState {} + +export class TopBarMenuItem extends React.Component<TopBarMenuItemProps, TopBarMenuItemState> { + public static defaultProps: Partial<TopBarMenuItemProps> = { + isPrimary: false, + style: DEFAULT_STYLE, + className: '', + isNightVersion: false, + }; + public render() { + const primaryStyles = this.props.isPrimary + ? { + borderRadius: 4, + border: `1px solid ${this.props.isNightVersion ? colors.grey : colors.greyishPink}`, + marginTop: 15, + paddingLeft: 9, + paddingRight: 9, + width: 77, + } + : {}; + const menuItemColor = this.props.isNightVersion ? 'white' : this.props.style.color; + const linkColor = _.isUndefined(menuItemColor) ? colors.darkestGrey : menuItemColor; + return ( + <div + className={`center ${this.props.className}`} + style={{ ...this.props.style, ...primaryStyles, color: menuItemColor }} + > + <Link to={this.props.path} className="text-decoration-none" style={{ color: linkColor }}> + {this.props.title} + </Link> + </div> + ); + } +} |