diff options
author | Fabio Berger <me@fabioberger.com> | 2017-11-22 04:03:08 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2017-11-22 04:03:08 +0800 |
commit | 3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b (patch) | |
tree | f101656799da807489253e17bea7abfaea90b62d /packages/website/ts/components/inputs | |
parent | 037f466e1f80f635b48f3235258402e2ce75fb7b (diff) | |
download | dexon-sol-tools-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.tar.gz dexon-sol-tools-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.tar.zst dexon-sol-tools-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')
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, + }); + } +} |