diff options
Diffstat (limited to 'packages/website/ts/pages/instant')
-rw-r--r-- | packages/website/ts/pages/instant/code_demo.tsx | 30 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/config_generator.tsx | 332 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/config_generator_address_input.tsx | 55 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/configurator.tsx | 104 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/fee_percentage_slider.tsx | 80 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/introducing_0x_instant.tsx | 57 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/need_more.tsx | 62 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/rc-slider.css | 295 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/screenshots.tsx | 35 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/select.tsx | 74 |
10 files changed, 943 insertions, 181 deletions
diff --git a/packages/website/ts/pages/instant/code_demo.tsx b/packages/website/ts/pages/instant/code_demo.tsx index a3b5fe847..c59f148b8 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/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..e43d47119 --- /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 { 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 { ConfigGeneratorAddressInput } from 'ts/pages/instant/config_generator_address_input'; +import { FeePercentageSlider } from 'ts/pages/instant/fee_percentage_slider'; +import { colors } from 'ts/style/colors'; +import { WebsitePaths } from 'ts/types'; +import { constants } from 'ts/utils/constants'; + +// New components +import { Heading } from 'ts/components/text'; +import { Select, SelectItemConfig } from 'ts/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..890e39da6 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/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..a63e1752e --- /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/pages/instant/code_demo'; +import { ConfigGenerator } from 'ts/pages/instant/config_generator'; + +import { Link } from 'ts/components/link'; +import { Column, FlexWrap } from 'ts/components/newLayout'; +import { Heading } from 'ts/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..c4d9f908f --- /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/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/introducing_0x_instant.tsx b/packages/website/ts/pages/instant/introducing_0x_instant.tsx deleted file mode 100644 index da3f09faa..000000000 --- a/packages/website/ts/pages/instant/introducing_0x_instant.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from 'react'; - -import { Button } from 'ts/components/ui/button'; -import { Container } from 'ts/components/ui/container'; -import { Text } from 'ts/components/ui/text'; -import { colors } from 'ts/style/colors'; -import { ScreenWidths } from 'ts/types'; - -export interface Introducing0xInstantProps { - screenWidth: ScreenWidths; - onCTAClick: () => void; -} - -export const Introducing0xInstant = (props: Introducing0xInstantProps) => { - const isSmallScreen = props.screenWidth === ScreenWidths.Sm; - const zero = ( - <Text fontColor={colors.white} fontSize="42px" fontWeight="600" fontFamily="Roboto Mono" Tag="span"> - 0 - </Text> - ); - const title = isSmallScreen ? ( - <div> - Introducing<br /> - {zero}x Instant - </div> - ) : ( - <div>Introducing {zero}x Instant</div> - ); - return ( - <div className="clearfix center lg-pt4 md-pt4" style={{ backgroundColor: colors.instantPrimaryBackground }}> - <div className="mx-auto inline-block align-middle py4" style={{ lineHeight: '44px', textAlign: 'center' }}> - <Container className="sm-center sm-pt3"> - <Text fontColor={colors.white} fontSize="42px" lineHeight="52px" fontWeight="600"> - {title} - </Text> - </Container> - <Container className="pb2 lg-pt2 md-pt2 sm-pt3 sm-px3 sm-center" maxWidth="600px"> - <Text fontColor={colors.grey500} fontSize="20px" lineHeight="32px" fontFamily="Roboto Mono"> - A free and flexible way to offer simple crypto - <br /> purchasing in any app or website. - </Text> - </Container> - <div className="py3"> - <Button - type="button" - backgroundColor={colors.mediumBlue} - fontColor={colors.white} - fontSize="18px" - onClick={props.onCTAClick} - > - Get Started - </Button> - </div> - </div> - </div> - ); -}; diff --git a/packages/website/ts/pages/instant/need_more.tsx b/packages/website/ts/pages/instant/need_more.tsx deleted file mode 100644 index 70aea7363..000000000 --- a/packages/website/ts/pages/instant/need_more.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from 'react'; - -import { Button } from 'ts/components/ui/button'; -import { Container } from 'ts/components/ui/container'; -import { Text } from 'ts/components/ui/text'; -import { colors } from 'ts/style/colors'; -import { ScreenWidths, WebsitePaths } from 'ts/types'; -import { constants } from 'ts/utils/constants'; -import { utils } from 'ts/utils/utils'; - -export interface NeedMoreProps { - screenWidth: ScreenWidths; -} -export const NeedMore = (props: NeedMoreProps) => { - const isSmallScreen = props.screenWidth === ScreenWidths.Sm; - const backgroundColor = isSmallScreen ? colors.instantTertiaryBackground : colors.instantSecondaryBackground; - const className = isSmallScreen ? 'flex flex-column items-center' : 'flex'; - const marginRight = isSmallScreen ? undefined : '200px'; - return ( - <Container className="flex flex-column items-center py4 px3" backgroundColor={backgroundColor}> - <Container className={className}> - <Container className="sm-center" marginRight={marginRight}> - <Text fontColor={colors.white} fontSize="32px" lineHeight="45px"> - Need more flexibility? - </Text> - <Text fontColor={colors.grey500} fontSize="18px" lineHeight="27px"> - View our full documentation or reach out if you have any questions. - </Text> - </Container> - <Container className="py3 flex"> - <Container marginRight="20px"> - <Button - type="button" - backgroundColor={colors.white} - fontColor={backgroundColor} - fontSize="18px" - onClick={onGetInTouchClick} - > - Get in Touch - </Button> - </Container> - <Button - type="button" - backgroundColor={colors.mediumBlue} - fontColor={colors.white} - fontSize="18px" - onClick={onDocsClick} - > - Explore the Docs - </Button> - </Container> - </Container> - </Container> - ); -}; - -const onGetInTouchClick = () => { - utils.openUrl(constants.URL_ZEROEX_CHAT); -}; -const onDocsClick = () => { - utils.openUrl(`${WebsitePaths.Wiki}#Get-Started-With-Instant`); -}; 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/screenshots.tsx b/packages/website/ts/pages/instant/screenshots.tsx deleted file mode 100644 index 7dcf17fd1..000000000 --- a/packages/website/ts/pages/instant/screenshots.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as _ from 'lodash'; -import * as React from 'react'; - -import { Container } from 'ts/components/ui/container'; -import { colors } from 'ts/style/colors'; -import { ScreenWidths } from 'ts/types'; - -export interface ScreenshotsProps { - screenWidth: ScreenWidths; -} - -export const Screenshots = (props: ScreenshotsProps) => { - const isSmallScreen = props.screenWidth === ScreenWidths.Sm; - const images = isSmallScreen - ? [ - 'images/instant/rep_screenshot.png', - 'images/instant/dai_screenshot.png', - 'images/instant/gods_screenshot.png', - ] - : [ - 'images/instant/nmr_screenshot.png', - 'images/instant/kitty_screenshot.png', - 'images/instant/rep_screenshot.png', - 'images/instant/dai_screenshot.png', - 'images/instant/gods_screenshot.png', - 'images/instant/gnt_screenshot.png', - ]; - return ( - <Container backgroundColor={colors.instantPrimaryBackground} className="py3 flex justify-center"> - {_.map(images, image => { - return <img className="px1 flex-none" width="300px" height="420px" src={image} key={image} />; - })} - </Container> - ); -}; 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); +`; |