diff options
author | fragosti <francesco.agosti93@gmail.com> | 2018-12-01 02:25:36 +0800 |
---|---|---|
committer | fragosti <francesco.agosti93@gmail.com> | 2018-12-01 02:25:36 +0800 |
commit | e65096ee7af7c6d442b5e106db0a07652cc5e047 (patch) | |
tree | e43fba0c7aab2154e9c7d21817aa014088c71334 | |
parent | f1354632a1a2915159f6d662f90b68fe8c3bab38 (diff) | |
download | dexon-sol-tools-e65096ee7af7c6d442b5e106db0a07652cc5e047.tar.gz dexon-sol-tools-e65096ee7af7c6d442b5e106db0a07652cc5e047.tar.zst dexon-sol-tools-e65096ee7af7c6d442b5e106db0a07652cc5e047.zip |
feat: implement multi token select component
-rw-r--r-- | packages/website/ts/components/ui/check_mark.tsx | 31 | ||||
-rw-r--r-- | packages/website/ts/components/ui/container.tsx | 6 | ||||
-rw-r--r-- | packages/website/ts/components/ui/multi_select.tsx | 38 | ||||
-rw-r--r-- | packages/website/ts/components/ui/select.tsx | 2 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/action_link.tsx | 7 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/config_generator.tsx | 109 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/configurator.tsx | 3 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/features.tsx | 6 |
8 files changed, 156 insertions, 46 deletions
diff --git a/packages/website/ts/components/ui/check_mark.tsx b/packages/website/ts/components/ui/check_mark.tsx new file mode 100644 index 000000000..86e427c8b --- /dev/null +++ b/packages/website/ts/components/ui/check_mark.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; + +import { colors } from '@0x/react-shared'; + +export interface CheckMarkProps { + color?: string; + isChecked?: boolean; +} + +export const CheckMark: React.StatelessComponent<CheckMarkProps> = ({ color, isChecked }) => ( + <svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle + cx="8.5" + cy="8.5" + r="8.5" + fill={isChecked ? color : 'white'} + stroke={isChecked ? undefined : '#CCCCCC'} + /> + <path + d="M2.5 4.5L1.79289 5.20711L2.5 5.91421L3.20711 5.20711L2.5 4.5ZM-0.707107 2.70711L1.79289 5.20711L3.20711 3.79289L0.707107 1.29289L-0.707107 2.70711ZM3.20711 5.20711L7.70711 0.707107L6.29289 -0.707107L1.79289 3.79289L3.20711 5.20711Z" + transform="translate(5 6.5)" + fill="white" + /> + </svg> +); + +CheckMark.displayName = 'Check'; + +CheckMark.defaultProps = { + color: colors.mediumBlue, +}; diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index cd0ed9986..4b76ce8be 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -9,6 +9,7 @@ type StringOrNum = string | number; export type ContainerTag = 'div' | 'span'; export interface ContainerProps { + margin?: string; marginTop?: StringOrNum; marginBottom?: StringOrNum; marginRight?: StringOrNum; @@ -48,7 +49,9 @@ export interface ContainerProps { id?: string; onClick?: (event: React.MouseEvent<HTMLElement>) => void; overflowX?: 'scroll' | 'hidden' | 'auto' | 'visible'; + overflowY?: 'scroll' | 'hidden' | 'auto' | 'visible'; shouldDarkenOnHover?: boolean; + hasBoxShadow?: boolean; shouldAddBoxShadowOnHover?: boolean; } @@ -62,6 +65,7 @@ export const PlainContainer: React.StatelessComponent<ContainerProps> = props => onClick, shouldDarkenOnHover, shouldAddBoxShadowOnHover, + hasBoxShadow, // tslint:disable-next-line:trailing-comma ...style } = props; @@ -74,6 +78,8 @@ export const PlainContainer: React.StatelessComponent<ContainerProps> = props => }; export const Container = styled(PlainContainer)` + box-sizing: border-box; + ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; &:hover { ${props => props.shouldDarkenOnHover diff --git a/packages/website/ts/components/ui/multi_select.tsx b/packages/website/ts/components/ui/multi_select.tsx index bf80443af..2cf44cae1 100644 --- a/packages/website/ts/components/ui/multi_select.tsx +++ b/packages/website/ts/components/ui/multi_select.tsx @@ -2,40 +2,42 @@ import { colors } from '@0x/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; -import { zIndex } from 'ts/style/z_index'; - import { Container } from './container'; -import { Overlay } from './overlay'; -import { Text } from './text'; export interface MultiSelectItemConfig { value: string; - displayText: React.ReactNode; + renderItemContent: (isSelected: boolean) => React.ReactNode; onClick?: () => void; } export interface MultiSelectProps { - selectedValues: string[]; + selectedValues?: string[]; items: MultiSelectItemConfig[]; backgroundColor?: string; - textColor?: string; + height?: string; } export class MultiSelect extends React.Component<MultiSelectProps> { public static defaultProps = { backgroundColor: colors.white, - textColor: colors.darkGrey, }; public render(): React.ReactNode { - const { items, backgroundColor, selectedValues } = this.props; + const { items, backgroundColor, selectedValues, height } = this.props; return ( - <Container backgroundColor={backgroundColor} borderRadius="4px"> + <Container + backgroundColor={backgroundColor} + borderRadius="4px" + width="100%" + height={height} + overflowY="scroll" + > {_.map(items, item => ( <MultiSelectItem key={item.value} - displayText={item.displayText} + renderItemContent={item.renderItemContent} + backgroundColor={backgroundColor} onClick={item.onClick} - isSelected={_.includes(selectedValues, item.value)} + isSelected={_.isUndefined(selectedValues) || _.includes(selectedValues, item.value)} /> ))} </Container> @@ -44,19 +46,21 @@ export class MultiSelect extends React.Component<MultiSelectProps> { } export interface MultiSelectItemProps { - displayText: React.ReactNode; + renderItemContent: (isSelected: boolean) => React.ReactNode; isSelected?: boolean; onClick?: () => void; + backgroundColor?: string; } export const MultiSelectItem: React.StatelessComponent<MultiSelectItemProps> = ({ - displayText, + renderItemContent, isSelected, onClick, + backgroundColor, }) => ( - <Container shouldDarkenOnHover={true} onClick={onClick}> - <Container borderBottom="1px solid black" padding="0px 15px"> - {displayText} + <Container cursor="pointer" shouldDarkenOnHover={true} onClick={onClick} backgroundColor={backgroundColor}> + <Container borderBottom={`1px solid ${colors.lightestGrey}`} margin="0px 15px" padding="10px 0px"> + {renderItemContent(isSelected)} </Container> </Container> ); diff --git a/packages/website/ts/components/ui/select.tsx b/packages/website/ts/components/ui/select.tsx index abf202c67..743b082b0 100644 --- a/packages/website/ts/components/ui/select.tsx +++ b/packages/website/ts/components/ui/select.tsx @@ -62,6 +62,7 @@ export class Select extends React.Component<SelectProps, SelectState> { cursor={hasItems ? 'pointer' : undefined} onClick={this._handleDropdownClick} borderRadius={borderRadius} + hasBoxShadow={isOpen} border={border} backgroundColor={backgroundColor} padding="0.8em" @@ -94,6 +95,7 @@ export class Select extends React.Component<SelectProps, SelectState> { position="absolute" onClick={this._closeDropdown} zIndex={zIndex.aboveOverlay} + hasBoxShadow={true} > {_.map(items, (item, index) => ( <SelectItem diff --git a/packages/website/ts/pages/instant/action_link.tsx b/packages/website/ts/pages/instant/action_link.tsx index 5612dc25b..c196f03ef 100644 --- a/packages/website/ts/pages/instant/action_link.tsx +++ b/packages/website/ts/pages/instant/action_link.tsx @@ -23,12 +23,7 @@ export class ActionLink extends React.Component<ActionLinkProps> { public render(): React.ReactNode { const { displayText, fontSize, color, className } = this.props; return ( - <Container - className={`flex items-center ${className}`} - marginRight="32px" - onClick={this._handleClick} - cursor="pointer" - > + <Container className={`flex items-center ${className}`} onClick={this._handleClick} cursor="pointer"> <Container> <Text fontSize="16px" fontColor={color}> {displayText} diff --git a/packages/website/ts/pages/instant/config_generator.tsx b/packages/website/ts/pages/instant/config_generator.tsx index cd215bc61..d1ea66557 100644 --- a/packages/website/ts/pages/instant/config_generator.tsx +++ b/packages/website/ts/pages/instant/config_generator.tsx @@ -5,6 +5,7 @@ import { ObjectMap } from '@0x/types'; import * as _ from 'lodash'; import * as React from 'react'; +import { CheckMark } from 'ts/components/ui/check_mark'; import { Container } from 'ts/components/ui/container'; import { MultiSelect } from 'ts/components/ui/multi_select'; import { Select, SelectItemConfig } from 'ts/components/ui/select'; @@ -54,42 +55,79 @@ export class ConfigGenerator extends React.Component<ConfigGeneratorProps> { <ConfigGeneratorSection title="Standard Relayer API Endpoint"> <Select value={value.orderSource} items={this._generateItems()} /> </ConfigGeneratorSection> - <ConfigGeneratorSection title="What tokens can users buy?"> + <ConfigGeneratorSection {...this._getTokenSelectorProps()}> {this._renderTokenMultiSelectOrSpinner()} </ConfigGeneratorSection> </Container> ); } + private readonly _getTokenSelectorProps = (): ConfigGeneratorSectionProps => { + if (_.isUndefined(this.props.value.availableAssetDatas)) { + return { + title: 'What tokens can users buy?', + actionText: 'Unselect All', + onActionTextClick: this._handleUnselectAllClick, + }; + } + return { + title: 'What tokens can users buy?', + actionText: 'Select All', + onActionTextClick: this._handleSelectAllClick, + }; + }; private readonly _generateItems = (): SelectItemConfig[] => { return _.map(SRA_ENDPOINTS, endpoint => ({ text: endpoint, onClick: this._handleSRASelection.bind(this, endpoint), })); }; + private readonly _getAllKnownAssetDatas = (): string[] => { + return _.map(this.state.allKnownTokens, token => assetDataUtils.encodeERC20AssetData(token.address)); + }; private readonly _handleSRASelection = (sraEndpoint: string) => { - const newConfig = { + const newConfig: ZeroExInstantBaseConfig = { ...this.props.value, orderSource: sraEndpoint, }; this.props.onConfigChange(newConfig); }; + private readonly _handleSelectAllClick = () => { + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + availableAssetDatas: undefined, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _handleUnselectAllClick = () => { + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + availableAssetDatas: [], + }; + this.props.onConfigChange(newConfig); + }; private readonly _handleTokenClick = (assetData: string) => { const { value } = this.props; - let newAvailableAssetDatas = []; - if (_.includes(value.availableAssetDatas, assetData)) { + const { allKnownTokens } = this.state; + let newAvailableAssetDatas: string[] = []; + const availableAssetDatas = value.availableAssetDatas; + if (_.isUndefined(availableAssetDatas)) { + // It being undefined means it's all tokens. + const allKnownAssetDatas = this._getAllKnownAssetDatas(); + newAvailableAssetDatas = _.pull(allKnownAssetDatas, assetData); + } else if (!_.includes(availableAssetDatas, assetData)) { // Add it - newAvailableAssetDatas = [...value.availableAssetDatas, assetData]; + newAvailableAssetDatas = [...availableAssetDatas, assetData]; } else { // Remove it - newAvailableAssetDatas = _.remove(value.availableAssetDatas, assetData); + newAvailableAssetDatas = _.pull(availableAssetDatas, assetData); } - const newConfig = { + const newConfig: ZeroExInstantBaseConfig = { ...this.props.value, availableAssetDatas: newAvailableAssetDatas, }; this.props.onConfigChange(newConfig); }; - private _setAllKnownTokens = async (callback: () => void): Promise<void> => { + private readonly _setAllKnownTokens = async (callback: () => void): Promise<void> => { const tokenInfos = await backendClient.getTokenInfosAsync(); const allKnownTokens = _.reduce( tokenInfos, @@ -101,7 +139,7 @@ export class ConfigGenerator extends React.Component<ConfigGeneratorProps> { ); this.setState({ allKnownTokens }, callback); }; - private _setAvailableAssetsFromOrderProvider = async (): Promise<void> => { + private readonly _setAvailableAssetsFromOrderProvider = async (): Promise<void> => { const { value } = this.props; if (!_.isUndefined(value.orderSource) && _.isString(value.orderSource)) { this.setState({ isLoadingAvailableTokens: true }); @@ -119,13 +157,23 @@ export class ConfigGenerator extends React.Component<ConfigGeneratorProps> { this.setState({ availableTokens, isLoadingAvailableTokens: false }); } }; - private _renderTokenMultiSelectOrSpinner = (): React.ReactNode => { + private readonly _renderTokenMultiSelectOrSpinner = (): React.ReactNode => { const { value } = this.props; const { availableTokens, isLoadingAvailableTokens } = this.state; + const multiSelectHeight = '200px'; if (isLoadingAvailableTokens) { return ( - <Container className="flex items-center"> - <Spinner /> + <Container + className="flex flex-column items-center justify-center" + height={multiSelectHeight} + backgroundColor={colors.white} + borderRadius="4px" + width="100%" + > + <Container position="relative" left="12px" marginBottom="20px"> + <Spinner /> + </Container> + <Text fontSize="16px">Loading...</Text> </Container> ); } @@ -133,15 +181,24 @@ export class ConfigGenerator extends React.Component<ConfigGeneratorProps> { const assetData = assetDataUtils.encodeERC20AssetData(token.address); return { value: assetDataUtils.encodeERC20AssetData(token.address), - displayText: ( - <Text> - <b>{token.symbol}</b> - {token.name} - </Text> + renderItemContent: (isSelected: boolean) => ( + <Container className="flex items-center"> + <Container marginRight="10px"> + <CheckMark isChecked={isSelected} /> + </Container> + <Text + fontSize="16px" + fontColor={isSelected ? colors.mediumBlue : colors.darkerGrey} + fontWeight={300} + > + <b>{token.symbol}</b> — {token.name} + </Text> + </Container> ), onClick: this._handleTokenClick.bind(this, assetData), }; }); - return <MultiSelect items={items} selectedValues={value.availableAssetDatas || []} />; + return <MultiSelect items={items} selectedValues={value.availableAssetDatas} height={multiSelectHeight} />; }; } @@ -151,13 +208,23 @@ export interface ConfigGeneratorSectionProps { onActionTextClick?: () => void; } -export const ConfigGeneratorSection: React.StatelessComponent<ConfigGeneratorSectionProps> = props => ( +export const ConfigGeneratorSection: React.StatelessComponent<ConfigGeneratorSectionProps> = ({ + title, + actionText, + onActionTextClick, + children, +}) => ( <Container marginBottom="30px"> - <Container marginBottom="10px"> + <Container marginBottom="10px" className="flex justify-between items-center"> <Text fontColor={colors.white} fontSize="16px" lineHeight="18px"> - {props.title} + {title} </Text> + {actionText && ( + <Text fontSize="12px" fontColor={colors.grey} onClick={onActionTextClick}> + {actionText} + </Text> + )} </Container> - {props.children} + {children} </Container> ); diff --git a/packages/website/ts/pages/instant/configurator.tsx b/packages/website/ts/pages/instant/configurator.tsx index cf9985675..f4987c0de 100644 --- a/packages/website/ts/pages/instant/configurator.tsx +++ b/packages/website/ts/pages/instant/configurator.tsx @@ -18,9 +18,10 @@ export interface ConfiguratorState { } export class Configurator extends React.Component<ConfiguratorProps> { - public state = { + public state: ConfiguratorState = { instantConfig: { orderSource: 'https://api.radarrelay.com/0x/v2/', + availableAssetDatas: [], }, }; public render(): React.ReactNode { diff --git a/packages/website/ts/pages/instant/features.tsx b/packages/website/ts/pages/instant/features.tsx index 1e187c9da..c88958bbf 100644 --- a/packages/website/ts/pages/instant/features.tsx +++ b/packages/website/ts/pages/instant/features.tsx @@ -90,7 +90,11 @@ const FeatureItem = (props: FeatureItemProps) => { </Text> </Container> <Container className="flex" marginTop="28px"> - {_.map(linkInfos, linkInfo => <ActionLink key={linkInfo.displayText} {...linkInfo} />)} + {_.map(linkInfos, linkInfo => ( + <Container marginRight="32px"> + <ActionLink key={linkInfo.displayText} {...linkInfo} /> + </Container> + ))} </Container> </Container> ); |