aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/components/ui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/website/ts/components/ui')
-rw-r--r--packages/website/ts/components/ui/alert.tsx27
-rw-r--r--packages/website/ts/components/ui/badge.tsx58
-rw-r--r--packages/website/ts/components/ui/copy_icon.tsx81
-rw-r--r--packages/website/ts/components/ui/drop_down_menu_item.tsx117
-rw-r--r--packages/website/ts/components/ui/ethereum_address.tsx35
-rw-r--r--packages/website/ts/components/ui/etherscan_icon.tsx50
-rw-r--r--packages/website/ts/components/ui/fake_text_field.tsx35
-rw-r--r--packages/website/ts/components/ui/flash_message.tsx40
-rw-r--r--packages/website/ts/components/ui/help_tooltip.tsx22
-rw-r--r--packages/website/ts/components/ui/identicon.tsx36
-rw-r--r--packages/website/ts/components/ui/input_label.tsx27
-rw-r--r--packages/website/ts/components/ui/labeled_switcher.tsx76
-rw-r--r--packages/website/ts/components/ui/lifecycle_raised_button.tsx105
-rw-r--r--packages/website/ts/components/ui/loading.tsx36
-rw-r--r--packages/website/ts/components/ui/menu_item.tsx54
-rw-r--r--packages/website/ts/components/ui/party.tsx150
-rw-r--r--packages/website/ts/components/ui/required_label.tsx15
-rw-r--r--packages/website/ts/components/ui/simple_loading.tsx23
-rw-r--r--packages/website/ts/components/ui/swap_icon.tsx46
-rw-r--r--packages/website/ts/components/ui/token_icon.tsx29
20 files changed, 1062 insertions, 0 deletions
diff --git a/packages/website/ts/components/ui/alert.tsx b/packages/website/ts/components/ui/alert.tsx
new file mode 100644
index 000000000..bf2f0baf5
--- /dev/null
+++ b/packages/website/ts/components/ui/alert.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+import {AlertTypes} from 'ts/types';
+
+const CUSTOM_GREEN = 'rgb(137, 199, 116)';
+
+interface AlertProps {
+ type: AlertTypes;
+ message: string|React.ReactNode;
+}
+
+export function Alert(props: AlertProps) {
+ const isAlert = props.type === AlertTypes.ERROR;
+ const errMsgStyles = {
+ background: isAlert ? colors.red200 : CUSTOM_GREEN,
+ color: 'white',
+ marginTop: 10,
+ padding: 4,
+ paddingLeft: 8,
+ };
+
+ return (
+ <div className="rounded center" style={errMsgStyles}>
+ {props.message}
+ </div>
+ );
+}
diff --git a/packages/website/ts/components/ui/badge.tsx b/packages/website/ts/components/ui/badge.tsx
new file mode 100644
index 000000000..1e3bbdb99
--- /dev/null
+++ b/packages/website/ts/components/ui/badge.tsx
@@ -0,0 +1,58 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+import {Styles} from 'ts/types';
+
+const styles: Styles = {
+ badge: {
+ width: 50,
+ fontSize: 11,
+ height: 10,
+ borderRadius: 5,
+ marginTop: 25,
+ lineHeight: 0.9,
+ fontFamily: 'Roboto Mono',
+ marginLeft: 3,
+ marginRight: 3,
+ },
+};
+
+interface BadgeProps {
+ title: string;
+ backgroundColor: string;
+}
+
+interface BadgeState {
+ isHovering: boolean;
+}
+
+export class Badge extends React.Component<BadgeProps, BadgeState> {
+ constructor(props: BadgeProps) {
+ super(props);
+ this.state = {
+ isHovering: false,
+ };
+ }
+ public render() {
+ const badgeStyle = {
+ ...styles.badge,
+ backgroundColor: this.props.backgroundColor,
+ opacity: this.state.isHovering ? 0.7 : 1,
+ };
+ return (
+ <div
+ className="p1 center"
+ style={badgeStyle}
+ onMouseOver={this.setHoverState.bind(this, true)}
+ onMouseOut={this.setHoverState.bind(this, false)}
+ >
+ {this.props.title}
+ </div>
+ );
+ }
+ private setHoverState(isHovering: boolean) {
+ this.setState({
+ isHovering,
+ });
+ }
+}
diff --git a/packages/website/ts/components/ui/copy_icon.tsx b/packages/website/ts/components/ui/copy_icon.tsx
new file mode 100644
index 000000000..f8abaa59e
--- /dev/null
+++ b/packages/website/ts/components/ui/copy_icon.tsx
@@ -0,0 +1,81 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
+import {colors} from 'material-ui/styles';
+import ReactTooltip = require('react-tooltip');
+
+interface CopyIconProps {
+ data: string;
+ callToAction?: string;
+}
+
+interface CopyIconState {
+ isHovering: boolean;
+}
+
+export class CopyIcon extends React.Component<CopyIconProps, CopyIconState> {
+ private copyTooltipTimeoutId: number;
+ private copyable: HTMLInputElement;
+ constructor(props: CopyIconProps) {
+ super(props);
+ this.state = {
+ isHovering: false,
+ };
+ }
+ public componentDidUpdate() {
+ // Remove tooltip if hover away
+ if (!this.state.isHovering && this.copyTooltipTimeoutId) {
+ clearInterval(this.copyTooltipTimeoutId);
+ this.hideTooltip();
+ }
+ }
+ public render() {
+ return (
+ <div className="inline-block">
+ <CopyToClipboard text={this.props.data} onCopy={this.onCopy.bind(this)}>
+ <div
+ className="inline flex"
+ style={{cursor: 'pointer', color: colors.amber600}}
+ ref={this.setRefToProperty.bind(this)}
+ data-tip={true}
+ data-for="copy"
+ data-event="click"
+ data-iscapture={true} // This let's the click event continue to propogate
+ onMouseOver={this.setHoverState.bind(this, true)}
+ onMouseOut={this.setHoverState.bind(this, false)}
+ >
+ <div>
+ <i style={{fontSize: 15}} className="zmdi zmdi-copy" />
+ </div>
+ {this.props.callToAction &&
+ <div className="pl1">{this.props.callToAction}</div>
+ }
+ </div>
+ </CopyToClipboard>
+ <ReactTooltip id="copy">Copied!</ReactTooltip>
+ </div>
+ );
+ }
+ private setRefToProperty(el: HTMLInputElement) {
+ this.copyable = el;
+ }
+ private setHoverState(isHovering: boolean) {
+ this.setState({
+ isHovering,
+ });
+ }
+ private onCopy() {
+ if (this.copyTooltipTimeoutId) {
+ clearInterval(this.copyTooltipTimeoutId);
+ }
+
+ const tooltipLifespanMs = 1000;
+ this.copyTooltipTimeoutId = window.setTimeout(() => {
+ this.hideTooltip();
+ }, tooltipLifespanMs);
+ }
+ private hideTooltip() {
+ ReactTooltip.hide(ReactDOM.findDOMNode(this.copyable));
+ }
+}
diff --git a/packages/website/ts/components/ui/drop_down_menu_item.tsx b/packages/website/ts/components/ui/drop_down_menu_item.tsx
new file mode 100644
index 000000000..b8b7eb167
--- /dev/null
+++ b/packages/website/ts/components/ui/drop_down_menu_item.tsx
@@ -0,0 +1,117 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {
+ Link as ScrollLink,
+} from 'react-scroll';
+import {Link} from 'react-router-dom';
+import Popover from 'material-ui/Popover';
+import Menu from 'material-ui/Menu';
+import MenuItem from 'material-ui/MenuItem';
+import {Styles, WebsitePaths} from 'ts/types';
+
+const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300;
+const CUSTOM_LIGHT_GRAY = '#848484';
+const DEFAULT_STYLE = {
+ fontSize: 14,
+};
+
+interface DropDownMenuItemProps {
+ title: string;
+ subMenuItems: React.ReactNode[];
+ style?: React.CSSProperties;
+ menuItemStyle?: React.CSSProperties;
+ isNightVersion?: boolean;
+}
+
+interface DropDownMenuItemState {
+ isDropDownOpen: boolean;
+ anchorEl?: HTMLInputElement;
+}
+
+export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> {
+ public static defaultProps: Partial<DropDownMenuItemProps> = {
+ style: DEFAULT_STYLE,
+ menuItemStyle: DEFAULT_STYLE,
+ isNightVersion: false,
+ };
+ private isHovering: boolean;
+ private popoverCloseCheckIntervalId: number;
+ constructor(props: DropDownMenuItemProps) {
+ super(props);
+ this.state = {
+ isDropDownOpen: false,
+ };
+ }
+ public componentDidMount() {
+ this.popoverCloseCheckIntervalId = window.setInterval(() => {
+ this.checkIfShouldClosePopover();
+ }, CHECK_CLOSE_POPOVER_INTERVAL_MS);
+ }
+ public componentWillUnmount() {
+ window.clearInterval(this.popoverCloseCheckIntervalId);
+ }
+ public render() {
+ const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color;
+ return (
+ <div
+ style={{...this.props.style, color: colorStyle}}
+ onMouseEnter={this.onHover.bind(this)}
+ onMouseLeave={this.onHoverOff.bind(this)}
+ >
+ <div className="flex relative">
+ <div style={{paddingRight: 10}}>
+ {this.props.title}
+ </div>
+ <div className="absolute" style={{paddingLeft: 3, right: 3, top: -2}}>
+ <i className="zmdi zmdi-caret-right" style={{fontSize: 22}} />
+ </div>
+ </div>
+ <Popover
+ open={this.state.isDropDownOpen}
+ anchorEl={this.state.anchorEl}
+ anchorOrigin={{horizontal: 'middle', vertical: 'bottom'}}
+ targetOrigin={{horizontal: 'middle', vertical: 'top'}}
+ onRequestClose={this.closePopover.bind(this)}
+ useLayerForClickAway={false}
+ >
+ <div
+ onMouseEnter={this.onHover.bind(this)}
+ onMouseLeave={this.onHoverOff.bind(this)}
+ >
+ <Menu style={{color: CUSTOM_LIGHT_GRAY}}>
+ {this.props.subMenuItems}
+ </Menu>
+ </div>
+ </Popover>
+ </div>
+ );
+ }
+ private onHover(event: React.FormEvent<HTMLInputElement>) {
+ this.isHovering = true;
+ this.checkIfShouldOpenPopover(event);
+ }
+ private checkIfShouldOpenPopover(event: React.FormEvent<HTMLInputElement>) {
+ if (this.state.isDropDownOpen) {
+ return; // noop
+ }
+
+ this.setState({
+ isDropDownOpen: true,
+ anchorEl: event.currentTarget,
+ });
+ }
+ private onHoverOff(event: React.FormEvent<HTMLInputElement>) {
+ this.isHovering = false;
+ }
+ private checkIfShouldClosePopover() {
+ if (!this.state.isDropDownOpen || this.isHovering) {
+ return; // noop
+ }
+ this.closePopover();
+ }
+ private closePopover() {
+ this.setState({
+ isDropDownOpen: false,
+ });
+ }
+}
diff --git a/packages/website/ts/components/ui/ethereum_address.tsx b/packages/website/ts/components/ui/ethereum_address.tsx
new file mode 100644
index 000000000..c3d03b78c
--- /dev/null
+++ b/packages/website/ts/components/ui/ethereum_address.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import {EtherScanIcon} from 'ts/components/ui/etherscan_icon';
+import ReactTooltip = require('react-tooltip');
+import {EtherscanLinkSuffixes} from 'ts/types';
+import {utils} from 'ts/utils/utils';
+
+interface EthereumAddressProps {
+ address: string;
+ networkId: number;
+}
+
+export const EthereumAddress = (props: EthereumAddressProps) => {
+ const tooltipId = `${props.address}-ethereum-address`;
+ const truncatedAddress = utils.getAddressBeginAndEnd(props.address);
+ return (
+ <div>
+ <div
+ className="inline"
+ style={{fontSize: 13}}
+ data-tip={true}
+ data-for={tooltipId}
+ >
+ {truncatedAddress}
+ </div>
+ <div className="pl1 inline">
+ <EtherScanIcon
+ addressOrTxHash={props.address}
+ networkId={props.networkId}
+ etherscanLinkSuffixes={EtherscanLinkSuffixes.address}
+ />
+ </div>
+ <ReactTooltip id={tooltipId}>{props.address}</ReactTooltip>
+ </div>
+ );
+};
diff --git a/packages/website/ts/components/ui/etherscan_icon.tsx b/packages/website/ts/components/ui/etherscan_icon.tsx
new file mode 100644
index 000000000..12044f44b
--- /dev/null
+++ b/packages/website/ts/components/ui/etherscan_icon.tsx
@@ -0,0 +1,50 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import ReactTooltip = require('react-tooltip');
+import {colors} from 'material-ui/styles';
+import {EtherscanLinkSuffixes} from 'ts/types';
+import {utils} from 'ts/utils/utils';
+
+interface EtherScanIconProps {
+ addressOrTxHash: string;
+ etherscanLinkSuffixes: EtherscanLinkSuffixes;
+ networkId: number;
+}
+
+export const EtherScanIcon = (props: EtherScanIconProps) => {
+ const etherscanLinkIfExists = utils.getEtherScanLinkIfExists(
+ props.addressOrTxHash, props.networkId, EtherscanLinkSuffixes.address,
+ );
+ const transactionTooltipId = `${props.addressOrTxHash}-etherscan-icon-tooltip`;
+ return (
+ <div className="inline">
+ {!_.isUndefined(etherscanLinkIfExists) ?
+ <a
+ href={etherscanLinkIfExists}
+ target="_blank"
+ >
+ {renderIcon()}
+ </a> :
+ <div
+ className="inline"
+ data-tip={true}
+ data-for={transactionTooltipId}
+ >
+ {renderIcon()}
+ <ReactTooltip id={transactionTooltipId}>
+ Your network (id: {props.networkId}) is not supported by Etherscan
+ </ReactTooltip>
+ </div>
+ }
+ </div>
+ );
+};
+
+function renderIcon() {
+ return (
+ <i
+ style={{color: colors.amber600}}
+ className="zmdi zmdi-open-in-new"
+ />
+ );
+}
diff --git a/packages/website/ts/components/ui/fake_text_field.tsx b/packages/website/ts/components/ui/fake_text_field.tsx
new file mode 100644
index 000000000..372785c2f
--- /dev/null
+++ b/packages/website/ts/components/ui/fake_text_field.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+import {InputLabel} from 'ts/components/ui/input_label';
+import {Styles} from 'ts/types';
+
+const styles: Styles = {
+ hr: {
+ borderBottom: '1px solid rgb(224, 224, 224)',
+ borderLeft: 'none rgb(224, 224, 224)',
+ borderRight: 'none rgb(224, 224, 224)',
+ borderTop: 'none rgb(224, 224, 224)',
+ bottom: 6,
+ boxSizing: 'content-box',
+ margin: 0,
+ position: 'absolute',
+ width: '100%',
+ },
+};
+
+interface FakeTextFieldProps {
+ label?: React.ReactNode | string;
+ children?: any;
+}
+
+export function FakeTextField(props: FakeTextFieldProps) {
+ return (
+ <div className="relative">
+ {props.label !== '' && <InputLabel text={props.label} />}
+ <div className="pb2" style={{height: 23}}>
+ {props.children}
+ </div>
+ <hr style={styles.hr} />
+ </div>
+ );
+}
diff --git a/packages/website/ts/components/ui/flash_message.tsx b/packages/website/ts/components/ui/flash_message.tsx
new file mode 100644
index 000000000..684aeef68
--- /dev/null
+++ b/packages/website/ts/components/ui/flash_message.tsx
@@ -0,0 +1,40 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import Snackbar from 'material-ui/Snackbar';
+import {Dispatcher} from 'ts/redux/dispatcher';
+
+const SHOW_DURATION_MS = 4000;
+
+interface FlashMessageProps {
+ dispatcher: Dispatcher;
+ flashMessage?: string|React.ReactNode;
+ showDurationMs?: number;
+ bodyStyle?: React.CSSProperties;
+}
+
+interface FlashMessageState {}
+
+export class FlashMessage extends React.Component<FlashMessageProps, FlashMessageState> {
+ public static defaultProps: Partial<FlashMessageProps> = {
+ showDurationMs: SHOW_DURATION_MS,
+ bodyStyle: {},
+ };
+ public render() {
+ if (!_.isUndefined(this.props.flashMessage)) {
+ return (
+ <Snackbar
+ open={true}
+ message={this.props.flashMessage}
+ autoHideDuration={this.props.showDurationMs}
+ onRequestClose={this.onClose.bind(this)}
+ bodyStyle={this.props.bodyStyle}
+ />
+ );
+ } else {
+ return null;
+ }
+ }
+ private onClose() {
+ this.props.dispatcher.hideFlashMessage();
+ }
+}
diff --git a/packages/website/ts/components/ui/help_tooltip.tsx b/packages/website/ts/components/ui/help_tooltip.tsx
new file mode 100644
index 000000000..003b795ef
--- /dev/null
+++ b/packages/website/ts/components/ui/help_tooltip.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import ReactTooltip = require('react-tooltip');
+
+interface HelpTooltipProps {
+ style?: React.CSSProperties;
+ explanation: React.ReactNode;
+}
+
+export const HelpTooltip = (props: HelpTooltipProps) => {
+ return (
+ <div
+ style={{...props.style}}
+ className="inline-block"
+ data-tip={props.explanation}
+ data-for="helpTooltip"
+ data-multiline={true}
+ >
+ <i style={{fontSize: 16}} className="zmdi zmdi-help" />
+ <ReactTooltip id="helpTooltip" />
+ </div>
+ );
+};
diff --git a/packages/website/ts/components/ui/identicon.tsx b/packages/website/ts/components/ui/identicon.tsx
new file mode 100644
index 000000000..814548fb4
--- /dev/null
+++ b/packages/website/ts/components/ui/identicon.tsx
@@ -0,0 +1,36 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {constants} from 'ts/utils/constants';
+import blockies = require('blockies');
+
+interface IdenticonProps {
+ address: string;
+ diameter: number;
+ style?: React.CSSProperties;
+}
+
+interface IdenticonState {}
+
+export class Identicon extends React.Component<IdenticonProps, IdenticonState> {
+ public static defaultProps: Partial<IdenticonProps> = {
+ style: {},
+ };
+ public render() {
+ let address = this.props.address;
+ if (_.isEmpty(address)) {
+ address = constants.NULL_ADDRESS;
+ }
+ const diameter = this.props.diameter;
+ const icon = blockies({
+ seed: address.toLowerCase(),
+ });
+ return (
+ <div
+ className="circle mx-auto relative transitionFix"
+ style={{width: diameter, height: diameter, overflow: 'hidden', ...this.props.style}}
+ >
+ <img src={icon.toDataURL()} style={{width: diameter, height: diameter, imageRendering: 'pixelated'}}/>
+ </div>
+ );
+ }
+}
diff --git a/packages/website/ts/components/ui/input_label.tsx b/packages/website/ts/components/ui/input_label.tsx
new file mode 100644
index 000000000..5866c70b6
--- /dev/null
+++ b/packages/website/ts/components/ui/input_label.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+
+export interface InputLabelProps {
+ text: string | Element | React.ReactNode;
+}
+
+const styles = {
+ label: {
+ color: colors.grey500,
+ fontSize: 12,
+ pointerEvents: 'none',
+ textAlign: 'left',
+ transform: 'scale(0.75) translate(0px, -28px)',
+ transformOrigin: 'left top 0px',
+ transition: 'all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms',
+ userSelect: 'none',
+ width: 240,
+ zIndex: 1,
+ },
+};
+
+export const InputLabel = (props: InputLabelProps) => {
+ return (
+ <label style={styles.label}>{props.text}</label>
+ );
+};
diff --git a/packages/website/ts/components/ui/labeled_switcher.tsx b/packages/website/ts/components/ui/labeled_switcher.tsx
new file mode 100644
index 000000000..3ed8ba0a4
--- /dev/null
+++ b/packages/website/ts/components/ui/labeled_switcher.tsx
@@ -0,0 +1,76 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+
+const CUSTOM_BLUE = '#63A6F1';
+
+interface LabeledSwitcherProps {
+ labelLeft: string;
+ labelRight: string;
+ isLeftInitiallySelected: boolean;
+ onLeftLabelClickAsync: () => Promise<boolean>;
+ onRightLabelClickAsync: () => Promise<boolean>;
+}
+
+interface LabeledSwitcherState {
+ isLeftSelected: boolean;
+}
+
+export class LabeledSwitcher extends React.Component<LabeledSwitcherProps, LabeledSwitcherState> {
+ constructor(props: LabeledSwitcherProps) {
+ super(props);
+ this.state = {
+ isLeftSelected: props.isLeftInitiallySelected,
+ };
+ }
+ public render() {
+ const isLeft = true;
+ return (
+ <div
+ className="rounded clearfix"
+ >
+ {this.renderLabel(this.props.labelLeft, isLeft, this.state.isLeftSelected)}
+ {this.renderLabel(this.props.labelRight, !isLeft, !this.state.isLeftSelected)}
+ </div>
+ );
+ }
+ private renderLabel(title: string, isLeft: boolean, isSelected: boolean) {
+ const borderStyle = `2px solid ${isSelected ? '#4F8BCF' : '#DADADA'}`;
+ const style = {
+ cursor: 'pointer',
+ backgroundColor: isSelected ? CUSTOM_BLUE : colors.grey200,
+ color: isSelected ? 'white' : '#A5A5A5',
+ boxShadow: isSelected ? `inset 0px 0px 4px #4083CE` : 'inset 0px 0px 4px #F7F6F6',
+ borderTop: borderStyle,
+ borderBottom: borderStyle,
+ [isLeft ? 'borderLeft' : 'borderRight']: borderStyle,
+ paddingTop: 12,
+ paddingBottom: 12,
+ };
+ return (
+ <div
+ className={`col col-6 center p1 ${isLeft ? 'rounded-left' : 'rounded-right'}`}
+ style={style}
+ onClick={this.onLabelClickAsync.bind(this, isLeft)}
+ >
+ {title}
+ </div>
+ );
+ }
+ private async onLabelClickAsync(isLeft: boolean): Promise<void> {
+ this.setState({
+ isLeftSelected: isLeft,
+ });
+ let didSucceed;
+ if (isLeft) {
+ didSucceed = await this.props.onLeftLabelClickAsync();
+ } else {
+ didSucceed = await this.props.onRightLabelClickAsync();
+ }
+ if (!didSucceed) {
+ this.setState({
+ isLeftSelected: !isLeft,
+ });
+ }
+ }
+}
diff --git a/packages/website/ts/components/ui/lifecycle_raised_button.tsx b/packages/website/ts/components/ui/lifecycle_raised_button.tsx
new file mode 100644
index 000000000..e93c80ba4
--- /dev/null
+++ b/packages/website/ts/components/ui/lifecycle_raised_button.tsx
@@ -0,0 +1,105 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {utils} from 'ts/utils/utils';
+import {Token} from 'ts/types';
+import {Blockchain} from 'ts/blockchain';
+import RaisedButton from 'material-ui/RaisedButton';
+
+const COMPLETE_STATE_SHOW_LENGTH_MS = 2000;
+
+enum ButtonState {
+ READY,
+ LOADING,
+ COMPLETE,
+};
+
+interface LifeCycleRaisedButtonProps {
+ isHidden?: boolean;
+ isDisabled?: boolean;
+ isPrimary?: boolean;
+ labelReady: React.ReactNode|string;
+ labelLoading: React.ReactNode|string;
+ labelComplete: React.ReactNode|string;
+ onClickAsyncFn: () => boolean;
+ backgroundColor?: string;
+ labelColor?: string;
+}
+
+interface LifeCycleRaisedButtonState {
+ buttonState: ButtonState;
+}
+
+export class LifeCycleRaisedButton extends
+ React.Component<LifeCycleRaisedButtonProps, LifeCycleRaisedButtonState> {
+ public static defaultProps: Partial<LifeCycleRaisedButtonProps> = {
+ isDisabled: false,
+ backgroundColor: 'white',
+ labelColor: 'rgb(97, 97, 97)',
+ };
+ private buttonTimeoutId: number;
+ private didUnmount: boolean;
+ constructor(props: LifeCycleRaisedButtonProps) {
+ super(props);
+ this.state = {
+ buttonState: ButtonState.READY,
+ };
+ }
+ public componentWillUnmount() {
+ clearTimeout(this.buttonTimeoutId);
+ this.didUnmount = true;
+ }
+ public render() {
+ if (this.props.isHidden === true) {
+ return <span />;
+ }
+
+ let label;
+ switch (this.state.buttonState) {
+ case ButtonState.READY:
+ label = this.props.labelReady;
+ break;
+ case ButtonState.LOADING:
+ label = this.props.labelLoading;
+ break;
+ case ButtonState.COMPLETE:
+ label = this.props.labelComplete;
+ break;
+ default:
+ throw utils.spawnSwitchErr('ButtonState', this.state.buttonState);
+ }
+ return (
+ <RaisedButton
+ primary={this.props.isPrimary}
+ label={label}
+ style={{width: '100%'}}
+ backgroundColor={this.props.backgroundColor}
+ labelColor={this.props.labelColor}
+ onTouchTap={this.onClickAsync.bind(this)}
+ disabled={this.props.isDisabled || this.state.buttonState !== ButtonState.READY}
+ />
+ );
+ }
+ public async onClickAsync() {
+ this.setState({
+ buttonState: ButtonState.LOADING,
+ });
+ const didSucceed = await this.props.onClickAsyncFn();
+ if (this.didUnmount) {
+ return; // noop since unmount called before async callback returned.
+ }
+ if (didSucceed) {
+ this.setState({
+ buttonState: ButtonState.COMPLETE,
+ });
+ this.buttonTimeoutId = window.setTimeout(() => {
+ this.setState({
+ buttonState: ButtonState.READY,
+ });
+ }, COMPLETE_STATE_SHOW_LENGTH_MS);
+ } else {
+ this.setState({
+ buttonState: ButtonState.READY,
+ });
+ }
+ }
+}
diff --git a/packages/website/ts/components/ui/loading.tsx b/packages/website/ts/components/ui/loading.tsx
new file mode 100644
index 000000000..39c119d8f
--- /dev/null
+++ b/packages/website/ts/components/ui/loading.tsx
@@ -0,0 +1,36 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import Paper from 'material-ui/Paper';
+import {utils} from 'ts/utils/utils';
+import {DefaultPlayer as Video} from 'react-html5video';
+import 'react-html5video/dist/styles.css';
+
+interface LoadingProps {}
+
+interface LoadingState {}
+
+export class Loading extends React.Component<LoadingProps, LoadingState> {
+ public render() {
+ return (
+ <div className="pt4 sm-px2 sm-pt2 sm-m1" style={{height: 500}}>
+ <Paper className="mx-auto" style={{maxWidth: 400}}>
+ {utils.isUserOnMobile() ?
+ <img className="p1" src="/gifs/0xAnimation.gif" width="96%" /> :
+ <div style={{pointerEvents: 'none'}}>
+ <Video
+ autoPlay={true}
+ loop={true}
+ muted={true}
+ controls={[]}
+ poster="/images/loading_poster.png"
+ >
+ <source src="/videos/0xAnimation.mp4" type="video/mp4" />
+ </Video>
+ </div>
+ }
+ <div className="center pt2" style={{paddingBottom: 11}}>Connecting to the blockchain...</div>
+ </Paper>
+ </div>
+ );
+ }
+}
diff --git a/packages/website/ts/components/ui/menu_item.tsx b/packages/website/ts/components/ui/menu_item.tsx
new file mode 100644
index 000000000..b9caa91fb
--- /dev/null
+++ b/packages/website/ts/components/ui/menu_item.tsx
@@ -0,0 +1,54 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {Link} from 'react-router-dom';
+import {Styles} from 'ts/types';
+import {constants} from 'ts/utils/constants';
+import {colors} from 'material-ui/styles';
+
+interface MenuItemProps {
+ to: string;
+ style?: React.CSSProperties;
+ onClick?: () => void;
+ className?: string;
+}
+
+interface MenuItemState {
+ isHovering: boolean;
+}
+
+export class MenuItem extends React.Component<MenuItemProps, MenuItemState> {
+ public static defaultProps: Partial<MenuItemProps> = {
+ onClick: _.noop,
+ className: '',
+ };
+ public constructor(props: MenuItemProps) {
+ super(props);
+ this.state = {
+ isHovering: false,
+ };
+ }
+ public render() {
+ const menuItemStyles = {
+ cursor: 'pointer',
+ opacity: this.state.isHovering ? 0.5 : 1,
+ };
+ return (
+ <Link to={this.props.to} style={{textDecoration: 'none', ...this.props.style}}>
+ <div
+ onClick={this.props.onClick.bind(this)}
+ className={`mx-auto ${this.props.className}`}
+ style={menuItemStyles}
+ onMouseEnter={this.onToggleHover.bind(this, true)}
+ onMouseLeave={this.onToggleHover.bind(this, false)}
+ >
+ {this.props.children}
+ </div>
+ </Link>
+ );
+ }
+ private onToggleHover(isHovering: boolean) {
+ this.setState({
+ isHovering,
+ });
+ }
+}
diff --git a/packages/website/ts/components/ui/party.tsx b/packages/website/ts/components/ui/party.tsx
new file mode 100644
index 000000000..b72e75181
--- /dev/null
+++ b/packages/website/ts/components/ui/party.tsx
@@ -0,0 +1,150 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import ReactTooltip = require('react-tooltip');
+import {colors} from 'material-ui/styles';
+import {Identicon} from 'ts/components/ui/identicon';
+import {EtherscanLinkSuffixes} from 'ts/types';
+import {utils} from 'ts/utils/utils';
+import {EthereumAddress} from 'ts/components/ui/ethereum_address';
+
+const MIN_ADDRESS_WIDTH = 60;
+const IMAGE_DIMENSION = 100;
+const IDENTICON_DIAMETER = 95;
+const CHECK_MARK_GREEN = 'rgb(0, 195, 62)';
+
+interface PartyProps {
+ label: string;
+ address: string;
+ networkId: number;
+ alternativeImage?: string;
+ identiconDiameter?: number;
+ identiconStyle?: React.CSSProperties;
+ isInTokenRegistry?: boolean;
+ hasUniqueNameAndSymbol?: boolean;
+}
+
+interface PartyState {}
+
+export class Party extends React.Component<PartyProps, PartyState> {
+ public static defaultProps: Partial<PartyProps> = {
+ identiconStyle: {},
+ identiconDiameter: IDENTICON_DIAMETER,
+ };
+ public render() {
+ const label = this.props.label;
+ const address = this.props.address;
+ const tooltipId = `${label}-${address}-tooltip`;
+ const identiconDiameter = this.props.identiconDiameter;
+ const addressWidth = identiconDiameter > MIN_ADDRESS_WIDTH ?
+ identiconDiameter : MIN_ADDRESS_WIDTH;
+ const emptyIdenticonStyles = {
+ width: identiconDiameter,
+ height: identiconDiameter,
+ backgroundColor: 'lightgray',
+ marginTop: 13,
+ marginBottom: 10,
+ };
+ const tokenImageStyle = {
+ width: IMAGE_DIMENSION,
+ height: IMAGE_DIMENSION,
+ };
+ const etherscanLinkIfExists = utils.getEtherScanLinkIfExists(
+ this.props.address, this.props.networkId, EtherscanLinkSuffixes.address,
+ );
+ const isRegistered = this.props.isInTokenRegistry;
+ const registeredTooltipId = `${this.props.address}-${isRegistered}-registeredTooltip`;
+ const uniqueNameAndSymbolTooltipId = `${this.props.address}-${isRegistered}-uniqueTooltip`;
+ return (
+ <div style={{overflow: 'hidden'}}>
+ <div className="pb1 center">{label}</div>
+ {_.isEmpty(address) ?
+ <div
+ className="circle mx-auto"
+ style={emptyIdenticonStyles}
+ /> :
+ <a
+ href={etherscanLinkIfExists}
+ target="_blank"
+ >
+ {isRegistered && !_.isUndefined(this.props.alternativeImage) ?
+ <img
+ style={tokenImageStyle}
+ src={this.props.alternativeImage}
+ /> :
+ <div
+ className="mx-auto"
+ style={{height: IMAGE_DIMENSION, width: IMAGE_DIMENSION}}
+ >
+ <Identicon
+ address={this.props.address}
+ diameter={identiconDiameter}
+ style={this.props.identiconStyle}
+ />
+ </div>
+ }
+ </a>
+ }
+ <div
+ className="mx-auto center pt1"
+ >
+ <div style={{height: 25}}>
+ <EthereumAddress address={address} networkId={this.props.networkId} />
+ </div>
+ {!_.isUndefined(this.props.isInTokenRegistry) &&
+ <div>
+ <div
+ data-tip={true}
+ data-for={registeredTooltipId}
+ className="mx-auto"
+ style={{fontSize: 13, width: 127}}
+ >
+ <span style={{color: isRegistered ? CHECK_MARK_GREEN : colors.red500}}>
+ <i
+ className={`zmdi ${isRegistered ? 'zmdi-check-circle' : 'zmdi-alert-triangle'}`}
+ />
+ </span>{' '}
+ <span>{isRegistered ? 'Registered' : 'Unregistered'} token</span>
+ <ReactTooltip id={registeredTooltipId}>
+ {isRegistered ?
+ <div>
+ This token address was found in the token registry<br />
+ smart contract and is therefore believed to be a<br />
+ legitimate token.
+ </div> :
+ <div>
+ This token is not included in the token registry<br />
+ smart contract. We cannot guarantee the legitimacy<br />
+ of this token. Make sure to verify its address on Etherscan.
+ </div>
+ }
+ </ReactTooltip>
+ </div>
+ </div>
+ }
+ {!_.isUndefined(this.props.hasUniqueNameAndSymbol) && !this.props.hasUniqueNameAndSymbol &&
+ <div>
+ <div
+ data-tip={true}
+ data-for={uniqueNameAndSymbolTooltipId}
+ className="mx-auto"
+ style={{fontSize: 13, width: 127}}
+ >
+ <span style={{color: colors.red500}}>
+ <i
+ className="zmdi zmdi-alert-octagon"
+ />
+ </span>{' '}
+ <span>Suspicious token</span>
+ <ReactTooltip id={uniqueNameAndSymbolTooltipId}>
+ This token shares it's name, symbol or both with<br />
+ a token in the 0x Token Registry but it has a different<br />
+ smart contract address. This is most likely a scam token!
+ </ReactTooltip>
+ </div>
+ </div>
+ }
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/packages/website/ts/components/ui/required_label.tsx b/packages/website/ts/components/ui/required_label.tsx
new file mode 100644
index 000000000..f9c73157a
--- /dev/null
+++ b/packages/website/ts/components/ui/required_label.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+
+export interface RequiredLabelProps {
+ label: string|React.ReactNode;
+}
+
+export const RequiredLabel = (props: RequiredLabelProps) => {
+ return (
+ <span>
+ {props.label}
+ <span style={{color: colors.red600}}>*</span>
+ </span>
+ );
+};
diff --git a/packages/website/ts/components/ui/simple_loading.tsx b/packages/website/ts/components/ui/simple_loading.tsx
new file mode 100644
index 000000000..12d09ecc4
--- /dev/null
+++ b/packages/website/ts/components/ui/simple_loading.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+import CircularProgress from 'material-ui/CircularProgress';
+
+export interface SimpleLoadingProps {
+ message: string;
+}
+
+export const SimpleLoading = (props: SimpleLoadingProps) => {
+ return (
+ <div className="mx-auto pt3" style={{maxWidth: 400, height: 409}}>
+ <div
+ className="relative"
+ style={{top: '50%', transform: 'translateY(-50%)', height: 95}}
+ >
+ <CircularProgress />
+ <div className="pt3 pb3">
+ {props.message}
+ </div>
+ </div>
+ </div>
+ );
+};
diff --git a/packages/website/ts/components/ui/swap_icon.tsx b/packages/website/ts/components/ui/swap_icon.tsx
new file mode 100644
index 000000000..89bb33d55
--- /dev/null
+++ b/packages/website/ts/components/ui/swap_icon.tsx
@@ -0,0 +1,46 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {constants} from 'ts/utils/constants';
+import {colors} from 'material-ui/styles';
+
+interface SwapIconProps {
+ swapTokensFn: () => void;
+}
+
+interface SwapIconState {
+ isHovering: boolean;
+}
+
+export class SwapIcon extends React.Component<SwapIconProps, SwapIconState> {
+ public constructor(props: SwapIconProps) {
+ super(props);
+ this.state = {
+ isHovering: false,
+ };
+ }
+ public render() {
+ const swapStyles = {
+ color: this.state.isHovering ? colors.amber600 : colors.amber800,
+ fontSize: 50,
+ };
+ return (
+ <div
+ className="mx-auto pt4"
+ style={{cursor: 'pointer', height: 50, width: 37.5}}
+ onClick={this.props.swapTokensFn}
+ onMouseEnter={this.onToggleHover.bind(this, true)}
+ onMouseLeave={this.onToggleHover.bind(this, false)}
+ >
+ <i
+ style={swapStyles}
+ className="zmdi zmdi-swap"
+ />
+ </div>
+ );
+ }
+ private onToggleHover(isHovering: boolean) {
+ this.setState({
+ isHovering,
+ });
+ }
+}
diff --git a/packages/website/ts/components/ui/token_icon.tsx b/packages/website/ts/components/ui/token_icon.tsx
new file mode 100644
index 000000000..168c09bd4
--- /dev/null
+++ b/packages/website/ts/components/ui/token_icon.tsx
@@ -0,0 +1,29 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {Token} from 'ts/types';
+import {Identicon} from 'ts/components/ui/identicon';
+
+interface TokenIconProps {
+ token: Token;
+ diameter: number;
+}
+
+interface TokenIconState {}
+
+export class TokenIcon extends React.Component<TokenIconProps, TokenIconState> {
+ public render() {
+ const token = this.props.token;
+ const diameter = this.props.diameter;
+ return (
+ <div>
+ {(token.isRegistered && !_.isUndefined(token.iconUrl)) ?
+ <img
+ style={{width: diameter, height: diameter}}
+ src={token.iconUrl}
+ /> :
+ <Identicon address={token.address} diameter={diameter} />
+ }
+ </div>
+ );
+ }
+}