feat: implement multi token select component
This commit is contained in:
31
packages/website/ts/components/ui/check_mark.tsx
Normal file
31
packages/website/ts/components/ui/check_mark.tsx
Normal file
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user