aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/components/inputs
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2017-11-22 04:03:08 +0800
committerFabio Berger <me@fabioberger.com>2017-11-22 04:03:08 +0800
commit3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b (patch)
treef101656799da807489253e17bea7abfaea90b62d /packages/website/ts/components/inputs
parent037f466e1f80f635b48f3235258402e2ce75fb7b (diff)
downloaddexon-0x-contracts-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.tar.gz
dexon-0x-contracts-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.tar.zst
dexon-0x-contracts-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.zip
Add website to mono repo, update packages to align with existing sub-packages, use new subscribeAsync 0x.js method
Diffstat (limited to 'packages/website/ts/components/inputs')
-rw-r--r--packages/website/ts/components/inputs/address_input.tsx74
-rw-r--r--packages/website/ts/components/inputs/allowance_toggle.tsx94
-rw-r--r--packages/website/ts/components/inputs/balance_bounded_input.tsx160
-rw-r--r--packages/website/ts/components/inputs/eth_amount_input.tsx51
-rw-r--r--packages/website/ts/components/inputs/expiration_input.tsx108
-rw-r--r--packages/website/ts/components/inputs/hash_input.tsx65
-rw-r--r--packages/website/ts/components/inputs/identicon_address_input.tsx56
-rw-r--r--packages/website/ts/components/inputs/token_amount_input.tsx69
-rw-r--r--packages/website/ts/components/inputs/token_input.tsx107
9 files changed, 784 insertions, 0 deletions
diff --git a/packages/website/ts/components/inputs/address_input.tsx b/packages/website/ts/components/inputs/address_input.tsx
new file mode 100644
index 000000000..57ad7a5e2
--- /dev/null
+++ b/packages/website/ts/components/inputs/address_input.tsx
@@ -0,0 +1,74 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {isAddress} from 'ethereum-address';
+import TextField from 'material-ui/TextField';
+import {colors} from 'material-ui/styles';
+import {Blockchain} from 'ts/blockchain';
+import {RequiredLabel} from 'ts/components/ui/required_label';
+
+interface AddressInputProps {
+ disabled?: boolean;
+ initialAddress: string;
+ isRequired?: boolean;
+ hintText?: string;
+ shouldHideLabel?: boolean;
+ label?: string;
+ shouldShowIncompleteErrs?: boolean;
+ updateAddress: (address?: string) => void;
+}
+
+interface AddressInputState {
+ address: string;
+ errMsg: string;
+}
+
+export class AddressInput extends React.Component<AddressInputProps, AddressInputState> {
+ constructor(props: AddressInputProps) {
+ super(props);
+ this.state = {
+ address: this.props.initialAddress,
+ errMsg: '',
+ };
+ }
+ public componentWillReceiveProps(nextProps: AddressInputProps) {
+ if (nextProps.shouldShowIncompleteErrs && this.props.isRequired &&
+ this.state.address === '') {
+ this.setState({
+ errMsg: 'Address is required',
+ });
+ }
+ }
+ public render() {
+ const label = this.props.isRequired ? <RequiredLabel label={this.props.label} /> :
+ this.props.label;
+ const labelDisplay = this.props.shouldHideLabel ? 'hidden' : 'block';
+ const hintText = this.props.hintText ? this.props.hintText : '';
+ return (
+ <div className="overflow-hidden">
+ <TextField
+ id={`address-field-${this.props.label}`}
+ disabled={_.isUndefined(this.props.disabled) ? false : this.props.disabled}
+ fullWidth={true}
+ hintText={hintText}
+ floatingLabelFixed={true}
+ floatingLabelStyle={{color: colors.grey500, display: labelDisplay}}
+ floatingLabelText={label}
+ errorText={this.state.errMsg}
+ value={this.state.address}
+ onChange={this.onOrderTakerAddressUpdated.bind(this)}
+ />
+ </div>
+ );
+ }
+ private onOrderTakerAddressUpdated(e: any) {
+ const address = e.target.value.toLowerCase();
+ const isValidAddress = isAddress(address) || address === '';
+ const errMsg = isValidAddress ? '' : 'Invalid ethereum address';
+ this.setState({
+ address,
+ errMsg,
+ });
+ const addressIfValid = isValidAddress ? address : undefined;
+ this.props.updateAddress(addressIfValid);
+ }
+}
diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx
new file mode 100644
index 000000000..f02112253
--- /dev/null
+++ b/packages/website/ts/components/inputs/allowance_toggle.tsx
@@ -0,0 +1,94 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import BigNumber from 'bignumber.js';
+import Toggle from 'material-ui/Toggle';
+import {Blockchain} from 'ts/blockchain';
+import {Dispatcher} from 'ts/redux/dispatcher';
+import {Token, TokenState, BalanceErrs} from 'ts/types';
+import {utils} from 'ts/utils/utils';
+import {errorReporter} from 'ts/utils/error_reporter';
+
+const DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1);
+
+interface AllowanceToggleProps {
+ blockchain: Blockchain;
+ dispatcher: Dispatcher;
+ onErrorOccurred: (errType: BalanceErrs) => void;
+ token: Token;
+ tokenState: TokenState;
+ userAddress: string;
+}
+
+interface AllowanceToggleState {
+ isSpinnerVisible: boolean;
+ prevAllowance: BigNumber;
+}
+
+export class AllowanceToggle extends React.Component<AllowanceToggleProps, AllowanceToggleState> {
+ constructor(props: AllowanceToggleProps) {
+ super(props);
+ this.state = {
+ isSpinnerVisible: false,
+ prevAllowance: props.tokenState.allowance,
+ };
+ }
+ public componentWillReceiveProps(nextProps: AllowanceToggleProps) {
+ if (!nextProps.tokenState.allowance.eq(this.state.prevAllowance)) {
+ this.setState({
+ isSpinnerVisible: false,
+ prevAllowance: nextProps.tokenState.allowance,
+ });
+ }
+ }
+ public render() {
+ return (
+ <div className="flex">
+ <div>
+ <Toggle
+ disabled={this.state.isSpinnerVisible}
+ toggled={this.isAllowanceSet()}
+ onToggle={this.onToggleAllowanceAsync.bind(this, this.props.token)}
+ />
+ </div>
+ {this.state.isSpinnerVisible &&
+ <div className="pl1" style={{paddingTop: 3}}>
+ <i className="zmdi zmdi-spinner zmdi-hc-spin" />
+ </div>
+ }
+ </div>
+ );
+ }
+ private async onToggleAllowanceAsync() {
+ if (this.props.userAddress === '') {
+ this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
+ return false;
+ }
+
+ this.setState({
+ isSpinnerVisible: true,
+ });
+
+ let newAllowanceAmountInBaseUnits = new BigNumber(0);
+ if (!this.isAllowanceSet()) {
+ newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS;
+ }
+ try {
+ await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
+ } catch (err) {
+ this.setState({
+ isSpinnerVisible: false,
+ });
+ const errMsg = '' + err;
+ if (_.includes(errMsg, 'User denied transaction')) {
+ return false;
+ }
+ utils.consoleLog(`Unexpected error encountered: ${err}`);
+ utils.consoleLog(err.stack);
+ await errorReporter.reportAsync(err);
+ this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed);
+ }
+ }
+ private isAllowanceSet() {
+ return !this.props.tokenState.allowance.eq(0);
+ }
+}
diff --git a/packages/website/ts/components/inputs/balance_bounded_input.tsx b/packages/website/ts/components/inputs/balance_bounded_input.tsx
new file mode 100644
index 000000000..1c8b410a4
--- /dev/null
+++ b/packages/website/ts/components/inputs/balance_bounded_input.tsx
@@ -0,0 +1,160 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import BigNumber from 'bignumber.js';
+import {ValidatedBigNumberCallback, InputErrMsg, WebsitePaths} from 'ts/types';
+import TextField from 'material-ui/TextField';
+import {RequiredLabel} from 'ts/components/ui/required_label';
+import {colors} from 'material-ui/styles';
+import {utils} from 'ts/utils/utils';
+import {Link} from 'react-router-dom';
+
+interface BalanceBoundedInputProps {
+ label?: string;
+ balance: BigNumber;
+ amount?: BigNumber;
+ onChange: ValidatedBigNumberCallback;
+ shouldShowIncompleteErrs?: boolean;
+ shouldCheckBalance: boolean;
+ validate?: (amount: BigNumber) => InputErrMsg;
+ onVisitBalancesPageClick?: () => void;
+ shouldHideVisitBalancesLink?: boolean;
+}
+
+interface BalanceBoundedInputState {
+ errMsg: InputErrMsg;
+ amountString: string;
+}
+
+export class BalanceBoundedInput extends
+ React.Component<BalanceBoundedInputProps, BalanceBoundedInputState> {
+ public static defaultProps: Partial<BalanceBoundedInputProps> = {
+ shouldShowIncompleteErrs: false,
+ shouldHideVisitBalancesLink: false,
+ };
+ constructor(props: BalanceBoundedInputProps) {
+ super(props);
+ const amountString = this.props.amount ? this.props.amount.toString() : '';
+ this.state = {
+ errMsg: this.validate(amountString, props.balance),
+ amountString,
+ };
+ }
+ public componentWillReceiveProps(nextProps: BalanceBoundedInputProps) {
+ if (nextProps === this.props) {
+ return;
+ }
+ const isCurrentAmountNumeric = utils.isNumeric(this.state.amountString);
+ if (!_.isUndefined(nextProps.amount)) {
+ let shouldResetState = false;
+ if (!isCurrentAmountNumeric) {
+ shouldResetState = true;
+ } else {
+ const currentAmount = new BigNumber(this.state.amountString);
+ if (!currentAmount.eq(nextProps.amount) || !nextProps.balance.eq(this.props.balance)) {
+ shouldResetState = true;
+ }
+ }
+ if (shouldResetState) {
+ const amountString = nextProps.amount.toString();
+ this.setState({
+ errMsg: this.validate(amountString, nextProps.balance),
+ amountString,
+ });
+ }
+ } else if (isCurrentAmountNumeric) {
+ const amountString = '';
+ this.setState({
+ errMsg: this.validate(amountString, nextProps.balance),
+ amountString,
+ });
+ }
+ }
+ public render() {
+ let errorText = this.state.errMsg;
+ if (this.props.shouldShowIncompleteErrs && this.state.amountString === '') {
+ errorText = 'This field is required';
+ }
+ let label: React.ReactNode|string = '';
+ if (!_.isUndefined(this.props.label)) {
+ label = <RequiredLabel label={this.props.label}/>;
+ }
+ return (
+ <TextField
+ fullWidth={true}
+ floatingLabelText={label}
+ floatingLabelFixed={true}
+ floatingLabelStyle={{color: colors.grey500, width: 206}}
+ errorText={errorText}
+ value={this.state.amountString}
+ hintText={<span style={{textTransform: 'capitalize'}}>amount</span>}
+ onChange={this.onValueChange.bind(this)}
+ underlineStyle={{width: 'calc(100% + 50px)'}}
+ />
+ );
+ }
+ private onValueChange(e: any, amountString: string) {
+ const errMsg = this.validate(amountString, this.props.balance);
+ this.setState({
+ amountString,
+ errMsg,
+ }, () => {
+ const isValid = _.isUndefined(errMsg);
+ if (utils.isNumeric(amountString)) {
+ this.props.onChange(isValid, new BigNumber(amountString));
+ } else {
+ this.props.onChange(isValid);
+ }
+ });
+ }
+ private validate(amountString: string, balance: BigNumber): InputErrMsg {
+ if (!utils.isNumeric(amountString)) {
+ return amountString !== '' ? 'Must be a number' : '';
+ }
+ const amount = new BigNumber(amountString);
+ if (amount.eq(0)) {
+ return 'Cannot be zero';
+ }
+ if (this.props.shouldCheckBalance && amount.gt(balance)) {
+ return (
+ <span>
+ Insufficient balance.{' '}
+ {this.renderIncreaseBalanceLink()}
+ </span>
+ );
+ }
+ const errMsg = _.isUndefined(this.props.validate) ? undefined : this.props.validate(amount);
+ return errMsg;
+ }
+ private renderIncreaseBalanceLink() {
+ if (this.props.shouldHideVisitBalancesLink) {
+ return null;
+ }
+
+ const increaseBalanceText = 'Increase balance';
+ const linkStyle = {
+ cursor: 'pointer',
+ color: colors.grey900,
+ textDecoration: 'underline',
+ display: 'inline',
+ };
+ if (_.isUndefined(this.props.onVisitBalancesPageClick)) {
+ return (
+ <Link
+ to={`${WebsitePaths.Portal}/balances`}
+ style={linkStyle}
+ >
+ {increaseBalanceText}
+ </Link>
+ );
+ } else {
+ return (
+ <div
+ onClick={this.props.onVisitBalancesPageClick}
+ style={linkStyle}
+ >
+ {increaseBalanceText}
+ </div>
+ );
+ }
+ }
+}
diff --git a/packages/website/ts/components/inputs/eth_amount_input.tsx b/packages/website/ts/components/inputs/eth_amount_input.tsx
new file mode 100644
index 000000000..ad551e125
--- /dev/null
+++ b/packages/website/ts/components/inputs/eth_amount_input.tsx
@@ -0,0 +1,51 @@
+import BigNumber from 'bignumber.js';
+import * as _ from 'lodash';
+import * as React from 'react';
+import {ZeroEx} from '0x.js';
+import {ValidatedBigNumberCallback} from 'ts/types';
+import {BalanceBoundedInput} from 'ts/components/inputs/balance_bounded_input';
+import {constants} from 'ts/utils/constants';
+
+interface EthAmountInputProps {
+ label?: string;
+ balance: BigNumber;
+ amount?: BigNumber;
+ onChange: ValidatedBigNumberCallback;
+ shouldShowIncompleteErrs: boolean;
+ onVisitBalancesPageClick?: () => void;
+ shouldCheckBalance: boolean;
+ shouldHideVisitBalancesLink?: boolean;
+}
+
+interface EthAmountInputState {}
+
+export class EthAmountInput extends React.Component<EthAmountInputProps, EthAmountInputState> {
+ public render() {
+ const amount = this.props.amount ?
+ ZeroEx.toUnitAmount(this.props.amount, constants.ETH_DECIMAL_PLACES) :
+ undefined;
+ return (
+ <div className="flex overflow-hidden" style={{height: 63}}>
+ <BalanceBoundedInput
+ label={this.props.label}
+ balance={this.props.balance}
+ amount={amount}
+ onChange={this.onChange.bind(this)}
+ shouldCheckBalance={this.props.shouldCheckBalance}
+ shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs}
+ onVisitBalancesPageClick={this.props.onVisitBalancesPageClick}
+ shouldHideVisitBalancesLink={this.props.shouldHideVisitBalancesLink}
+ />
+ <div style={{paddingTop: _.isUndefined(this.props.label) ? 15 : 40}}>
+ ETH
+ </div>
+ </div>
+ );
+ }
+ private onChange(isValid: boolean, amount?: BigNumber) {
+ const baseUnitAmountIfExists = _.isUndefined(amount) ?
+ undefined :
+ ZeroEx.toBaseUnitAmount(amount, constants.ETH_DECIMAL_PLACES);
+ this.props.onChange(isValid, baseUnitAmountIfExists);
+ }
+}
diff --git a/packages/website/ts/components/inputs/expiration_input.tsx b/packages/website/ts/components/inputs/expiration_input.tsx
new file mode 100644
index 000000000..32dcad189
--- /dev/null
+++ b/packages/website/ts/components/inputs/expiration_input.tsx
@@ -0,0 +1,108 @@
+import * as React from 'react';
+import * as _ from 'lodash';
+import DatePicker from 'material-ui/DatePicker';
+import TimePicker from 'material-ui/TimePicker';
+import {utils} from 'ts/utils/utils';
+import BigNumber from 'bignumber.js';
+import * as moment from 'moment';
+
+interface ExpirationInputProps {
+ orderExpiryTimestamp: BigNumber;
+ updateOrderExpiry: (unixTimestampSec: BigNumber) => void;
+}
+
+interface ExpirationInputState {
+ dateMoment: moment.Moment;
+ timeMoment: moment.Moment;
+}
+
+export class ExpirationInput extends React.Component<ExpirationInputProps, ExpirationInputState> {
+ private earliestPickableMoment: moment.Moment;
+ constructor(props: ExpirationInputProps) {
+ super(props);
+ // Set the earliest pickable date to today at 00:00, so users can only pick the current or later dates
+ this.earliestPickableMoment = moment().startOf('day');
+ const expirationMoment = utils.convertToMomentFromUnixTimestamp(props.orderExpiryTimestamp);
+ const initialOrderExpiryTimestamp = utils.initialOrderExpiryUnixTimestampSec();
+ const didUserSetExpiry = !initialOrderExpiryTimestamp.eq(props.orderExpiryTimestamp);
+ this.state = {
+ dateMoment: didUserSetExpiry ? expirationMoment : undefined,
+ timeMoment: didUserSetExpiry ? expirationMoment : undefined,
+ };
+ }
+ public render() {
+ const date = this.state.dateMoment ? this.state.dateMoment.toDate() : undefined;
+ const time = this.state.timeMoment ? this.state.timeMoment.toDate() : undefined;
+ return (
+ <div className="clearfix">
+ <div className="col col-6 overflow-hidden pr3 flex relative">
+ <DatePicker
+ className="overflow-hidden"
+ hintText="Date"
+ mode="landscape"
+ autoOk={true}
+ value={date}
+ onChange={this.onDateChanged.bind(this)}
+ shouldDisableDate={this.shouldDisableDate.bind(this)}
+ />
+ <div
+ className="absolute"
+ style={{fontSize: 20, right: 40, top: 13, pointerEvents: 'none'}}
+ >
+ <i className="zmdi zmdi-calendar" />
+ </div>
+ </div>
+ <div className="col col-5 overflow-hidden flex relative">
+ <TimePicker
+ className="overflow-hidden"
+ hintText="Time"
+ autoOk={true}
+ value={time}
+ onChange={this.onTimeChanged.bind(this)}
+ />
+ <div
+ className="absolute"
+ style={{fontSize: 20, right: 9, top: 13, pointerEvents: 'none'}}
+ >
+ <i className="zmdi zmdi-time" />
+ </div>
+ </div>
+ <div
+ onClick={this.clearDates.bind(this)}
+ className="col col-1 pt2"
+ style={{textAlign: 'right'}}
+ >
+ <i style={{fontSize: 16, cursor: 'pointer'}} className="zmdi zmdi-close" />
+ </div>
+ </div>
+ );
+ }
+ private shouldDisableDate(date: Date): boolean {
+ return moment(date).startOf('day').isBefore(this.earliestPickableMoment);
+ }
+ private clearDates() {
+ this.setState({
+ dateMoment: undefined,
+ timeMoment: undefined,
+ });
+ const defaultDateTime = utils.initialOrderExpiryUnixTimestampSec();
+ this.props.updateOrderExpiry(defaultDateTime);
+ }
+ private onDateChanged(e: any, date: Date) {
+ const dateMoment = moment(date);
+ this.setState({
+ dateMoment,
+ });
+ const timestamp = utils.convertToUnixTimestampSeconds(dateMoment, this.state.timeMoment);
+ this.props.updateOrderExpiry(timestamp);
+ }
+ private onTimeChanged(e: any, time: Date) {
+ const timeMoment = moment(time);
+ this.setState({
+ timeMoment,
+ });
+ const dateMoment = _.isUndefined(this.state.dateMoment) ? moment() : this.state.dateMoment;
+ const timestamp = utils.convertToUnixTimestampSeconds(dateMoment, timeMoment);
+ this.props.updateOrderExpiry(timestamp);
+ }
+}
diff --git a/packages/website/ts/components/inputs/hash_input.tsx b/packages/website/ts/components/inputs/hash_input.tsx
new file mode 100644
index 000000000..3e42f1d5f
--- /dev/null
+++ b/packages/website/ts/components/inputs/hash_input.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react';
+import {Blockchain} from 'ts/blockchain';
+import {ZeroEx, Order} from '0x.js';
+import {FakeTextField} from 'ts/components/ui/fake_text_field';
+import ReactTooltip = require('react-tooltip');
+import {HashData, Styles} from 'ts/types';
+import {constants} from 'ts/utils/constants';
+
+const styles: Styles = {
+ textField: {
+ overflow: 'hidden',
+ paddingTop: 8,
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ },
+};
+
+interface HashInputProps {
+ blockchain: Blockchain;
+ blockchainIsLoaded: boolean;
+ hashData: HashData;
+ label: string;
+}
+
+interface HashInputState {}
+
+export class HashInput extends React.Component<HashInputProps, HashInputState> {
+ public render() {
+ const msgHashHex = this.props.blockchainIsLoaded ? this.generateMessageHashHex() : '';
+ return (
+ <div>
+ <FakeTextField label={this.props.label}>
+ <div
+ style={styles.textField}
+ data-tip={true}
+ data-for="hashTooltip"
+ >
+ {msgHashHex}
+ </div>
+ </FakeTextField>
+ <ReactTooltip id="hashTooltip">{msgHashHex}</ReactTooltip>
+ </div>
+ );
+ }
+ private generateMessageHashHex() {
+ const exchangeContractAddress = this.props.blockchain.getExchangeContractAddressIfExists();
+ const hashData = this.props.hashData;
+ const order: Order = {
+ exchangeContractAddress,
+ expirationUnixTimestampSec: hashData.orderExpiryTimestamp,
+ feeRecipient: hashData.feeRecipientAddress,
+ maker: hashData.orderMakerAddress,
+ makerFee: hashData.makerFee,
+ makerTokenAddress: hashData.depositTokenContractAddr,
+ makerTokenAmount: hashData.depositAmount,
+ salt: hashData.orderSalt,
+ taker: hashData.orderTakerAddress,
+ takerFee: hashData.takerFee,
+ takerTokenAddress: hashData.receiveTokenContractAddr,
+ takerTokenAmount: hashData.receiveAmount,
+ };
+ const orderHash = ZeroEx.getOrderHashHex(order);
+ return orderHash;
+ }
+}
diff --git a/packages/website/ts/components/inputs/identicon_address_input.tsx b/packages/website/ts/components/inputs/identicon_address_input.tsx
new file mode 100644
index 000000000..6452f5fe9
--- /dev/null
+++ b/packages/website/ts/components/inputs/identicon_address_input.tsx
@@ -0,0 +1,56 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+import {Blockchain} from 'ts/blockchain';
+import {Identicon} from 'ts/components/ui/identicon';
+import {RequiredLabel} from 'ts/components/ui/required_label';
+import {AddressInput} from 'ts/components/inputs/address_input';
+import {InputLabel} from 'ts/components/ui/input_label';
+
+interface IdenticonAddressInputProps {
+ initialAddress: string;
+ isRequired?: boolean;
+ label: string;
+ updateOrderAddress: (address?: string) => void;
+}
+
+interface IdenticonAddressInputState {
+ address: string;
+}
+
+export class IdenticonAddressInput extends React.Component<IdenticonAddressInputProps, IdenticonAddressInputState> {
+ constructor(props: IdenticonAddressInputProps) {
+ super(props);
+ this.state = {
+ address: props.initialAddress,
+ };
+ }
+ public render() {
+ const label = this.props.isRequired ? <RequiredLabel label={this.props.label} /> :
+ this.props.label;
+ return (
+ <div className="relative" style={{width: '100%'}}>
+ <InputLabel text={label} />
+ <div className="flex">
+ <div className="col col-1 pb1 pr1" style={{paddingTop: 13}}>
+ <Identicon address={this.state.address} diameter={26} />
+ </div>
+ <div className="col col-11 pb1 pl1" style={{height: 65}}>
+ <AddressInput
+ hintText="e.g 0x75bE4F78AA3699B3A348c84bDB2a96c3Db..."
+ shouldHideLabel={true}
+ initialAddress={this.props.initialAddress}
+ updateAddress={this.updateAddress.bind(this)}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+ private updateAddress(address?: string): void {
+ this.setState({
+ address,
+ });
+ this.props.updateOrderAddress(address);
+ }
+}
diff --git a/packages/website/ts/components/inputs/token_amount_input.tsx b/packages/website/ts/components/inputs/token_amount_input.tsx
new file mode 100644
index 000000000..e19af8984
--- /dev/null
+++ b/packages/website/ts/components/inputs/token_amount_input.tsx
@@ -0,0 +1,69 @@
+import * as React from 'react';
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {ZeroEx} from '0x.js';
+import {Link} from 'react-router-dom';
+import {colors} from 'material-ui/styles';
+import {Token, TokenState, InputErrMsg, ValidatedBigNumberCallback, WebsitePaths} from 'ts/types';
+import {BalanceBoundedInput} from 'ts/components/inputs/balance_bounded_input';
+
+interface TokenAmountInputProps {
+ label: string;
+ token: Token;
+ tokenState: TokenState;
+ amount?: BigNumber;
+ shouldShowIncompleteErrs: boolean;
+ shouldCheckBalance: boolean;
+ shouldCheckAllowance: boolean;
+ onChange: ValidatedBigNumberCallback;
+ onVisitBalancesPageClick?: () => void;
+}
+
+interface TokenAmountInputState {}
+
+export class TokenAmountInput extends React.Component<TokenAmountInputProps, TokenAmountInputState> {
+ public render() {
+ const amount = this.props.amount ?
+ ZeroEx.toUnitAmount(this.props.amount, this.props.token.decimals) :
+ undefined;
+ return (
+ <div className="flex overflow-hidden" style={{height: 84}}>
+ <BalanceBoundedInput
+ label={this.props.label}
+ amount={amount}
+ balance={ZeroEx.toUnitAmount(this.props.tokenState.balance, this.props.token.decimals)}
+ onChange={this.onChange.bind(this)}
+ validate={this.validate.bind(this)}
+ shouldCheckBalance={this.props.shouldCheckBalance}
+ shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs}
+ onVisitBalancesPageClick={this.props.onVisitBalancesPageClick}
+ />
+ <div style={{paddingTop: 39}}>
+ {this.props.token.symbol}
+ </div>
+ </div>
+ );
+ }
+ private onChange(isValid: boolean, amount?: BigNumber) {
+ let baseUnitAmount;
+ if (!_.isUndefined(amount)) {
+ baseUnitAmount = ZeroEx.toBaseUnitAmount(amount, this.props.token.decimals);
+ }
+ this.props.onChange(isValid, baseUnitAmount);
+ }
+ private validate(amount: BigNumber): InputErrMsg {
+ if (this.props.shouldCheckAllowance && amount.gt(this.props.tokenState.allowance)) {
+ return (
+ <span>
+ Insufficient allowance.{' '}
+ <Link
+ to={`${WebsitePaths.Portal}/balances`}
+ style={{cursor: 'pointer', color: colors.grey900}}
+ >
+ Set allowance
+ </Link>
+ </span>
+ );
+ }
+ }
+}
diff --git a/packages/website/ts/components/inputs/token_input.tsx b/packages/website/ts/components/inputs/token_input.tsx
new file mode 100644
index 000000000..2be74d4fd
--- /dev/null
+++ b/packages/website/ts/components/inputs/token_input.tsx
@@ -0,0 +1,107 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import Paper from 'material-ui/Paper';
+import {colors} from 'material-ui/styles';
+import {Blockchain} from 'ts/blockchain';
+import {Dispatcher} from 'ts/redux/dispatcher';
+import {AssetToken, Side, TokenByAddress, BlockchainErrs, Token, TokenState} from 'ts/types';
+import {AssetPicker} from 'ts/components/generate_order/asset_picker';
+import {InputLabel} from 'ts/components/ui/input_label';
+import {TokenIcon} from 'ts/components/ui/token_icon';
+
+const TOKEN_ICON_DIMENSION = 80;
+
+interface TokenInputProps {
+ blockchain: Blockchain;
+ blockchainErr: BlockchainErrs;
+ dispatcher: Dispatcher;
+ label: string;
+ side: Side;
+ networkId: number;
+ assetToken: AssetToken;
+ updateChosenAssetToken: (side: Side, token: AssetToken) => void;
+ tokenByAddress: TokenByAddress;
+ userAddress: string;
+}
+
+interface TokenInputState {
+ isHoveringIcon: boolean;
+ isPickerOpen: boolean;
+ trackCandidateTokenIfExists?: Token;
+}
+
+export class TokenInput extends React.Component<TokenInputProps, TokenInputState> {
+ constructor(props: TokenInputProps) {
+ super(props);
+ this.state = {
+ isHoveringIcon: false,
+ isPickerOpen: false,
+ };
+ }
+ public render() {
+ const token = this.props.tokenByAddress[this.props.assetToken.address];
+ const iconStyles = {
+ cursor: 'pointer',
+ opacity: this.state.isHoveringIcon ? 0.5 : 1,
+ };
+ return (
+ <div className="relative">
+ <div className="pb1">
+ <InputLabel text={this.props.label} />
+ </div>
+ <Paper
+ zDepth={1}
+ style={{cursor: 'pointer'}}
+ onMouseEnter={this.onToggleHover.bind(this, true)}
+ onMouseLeave={this.onToggleHover.bind(this, false)}
+ onClick={this.onAssetClicked.bind(this)}
+ >
+ <div
+ className="mx-auto pt2"
+ style={{width: TOKEN_ICON_DIMENSION, ...iconStyles}}
+ >
+ <TokenIcon token={token} diameter={TOKEN_ICON_DIMENSION} />
+ </div>
+ <div className="py1 center" style={{color: colors.grey500}}>
+ {token.name}
+ </div>
+ </Paper>
+ <AssetPicker
+ userAddress={this.props.userAddress}
+ networkId={this.props.networkId}
+ blockchain={this.props.blockchain}
+ dispatcher={this.props.dispatcher}
+ isOpen={this.state.isPickerOpen}
+ currentTokenAddress={this.props.assetToken.address}
+ onTokenChosen={this.onTokenChosen.bind(this)}
+ tokenByAddress={this.props.tokenByAddress}
+ />
+ </div>
+ );
+ }
+ private onTokenChosen(tokenAddress: string) {
+ const assetToken: AssetToken = {
+ address: tokenAddress,
+ amount: this.props.assetToken.amount,
+ };
+ this.props.updateChosenAssetToken(this.props.side, assetToken);
+ this.setState({
+ isPickerOpen: false,
+ });
+ }
+ private onToggleHover(isHoveringIcon: boolean) {
+ this.setState({
+ isHoveringIcon,
+ });
+ }
+ private onAssetClicked() {
+ if (this.props.blockchainErr !== '') {
+ this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
+ return;
+ }
+
+ this.setState({
+ isPickerOpen: true,
+ });
+ }
+}