aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/pages/instant
diff options
context:
space:
mode:
authorfragosti <francesco.agosti93@gmail.com>2018-12-21 08:01:53 +0800
committerfragosti <francesco.agosti93@gmail.com>2018-12-21 08:01:53 +0800
commitabdf91c691b924b75d71db49fba296da9c8ddcac (patch)
tree78e62a107f1de7f3b16dd63bdbc039ab26b561a3 /packages/website/ts/pages/instant
parent9b540fd8e52e7578d3749e6d9ef9cd97d602ffb3 (diff)
downloaddexon-sol-tools-abdf91c691b924b75d71db49fba296da9c8ddcac.tar.gz
dexon-sol-tools-abdf91c691b924b75d71db49fba296da9c8ddcac.tar.zst
dexon-sol-tools-abdf91c691b924b75d71db49fba296da9c8ddcac.zip
feat: move all @next files to non @next directory
Diffstat (limited to 'packages/website/ts/pages/instant')
-rw-r--r--packages/website/ts/pages/instant/code_demo.tsx30
-rw-r--r--packages/website/ts/pages/instant/config_generator.tsx332
-rw-r--r--packages/website/ts/pages/instant/config_generator_address_input.tsx55
-rw-r--r--packages/website/ts/pages/instant/configurator.tsx104
-rw-r--r--packages/website/ts/pages/instant/fee_percentage_slider.tsx80
-rw-r--r--packages/website/ts/pages/instant/rc-slider.css295
-rw-r--r--packages/website/ts/pages/instant/select.tsx74
7 files changed, 943 insertions, 27 deletions
diff --git a/packages/website/ts/pages/instant/code_demo.tsx b/packages/website/ts/pages/instant/code_demo.tsx
index a3b5fe847..4a3022df5 100644
--- a/packages/website/ts/pages/instant/code_demo.tsx
+++ b/packages/website/ts/pages/instant/code_demo.tsx
@@ -2,9 +2,8 @@ import * as React from 'react';
import * as CopyToClipboard from 'react-copy-to-clipboard';
import SyntaxHighlighter from 'react-syntax-highlighter';
-import { Button } from 'ts/components/ui/button';
+import { Button } from 'ts/@next/components/button';
import { Container } from 'ts/components/ui/container';
-import { colors } from 'ts/style/colors';
import { styled } from 'ts/style/theme';
import { zIndex } from 'ts/style/z_index';
@@ -12,7 +11,7 @@ const CustomPre = styled.pre`
margin: 0px;
line-height: 24px;
overflow: scroll;
- width: 600px;
+ width: 100%;
height: 100%;
max-height: 800px;
border-radius: 4px;
@@ -23,19 +22,21 @@ const CustomPre = styled.pre`
border: none;
}
code:first-of-type {
- background-color: #2a2a2a !important;
+ background-color: #060d0d !important;
color: #999;
- min-height: 98%;
+ min-height: 100%;
text-align: center;
- padding-right: 5px !important;
- padding-left: 5px;
margin-right: 15px;
line-height: 25px;
- padding-top: 10px;
+ padding: 10px 7px !important;
}
code:last-of-type {
position: relative;
top: 10px;
+ top: 0;
+ padding-top: 11px;
+ display: inline-block;
+ line-height: 25px;
}
`;
@@ -130,7 +131,7 @@ const customStyle = {
hljs: {
display: 'block',
overflowX: 'hidden',
- background: colors.instantSecondaryBackground,
+ background: '#1B2625',
color: 'white',
fontSize: '12px',
},
@@ -160,9 +161,7 @@ export class CodeDemo extends React.Component<CodeDemoProps, CodeDemoState> {
<Container position="relative" height="100%">
<Container position="absolute" top="10px" right="10px" zIndex={zIndex.overlay - 1}>
<CopyToClipboard text={this.props.children} onCopy={this._handleCopyClick}>
- <Button fontSize="14px">
- <b>{copyButtonText}</b>
- </Button>
+ <StyledButton>{copyButtonText}</StyledButton>
</CopyToClipboard>
</Container>
<SyntaxHighlighter language="html" style={customStyle} showLineNumbers={true} PreTag={CustomPre}>
@@ -175,3 +174,10 @@ export class CodeDemo extends React.Component<CodeDemoProps, CodeDemoState> {
this.setState({ didCopyCode: true });
};
}
+
+const StyledButton = styled(Button)`
+ border-radius: 4px;
+ font-size: 15px;
+ font-weight: 400;
+ padding: 9px 21px 7px;
+`;
diff --git a/packages/website/ts/pages/instant/config_generator.tsx b/packages/website/ts/pages/instant/config_generator.tsx
new file mode 100644
index 000000000..3f00e33e2
--- /dev/null
+++ b/packages/website/ts/pages/instant/config_generator.tsx
@@ -0,0 +1,332 @@
+import { StandardRelayerAPIOrderProvider } from '@0x/asset-buyer';
+import { getContractAddressesForNetworkOrThrow } from '@0x/contract-addresses';
+import { assetDataUtils } from '@0x/order-utils';
+import { ObjectMap } from '@0x/types';
+import * as _ from 'lodash';
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { ConfigGeneratorAddressInput } from 'ts/@next/pages/instant/config_generator_address_input';
+import { FeePercentageSlider } from 'ts/@next/pages/instant/fee_percentage_slider';
+import { CheckMark } from 'ts/components/ui/check_mark';
+import { Container } from 'ts/components/ui/container';
+import { MultiSelect } from 'ts/components/ui/multi_select';
+import { Spinner } from 'ts/components/ui/spinner';
+import { Text } from 'ts/components/ui/text';
+import { colors } from 'ts/style/colors';
+import { WebsitePaths } from 'ts/types';
+import { constants } from 'ts/utils/constants';
+
+// New components
+import { Heading } from 'ts/@next/components/text';
+import { Select, SelectItemConfig } from 'ts/@next/pages/instant/select';
+
+import { assetMetaDataMap } from '../../../../../instant/src/data/asset_meta_data_map';
+import { ERC20AssetMetaData, ZeroExInstantBaseConfig } from '../../../../../instant/src/types';
+
+export interface ConfigGeneratorProps {
+ value: ZeroExInstantBaseConfig;
+ onConfigChange: (config: ZeroExInstantBaseConfig) => void;
+}
+
+export interface ConfigGeneratorState {
+ isLoadingAvailableTokens: boolean;
+ // Address to token info
+ availableTokens?: ObjectMap<ERC20AssetMetaData>;
+}
+
+const SRA_ENDPOINTS = ['https://api.radarrelay.com/0x/v2/', 'https://sra.bamboorelay.com/0x/v2/'];
+
+export class ConfigGenerator extends React.Component<ConfigGeneratorProps, ConfigGeneratorState> {
+ public state: ConfigGeneratorState = {
+ isLoadingAvailableTokens: true,
+ };
+ public componentDidMount(): void {
+ // tslint:disable-next-line:no-floating-promises
+ this._setAvailableAssetsFromOrderProvider();
+ }
+ public componentDidUpdate(prevProps: ConfigGeneratorProps): void {
+ if (prevProps.value.orderSource !== this.props.value.orderSource) {
+ // tslint:disable-next-line:no-floating-promises
+ this._setAvailableAssetsFromOrderProvider();
+ const newConfig: ZeroExInstantBaseConfig = {
+ ...this.props.value,
+ availableAssetDatas: undefined,
+ };
+ this.props.onConfigChange(newConfig);
+ }
+ }
+ public render(): React.ReactNode {
+ const { value } = this.props;
+ if (!_.isString(value.orderSource)) {
+ throw new Error('ConfigGenerator component only supports string values as an orderSource.');
+ }
+ return (
+ <Container minWidth="350px">
+ <ConfigGeneratorSection title="Liquidity Source">
+ <Select
+ shouldIncludeEmpty={false}
+ id=""
+ value={value.orderSource}
+ items={this._generateItems()}
+ onChange={this._handleSRASelection.bind(this)}
+ />
+ </ConfigGeneratorSection>
+ <ConfigGeneratorSection {...this._getTokenSelectorProps()}>
+ {this._renderTokenMultiSelectOrSpinner()}
+ </ConfigGeneratorSection>
+ <ConfigGeneratorSection title="Transaction fee ETH address" marginBottom="10px" isOptional={true}>
+ <ConfigGeneratorAddressInput
+ value={value.affiliateInfo ? value.affiliateInfo.feeRecipient : ''}
+ onChange={this._handleAffiliateAddressChange}
+ />
+ </ConfigGeneratorSection>
+ <ConfigGeneratorSection
+ title="Fee percentage"
+ actionText="Learn more"
+ onActionTextClick={this._handleAffiliatePercentageLearnMoreClick}
+ >
+ <FeePercentageSlider
+ value={value.affiliateInfo.feePercentage}
+ onChange={this._handleAffiliatePercentageChange}
+ isDisabled={
+ _.isUndefined(value.affiliateInfo) ||
+ _.isUndefined(value.affiliateInfo.feeRecipient) ||
+ _.isEmpty(value.affiliateInfo.feeRecipient)
+ }
+ />
+ </ConfigGeneratorSection>
+ </Container>
+ );
+ }
+ private readonly _getTokenSelectorProps = (): ConfigGeneratorSectionProps => {
+ if (_.isEmpty(this.state.availableTokens)) {
+ return {
+ title: 'What tokens can users buy?',
+ };
+ }
+ 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 => ({
+ label: endpoint,
+ value: endpoint,
+ onClick: this._handleSRASelection.bind(this, endpoint),
+ }));
+ };
+ private readonly _handleAffiliatePercentageLearnMoreClick = (): void => {
+ window.open(`${WebsitePaths.Wiki}#Learn-About-Affiliate-Fees`, '_blank');
+ };
+ private readonly _handleSRASelection = (event: React.ChangeEvent<HTMLSelectElement>) => {
+ const sraEndpoint = event.target.value;
+ const newConfig: ZeroExInstantBaseConfig = {
+ ...this.props.value,
+ orderSource: sraEndpoint,
+ };
+ this.props.onConfigChange(newConfig);
+ };
+ private readonly _handleAffiliateAddressChange = (address: string, isValid: boolean) => {
+ const oldConfig: ZeroExInstantBaseConfig = this.props.value;
+ const newConfig: ZeroExInstantBaseConfig = {
+ ...oldConfig,
+ affiliateInfo: {
+ feeRecipient: address,
+ feePercentage: oldConfig.affiliateInfo.feePercentage,
+ },
+ };
+ this.props.onConfigChange(newConfig);
+ };
+ private readonly _handleAffiliatePercentageChange = (value: number) => {
+ const oldConfig: ZeroExInstantBaseConfig = this.props.value;
+ const newConfig: ZeroExInstantBaseConfig = {
+ ...oldConfig,
+ affiliateInfo: {
+ feeRecipient: oldConfig.affiliateInfo.feeRecipient,
+ feePercentage: value,
+ },
+ };
+ 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: string[] = [];
+ const allKnownAssetDatas = _.keys(this.state.availableTokens);
+ const availableAssetDatas = value.availableAssetDatas;
+ if (_.isUndefined(availableAssetDatas)) {
+ // It being undefined means it's all tokens.
+ newAvailableAssetDatas = _.pull(allKnownAssetDatas, assetData);
+ } else if (!_.includes(availableAssetDatas, assetData)) {
+ // Add it
+ newAvailableAssetDatas = [...availableAssetDatas, assetData];
+ if (newAvailableAssetDatas.length === allKnownAssetDatas.length) {
+ // If all tokens are manually selected, just show none.
+ newAvailableAssetDatas = undefined;
+ }
+ } else {
+ // Remove it
+ newAvailableAssetDatas = _.pull(availableAssetDatas, assetData);
+ }
+ const newConfig: ZeroExInstantBaseConfig = {
+ ...this.props.value,
+ availableAssetDatas: newAvailableAssetDatas,
+ };
+ this.props.onConfigChange(newConfig);
+ };
+ private readonly _setAvailableAssetsFromOrderProvider = async (): Promise<void> => {
+ const { value } = this.props;
+ if (!_.isUndefined(value.orderSource) && _.isString(value.orderSource)) {
+ this.setState({ isLoadingAvailableTokens: true });
+ const networkId = constants.NETWORK_ID_MAINNET;
+ const sraOrderProvider = new StandardRelayerAPIOrderProvider(value.orderSource, networkId);
+ const etherTokenAddress = getContractAddressesForNetworkOrThrow(networkId).etherToken;
+ const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddress);
+ const assetDatas = await sraOrderProvider.getAvailableMakerAssetDatasAsync(etherTokenAssetData);
+ const availableTokens = _.reduce(
+ assetDatas,
+ (acc, assetData) => {
+ const metaDataIfExists = assetMetaDataMap[assetData] as ERC20AssetMetaData;
+ if (metaDataIfExists) {
+ acc[assetData] = metaDataIfExists;
+ }
+ return acc;
+ },
+ {} as ObjectMap<ERC20AssetMetaData>,
+ );
+ this.setState({ availableTokens, isLoadingAvailableTokens: false });
+ }
+ };
+ private readonly _renderTokenMultiSelectOrSpinner = (): React.ReactNode => {
+ const { value } = this.props;
+ const { availableTokens, isLoadingAvailableTokens } = this.state;
+ const multiSelectHeight = '200px';
+ if (isLoadingAvailableTokens) {
+ return (
+ <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>
+ );
+ }
+ const availableAssetDatas = _.keys(availableTokens);
+ if (availableAssetDatas.length === 0) {
+ return (
+ <Container
+ className="flex flex-column items-center justify-center"
+ height={multiSelectHeight}
+ backgroundColor={colors.white}
+ borderRadius="4px"
+ width="100%"
+ >
+ <Text fontSize="16px">No tokens available. Try another endpoint?</Text>
+ </Container>
+ );
+ }
+ const items = _.map(_.keys(availableTokens), assetData => {
+ const metaData = availableTokens[assetData];
+ return {
+ value: assetData,
+ renderItemContent: (isSelected: boolean) => (
+ <Container className="flex items-center">
+ <Container marginRight="10px">
+ <CheckMark isChecked={isSelected} color={colors.brandLight} />
+ </Container>
+ <CheckboxText isSelected={isSelected}>
+ {metaData.symbol.toUpperCase()} — {metaData.name}
+ </CheckboxText>
+ </Container>
+ ),
+ onClick: this._handleTokenClick.bind(this, assetData),
+ };
+ });
+ return <MultiSelect items={items} selectedValues={value.availableAssetDatas} height={multiSelectHeight} />;
+ };
+}
+
+export interface ConfigGeneratorSectionProps {
+ title: string;
+ actionText?: string;
+ onActionTextClick?: () => void;
+ isOptional?: boolean;
+ marginBottom?: string;
+}
+
+export const ConfigGeneratorSection: React.StatelessComponent<ConfigGeneratorSectionProps> = ({
+ title,
+ actionText,
+ onActionTextClick,
+ isOptional,
+ marginBottom,
+ children,
+}) => (
+ <Container marginBottom={marginBottom}>
+ <Container marginBottom="10px" className="flex justify-between items-center">
+ <Heading size="small" marginBottom="0" isFlex={true}>
+ <span>{title}</span>
+ {isOptional && <OptionalText> Optional</OptionalText>}
+ </Heading>
+ {actionText && <OptionalAction onClick={onActionTextClick}>{actionText}</OptionalAction>}
+ </Container>
+ {children}
+ </Container>
+);
+
+ConfigGeneratorSection.defaultProps = {
+ marginBottom: '30px',
+};
+
+const OptionalText = styled.span`
+ display: inline;
+ font-size: 14px;
+ color: #999999;
+ flex-shrink: 0;
+`;
+
+interface CheckboxTextProps {
+ isSelected?: boolean;
+}
+
+const CheckboxText =
+ styled.span <
+ CheckboxTextProps >
+ `
+ font-size: 14px;
+ line-height: 18px;
+ color: ${props => (props.isSelected ? colors.brandDark : '#666666')}
+`;
+
+const OptionalAction = styled(OptionalText)`
+ cursor: pointer;
+`;
diff --git a/packages/website/ts/pages/instant/config_generator_address_input.tsx b/packages/website/ts/pages/instant/config_generator_address_input.tsx
index ccbaf4482..9b0e9b1d1 100644
--- a/packages/website/ts/pages/instant/config_generator_address_input.tsx
+++ b/packages/website/ts/pages/instant/config_generator_address_input.tsx
@@ -1,11 +1,13 @@
-import { colors } from '@0x/react-shared';
import { addressUtils } from '@0x/utils';
import * as _ from 'lodash';
import * as React from 'react';
+import styled from 'styled-components';
+
+import { colors } from 'ts/style/colors';
import { Container } from 'ts/components/ui/container';
-import { Input } from 'ts/components/ui/input';
-import { Text } from 'ts/components/ui/text';
+
+import { Paragraph } from 'ts/@next/components/text';
export interface ConfigGeneratorAddressInputProps {
value?: string;
@@ -16,6 +18,19 @@ export interface ConfigGeneratorAddressInputState {
errMsg: string;
}
+export interface InputProps {
+ className?: string;
+ value?: string;
+ width?: string;
+ fontSize?: string;
+ fontColor?: string;
+ padding?: string;
+ placeholderColor?: string;
+ placeholder?: string;
+ backgroundColor?: string;
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
+}
+
export class ConfigGeneratorAddressInput extends React.Component<
ConfigGeneratorAddressInputProps,
ConfigGeneratorAddressInputState
@@ -26,22 +41,13 @@ export class ConfigGeneratorAddressInput extends React.Component<
public render(): React.ReactNode {
const { errMsg } = this.state;
const hasError = !_.isEmpty(errMsg);
- const border = hasError ? '1px solid red' : undefined;
return (
<Container height="80px">
- <Input
- width="100%"
- fontSize="16px"
- padding="0.7em 1em"
- value={this.props.value}
- onChange={this._handleChange}
- placeholder="0xe99...aa8da4"
- border={border}
- />
+ <Input value={this.props.value} onChange={this._handleChange} placeholder="0xe99...aa8da4" />
<Container marginTop="5px" isHidden={!hasError} height="25px">
- <Text fontSize="14px" fontColor={colors.grey} fontStyle="italic">
+ <Paragraph size="small" isNoMargin={true}>
{errMsg}
- </Text>
+ </Paragraph>
</Container>
</Container>
);
@@ -57,3 +63,22 @@ export class ConfigGeneratorAddressInput extends React.Component<
this.props.onChange(address, isValidAddress);
};
}
+
+const PlainInput: React.StatelessComponent<InputProps> = ({ value, className, placeholder, onChange }) => (
+ <input className={className} value={value} onChange={onChange} placeholder={placeholder} />
+);
+
+export const Input = styled(PlainInput)`
+ background-color: ${colors.white};
+ color: ${colors.textDarkSecondary};
+ font-size: 1rem;
+ width: 100%;
+ padding: 16px 20px 18px;
+ border-radius: 4px;
+ border: 1px solid transparent;
+ outline: none;
+ &::placeholder {
+ color: #333333;
+ opacity: 0.5;
+ }
+`;
diff --git a/packages/website/ts/pages/instant/configurator.tsx b/packages/website/ts/pages/instant/configurator.tsx
new file mode 100644
index 000000000..7c67e6333
--- /dev/null
+++ b/packages/website/ts/pages/instant/configurator.tsx
@@ -0,0 +1,104 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { CodeDemo } from 'ts/@next/pages/instant/code_demo';
+import { ConfigGenerator } from 'ts/@next/pages/instant/config_generator';
+
+import { Link } from 'ts/@next/components/link';
+import { Column, FlexWrap } from 'ts/@next/components/newLayout';
+import { Heading } from 'ts/@next/components/text';
+import { WebsitePaths } from 'ts/types';
+
+import { ZeroExInstantBaseConfig } from '../../../../../instant/src/types';
+
+export interface ConfiguratorState {
+ instantConfig: ZeroExInstantBaseConfig;
+}
+
+export class Configurator extends React.Component {
+ public state: ConfiguratorState = {
+ instantConfig: {
+ orderSource: 'https://api.radarrelay.com/0x/v2/',
+ availableAssetDatas: undefined,
+ affiliateInfo: {
+ feeRecipient: '',
+ feePercentage: 0,
+ },
+ },
+ };
+ public render(): React.ReactNode {
+ const codeToDisplay = this._generateCodeDemoCode();
+ return (
+ <FlexWrap isFlex={true}>
+ <Column width="442px" padding="0 70px 0 0">
+ <ConfigGenerator value={this.state.instantConfig} onConfigChange={this._handleConfigChange} />
+ </Column>
+ <Column width="100%">
+ <HeadingWrapper>
+ <Heading size="small" marginBottom="15px">
+ Code Snippet
+ </Heading>
+ <Link href={`${WebsitePaths.Wiki}#Get-Started-With-Instant`} isBlock={true} target="_blank">
+ Explore the Docs
+ </Link>
+ </HeadingWrapper>
+ <CodeDemo key={codeToDisplay}>{codeToDisplay}</CodeDemo>
+ </Column>
+ </FlexWrap>
+ );
+ }
+ private readonly _handleConfigChange = (config: ZeroExInstantBaseConfig) => {
+ this.setState({
+ instantConfig: config,
+ });
+ };
+ private readonly _generateCodeDemoCode = (): string => {
+ const { instantConfig } = this.state;
+ return `<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="https://instant.0x.org/instant.js"></script>
+ </head>
+ <body>
+ <script>
+ zeroExInstant.render({
+ orderSource: '${instantConfig.orderSource}',${
+ !_.isUndefined(instantConfig.affiliateInfo) && instantConfig.affiliateInfo.feeRecipient
+ ? `\n affiliateInfo: {
+ feeRecipient: '${instantConfig.affiliateInfo.feeRecipient.toLowerCase()}',
+ feePercentage: ${instantConfig.affiliateInfo.feePercentage}
+ },`
+ : ''
+ }${
+ !_.isUndefined(instantConfig.availableAssetDatas)
+ ? `\n availableAssetDatas: ${this._renderAvailableAssetDatasString(
+ instantConfig.availableAssetDatas,
+ )}`
+ : ''
+ }
+ }, 'body');
+ </script>
+ </body>
+ </html>`;
+ };
+ private readonly _renderAvailableAssetDatasString = (availableAssetDatas: string[]): string => {
+ const stringAvailableAssetDatas = availableAssetDatas.map(assetData => `'${assetData}'`);
+ if (availableAssetDatas.length < 2) {
+ return `[${stringAvailableAssetDatas.join(', ')}]`;
+ }
+ return `[\n ${stringAvailableAssetDatas.join(
+ ', \n ',
+ )}\n ]`;
+ };
+}
+
+const HeadingWrapper = styled.div`
+ display: flex;
+ justify-content: space-between;
+
+ a {
+ transform: translateY(-8px);
+ }
+`;
diff --git a/packages/website/ts/pages/instant/fee_percentage_slider.tsx b/packages/website/ts/pages/instant/fee_percentage_slider.tsx
new file mode 100644
index 000000000..5775d6dfb
--- /dev/null
+++ b/packages/website/ts/pages/instant/fee_percentage_slider.tsx
@@ -0,0 +1,80 @@
+import Slider from 'rc-slider';
+import * as React from 'react';
+import styled from 'styled-components';
+import 'ts/@next/pages/instant/rc-slider.css';
+
+import { colors } from 'ts/style/colors';
+
+const SliderWithTooltip = (Slider as any).createSliderWithTooltip(Slider);
+// tslint:disable-next-line:no-unused-expression
+
+export interface FeePercentageSliderProps {
+ value: number;
+ isDisabled?: boolean;
+ onChange: (value: number) => void;
+}
+
+export class FeePercentageSlider extends React.Component<FeePercentageSliderProps> {
+ public render(): React.ReactNode {
+ return (
+ <StyledSlider
+ min={0}
+ max={0.05}
+ step={0.0025}
+ value={this.props.value}
+ disabled={this.props.isDisabled}
+ onChange={this.props.onChange}
+ tipFormatter={this._feePercentageSliderFormatter}
+ tipProps={{ placement: 'bottom', overlayStyle: { backgroundColor: '#fff', borderRadius: '4px' } }}
+ trackStyle={{
+ backgroundColor: colors.brandLight,
+ }}
+ railStyle={{
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ }}
+ handleStyle={{
+ border: 'none',
+ boxShadow: 'none',
+ }}
+ activeDotStyle={{
+ boxShadow: 'none',
+ border: 'none',
+ }}
+ />
+ );
+ }
+ private readonly _feePercentageSliderFormatter = (value: number): React.ReactNode => {
+ return <Text>{`${(value * 100).toFixed(2)}%`}</Text>;
+ };
+}
+
+const StyledSlider = styled(SliderWithTooltip)`
+ .rc-slider-tooltip__inner {
+ box-shadow: none !important;
+ background-color: ${colors.white} !important;
+ border-radius: 4px !important;
+ padding: 3px 12px !important;
+ height: auto !important;
+ position: relative;
+ top: 7px;
+ &:after {
+ border: solid transparent;
+ content: ' ';
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ border-width: 6px;
+ bottom: 100%;
+ left: 100%;
+ border-bottom-color: ${colors.white};
+ margin-left: -60%;
+ }
+ }
+`;
+
+const Text = styled.span`
+ color: #000000;
+ font-size: 12px;
+ line-height: 18px;
+`;
diff --git a/packages/website/ts/pages/instant/rc-slider.css b/packages/website/ts/pages/instant/rc-slider.css
new file mode 100644
index 000000000..63038324e
--- /dev/null
+++ b/packages/website/ts/pages/instant/rc-slider.css
@@ -0,0 +1,295 @@
+.rc-slider {
+ position: relative;
+ height: 14px;
+ padding: 5px 0;
+ width: 100%;
+ border-radius: 6px;
+ -ms-touch-action: none;
+ touch-action: none;
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+.rc-slider * {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+.rc-slider-rail {
+ position: absolute;
+ width: 100%;
+ background-color: #e9e9e9;
+ height: 4px;
+ border-radius: 6px;
+}
+
+.rc-slider-track {
+ position: absolute;
+ left: 0;
+ height: 4px;
+ border-radius: 6px;
+ background-color: #abe2fb;
+}
+
+.rc-slider-handle {
+ position: absolute;
+ margin-left: -7px;
+ margin-top: -5px;
+ width: 14px;
+ height: 14px;
+ cursor: pointer;
+ cursor: -webkit-grab;
+ cursor: grab;
+ border-radius: 50%;
+ border: solid 2px #96dbfa;
+ background-color: #fff;
+ -ms-touch-action: pan-x;
+ touch-action: pan-x;
+}
+
+.rc-slider-handle:focus {
+ border-color: #57c5f7;
+ box-shadow: 0 0 0 5px #96dbfa;
+ outline: none;
+}
+
+.rc-slider-handle-click-focused:focus {
+ border-color: #96dbfa;
+ box-shadow: unset;
+}
+
+.rc-slider-handle:hover {
+ border-color: #57c5f7;
+}
+
+.rc-slider-handle:active {
+ border-color: #57c5f7;
+ box-shadow: 0 0 5px #57c5f7;
+ cursor: -webkit-grabbing;
+ cursor: grabbing;
+}
+
+.rc-slider-mark {
+ position: absolute;
+ top: 18px;
+ left: 0;
+ width: 100%;
+ font-size: 12px;
+}
+
+.rc-slider-mark-text {
+ position: absolute;
+ display: inline-block;
+ vertical-align: middle;
+ text-align: center;
+ cursor: pointer;
+ color: #999;
+}
+
+.rc-slider-mark-text-active {
+ color: #666;
+}
+
+.rc-slider-step {
+ position: absolute;
+ width: 100%;
+ height: 4px;
+ background: transparent;
+}
+
+.rc-slider-dot {
+ position: absolute;
+ bottom: -2px;
+ margin-left: -4px;
+ width: 8px;
+ height: 8px;
+ border: 2px solid #e9e9e9;
+ background-color: #fff;
+ cursor: pointer;
+ border-radius: 50%;
+ vertical-align: middle;
+}
+
+.rc-slider-dot-active {
+ border-color: #96dbfa;
+}
+
+.rc-slider-disabled {
+ opacity: 0.2;
+}
+
+.rc-slider-disabled .rc-slider-track {
+ background-color: #ccc;
+}
+
+.rc-slider-disabled .rc-slider-handle,
+.rc-slider-disabled .rc-slider-dot {
+ border-color: #ccc;
+ box-shadow: none;
+ background-color: #fff;
+ cursor: not-allowed;
+}
+
+.rc-slider-disabled .rc-slider-mark-text,
+.rc-slider-disabled .rc-slider-dot {
+ cursor: not-allowed !important;
+}
+
+.rc-slider-vertical {
+ width: 14px;
+ height: 100%;
+ padding: 0 5px;
+}
+
+.rc-slider-vertical .rc-slider-rail {
+ height: 100%;
+ width: 4px;
+}
+
+.rc-slider-vertical .rc-slider-track {
+ left: 5px;
+ bottom: 0;
+ width: 4px;
+}
+
+.rc-slider-vertical .rc-slider-handle {
+ margin-left: -5px;
+ margin-bottom: -7px;
+ -ms-touch-action: pan-y;
+ touch-action: pan-y;
+}
+
+.rc-slider-vertical .rc-slider-mark {
+ top: 0;
+ left: 18px;
+ height: 100%;
+}
+
+.rc-slider-vertical .rc-slider-step {
+ height: 100%;
+ width: 4px;
+}
+
+.rc-slider-vertical .rc-slider-dot {
+ left: 2px;
+ margin-bottom: -4px;
+}
+
+.rc-slider-vertical .rc-slider-dot:first-child {
+ margin-bottom: -4px;
+}
+
+.rc-slider-vertical .rc-slider-dot:last-child {
+ margin-bottom: -4px;
+}
+
+.rc-slider-tooltip-zoom-down-enter,
+.rc-slider-tooltip-zoom-down-appear {
+ animation-duration: .3s;
+ animation-fill-mode: both;
+ display: block !important;
+ animation-play-state: paused;
+}
+
+.rc-slider-tooltip-zoom-down-leave {
+ animation-duration: .3s;
+ animation-fill-mode: both;
+ display: block !important;
+ animation-play-state: paused;
+}
+
+.rc-slider-tooltip-zoom-down-enter.rc-slider-tooltip-zoom-down-enter-active,
+.rc-slider-tooltip-zoom-down-appear.rc-slider-tooltip-zoom-down-appear-active {
+ animation-name: rcSliderTooltipZoomDownIn;
+ animation-play-state: running;
+}
+
+.rc-slider-tooltip-zoom-down-leave.rc-slider-tooltip-zoom-down-leave-active {
+ animation-name: rcSliderTooltipZoomDownOut;
+ animation-play-state: running;
+}
+
+.rc-slider-tooltip-zoom-down-enter,
+.rc-slider-tooltip-zoom-down-appear {
+ transform: scale(0, 0);
+ animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
+}
+
+.rc-slider-tooltip-zoom-down-leave {
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+}
+
+@keyframes rcSliderTooltipZoomDownIn {
+ 0% {
+ opacity: 0;
+ transform-origin: 50% 100%;
+ transform: scale(0, 0);
+ }
+
+ 100% {
+ transform-origin: 50% 100%;
+ transform: scale(1, 1);
+ }
+}
+
+@keyframes rcSliderTooltipZoomDownOut {
+ 0% {
+ transform-origin: 50% 100%;
+ transform: scale(1, 1);
+ }
+
+ 100% {
+ opacity: 0;
+ transform-origin: 50% 100%;
+ transform: scale(0, 0);
+ }
+}
+
+.rc-slider-tooltip {
+ position: absolute;
+ left: -9999px;
+ top: -9999px;
+ visibility: visible;
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+.rc-slider-tooltip * {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+.rc-slider-tooltip-hidden {
+ display: none;
+}
+
+.rc-slider-tooltip-placement-top {
+ padding: 4px 0 8px 0;
+}
+
+.rc-slider-tooltip-inner {
+ padding: 4px 6px 4px;
+ min-width: 24px;
+ height: 24px;
+ font-size: 12px;
+ line-height: 1;
+ color: #000;
+ text-align: center;
+ text-decoration: none;
+}
+
+.rc-slider-tooltip-arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+}
+
+.rc-slider-tooltip-placement-top .rc-slider-tooltip-arrow {
+ bottom: 4px;
+ left: 50%;
+ margin-left: -4px;
+ border-width: 4px 4px 0;
+ border-top-color: #6c6c6c;
+}
diff --git a/packages/website/ts/pages/instant/select.tsx b/packages/website/ts/pages/instant/select.tsx
new file mode 100644
index 000000000..d4146cfb0
--- /dev/null
+++ b/packages/website/ts/pages/instant/select.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+export interface SelectItemConfig {
+ label: string;
+ value?: string;
+ onClick?: () => void;
+}
+
+interface SelectProps {
+ value?: string;
+ id: string;
+ items: SelectItemConfig[];
+ emptyText?: string;
+ onChange?: (ev: React.ChangeEvent<HTMLSelectElement>) => void;
+ shouldIncludeEmpty: boolean;
+}
+
+export const Select: React.FunctionComponent<SelectProps> = ({
+ value,
+ id,
+ items,
+ shouldIncludeEmpty,
+ emptyText,
+ onChange,
+}) => {
+ return (
+ <Container>
+ <StyledSelect id={id} onChange={onChange}>
+ {shouldIncludeEmpty && <option value="">{emptyText}</option>}
+ {items.map((item, index) => (
+ <option
+ key={`${id}-item-${index}`}
+ value={item.value}
+ selected={item.value === value}
+ onClick={item.onClick}
+ >
+ {item.label}
+ </option>
+ ))}
+ </StyledSelect>
+ <Caret width="12" height="7" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M11 1L6 6 1 1" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
+ </Caret>
+ </Container>
+ );
+};
+
+Select.defaultProps = {
+ emptyText: 'Select...',
+ shouldIncludeEmpty: true,
+};
+
+const Container = styled.div`
+ background-color: #fff;
+ border-radius: 4px;
+ display: flex;
+ width: 100%;
+ position: relative;
+`;
+
+const StyledSelect = styled.select`
+ appearance: none;
+ border: 0;
+ font-size: 1rem;
+ width: 100%;
+ padding: 20px 20px 20px 20px;
+`;
+
+const Caret = styled.svg`
+ position: absolute;
+ right: 20px;
+ top: calc(50% - 4px);
+`;