diff options
Diffstat (limited to 'packages/website/ts/pages/instant')
-rw-r--r-- | packages/website/ts/pages/instant/action_link.tsx | 46 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/code_demo.tsx | 148 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/config_generator.tsx | 291 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/config_generator_address_input.tsx | 58 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/configurator.tsx | 99 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/features.tsx | 116 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/instant.tsx | 87 | ||||
-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/screenshots.tsx | 35 |
10 files changed, 999 insertions, 0 deletions
diff --git a/packages/website/ts/pages/instant/action_link.tsx b/packages/website/ts/pages/instant/action_link.tsx new file mode 100644 index 000000000..c196f03ef --- /dev/null +++ b/packages/website/ts/pages/instant/action_link.tsx @@ -0,0 +1,46 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; +import { colors } from 'ts/style/colors'; +import { utils } from 'ts/utils/utils'; + +export interface ActionLinkProps { + displayText: string; + linkSrc?: string; + onClick?: () => void; + fontSize?: number; + color?: string; + className?: string; +} + +export class ActionLink extends React.Component<ActionLinkProps> { + public static defaultProps = { + fontSize: 16, + color: colors.white, + }; + public render(): React.ReactNode { + const { displayText, fontSize, color, className } = this.props; + return ( + <Container className={`flex items-center ${className}`} onClick={this._handleClick} cursor="pointer"> + <Container> + <Text fontSize="16px" fontColor={color}> + {displayText} + </Text> + </Container> + <Container paddingTop="1px" paddingLeft="6px"> + <i className="zmdi zmdi-chevron-right bold" style={{ fontSize, color }} /> + </Container> + </Container> + ); + } + + private readonly _handleClick = (event: React.MouseEvent<HTMLElement>) => { + if (!_.isUndefined(this.props.onClick)) { + this.props.onClick(); + } else if (!_.isUndefined(this.props.linkSrc)) { + utils.openUrl(this.props.linkSrc); + } + }; +} diff --git a/packages/website/ts/pages/instant/code_demo.tsx b/packages/website/ts/pages/instant/code_demo.tsx new file mode 100644 index 000000000..e57e39dff --- /dev/null +++ b/packages/website/ts/pages/instant/code_demo.tsx @@ -0,0 +1,148 @@ +import * as React from 'react'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import { colors } from 'ts/style/colors'; +import { styled } from 'ts/style/theme'; + +const CustomPre = styled.pre` + margin: 0px; + line-height: 24px; + overflow: scroll; + width: 600px; + height: 100%; + max-height: 800px; + border-radius: 4px; + code { + background-color: inherit !important; + border-radius: 0px; + font-family: 'Roboto Mono', sans-serif; + border: none; + } + code:first-of-type { + background-color: #2a2a2a !important; + color: #999; + min-height: 100%; + text-align: center; + padding-right: 5px !important; + padding-left: 5px; + margin-right: 15px; + line-height: 25px; + padding-top: 10px; + } + code:last-of-type { + position: relative; + top: 10px; + } +`; + +const customStyle = { + 'hljs-comment': { + color: '#7e7887', + }, + 'hljs-quote': { + color: '#7e7887', + }, + 'hljs-variable': { + color: '#be4678', + }, + 'hljs-template-variable': { + color: '#be4678', + }, + 'hljs-attribute': { + color: '#be4678', + }, + 'hljs-regexp': { + color: '#be4678', + }, + 'hljs-link': { + color: '#be4678', + }, + 'hljs-tag': { + color: '#61f5ff', + }, + 'hljs-name': { + color: '#61f5ff', + }, + 'hljs-selector-id': { + color: '#be4678', + }, + 'hljs-selector-class': { + color: '#be4678', + }, + 'hljs-number': { + color: '#c994ff', + }, + 'hljs-meta': { + color: '#aa573c', + }, + 'hljs-built_in': { + color: '#aa573c', + }, + 'hljs-builtin-name': { + color: '#aa573c', + }, + 'hljs-literal': { + color: '#aa573c', + }, + 'hljs-type': { + color: '#aa573c', + }, + 'hljs-params': { + color: '#aa573c', + }, + 'hljs-string': { + color: '#bcff88', + }, + 'hljs-symbol': { + color: '#2a9292', + }, + 'hljs-bullet': { + color: '#2a9292', + }, + 'hljs-title': { + color: '#576ddb', + }, + 'hljs-section': { + color: '#576ddb', + }, + 'hljs-keyword': { + color: '#955ae7', + }, + 'hljs-selector-tag': { + color: '#955ae7', + }, + 'hljs-deletion': { + color: '#19171c', + display: 'inline-block', + width: '100%', + backgroundColor: '#be4678', + }, + 'hljs-addition': { + color: '#19171c', + display: 'inline-block', + width: '100%', + backgroundColor: '#2a9292', + }, + hljs: { + display: 'block', + overflowX: 'hidden', + background: colors.instantSecondaryBackground, + color: 'white', + fontSize: '12px', + }, + 'hljs-emphasis': { + fontStyle: 'italic', + }, + 'hljs-strong': { + fontWeight: 'bold', + }, +}; + +export interface CodeDemoProps { + children: string; +} + +export const CodeDemo: React.StatelessComponent<CodeDemoProps> = props => ( + <SyntaxHighlighter language="html" style={customStyle} showLineNumbers={true} PreTag={CustomPre}> + {props.children} + </SyntaxHighlighter> +); 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..efd1be096 --- /dev/null +++ b/packages/website/ts/pages/instant/config_generator.tsx @@ -0,0 +1,291 @@ +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 Slider from 'material-ui/Slider'; +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'; +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 { colors } from 'ts/style/colors'; +import { WebsiteBackendTokenInfo } from 'ts/types'; +import { backendClient } from 'ts/utils/backend_client'; +import { constants } from 'ts/utils/constants'; + +import { ZeroExInstantBaseConfig } from '../../../../instant/src/types'; + +export interface ConfigGeneratorProps { + value: ZeroExInstantBaseConfig; + onConfigChange: (config: ZeroExInstantBaseConfig) => void; +} + +export interface ConfigGeneratorState { + isLoadingAvailableTokens: boolean; + // Address to token info + allKnownTokens: ObjectMap<WebsiteBackendTokenInfo>; + availableTokens?: WebsiteBackendTokenInfo[]; +} + +const SRA_ENDPOINTS = ['https://api.radarrelay.com/0x/v2/', 'https://api.openrelay.xyz/v2/']; + +export class ConfigGenerator extends React.Component<ConfigGeneratorProps, ConfigGeneratorState> { + public state: ConfigGeneratorState = { + isLoadingAvailableTokens: true, + allKnownTokens: {}, + }; + public componentDidMount(): void { + this._setAllKnownTokens(this._setAvailableAssetsFromOrderProvider); + } + public componentDidUpdate(prevProps: ConfigGeneratorProps): void { + if (prevProps.value.orderSource !== this.props.value.orderSource) { + this._setAvailableAssetsFromOrderProvider(); + } + } + 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="Standard relayer API endpoint"> + <Select value={value.orderSource} items={this._generateItems()} /> + </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} + > + <Slider + min={0} + max={0.05} + step={0.0025} + value={value.affiliateInfo.feePercentage} + onChange={this._handleAffiliatePercentageChange} + /> + </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 _handleAffiliatePercentageLearnMoreClick = (): void => { + window.open('/wiki#Learn-About-Affiliate-Fees', '_blank'); + }; + private readonly _handleSRASelection = (sraEndpoint: string) => { + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + orderSource: sraEndpoint, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _handleAffiliateAddressChange = (address: string) => { + const oldConfig: ZeroExInstantBaseConfig = this.props.value; + const newConfig: ZeroExInstantBaseConfig = { + ...oldConfig, + affiliateInfo: { + feeRecipient: address, + feePercentage: oldConfig.affiliateInfo.feePercentage, + }, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _handleAffiliatePercentageChange = (event: any, 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; + 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 = [...availableAssetDatas, assetData]; + } else { + // Remove it + newAvailableAssetDatas = _.pull(availableAssetDatas, assetData); + } + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + availableAssetDatas: newAvailableAssetDatas, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _setAllKnownTokens = async (callback: () => void): Promise<void> => { + const tokenInfos = await backendClient.getTokenInfosAsync(); + const allKnownTokens = _.reduce( + tokenInfos, + (acc, tokenInfo) => { + acc[tokenInfo.address] = tokenInfo; + return acc; + }, + {} as ObjectMap<WebsiteBackendTokenInfo>, + ); + this.setState({ allKnownTokens }, callback); + }; + 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 = _.compact( + _.map(assetDatas, assetData => { + const address = assetDataUtils.decodeAssetDataOrThrow(assetData).tokenAddress; + return this.state.allKnownTokens[address]; + }), + ); + 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 items = _.map(availableTokens, token => { + const assetData = assetDataUtils.encodeERC20AssetData(token.address); + return { + value: assetDataUtils.encodeERC20AssetData(token.address), + 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} 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"> + <Container> + <Text fontColor={colors.white} fontSize="16px" lineHeight="18px"> + {title} + </Text> + {isOptional && ( + <Text fontColor={colors.grey} fontSize="16px" lineHeight="18px"> + (optional) + </Text> + )} + </Container> + {actionText && ( + <Text fontSize="12px" fontColor={colors.grey} onClick={onActionTextClick}> + {actionText} + </Text> + )} + </Container> + {children} + </Container> +); + +ConfigGeneratorSection.defaultProps = { + marginBottom: '30px', +}; diff --git a/packages/website/ts/pages/instant/config_generator_address_input.tsx b/packages/website/ts/pages/instant/config_generator_address_input.tsx new file mode 100644 index 000000000..2e5a6e533 --- /dev/null +++ b/packages/website/ts/pages/instant/config_generator_address_input.tsx @@ -0,0 +1,58 @@ +import { colors } from '@0x/react-shared'; +import { addressUtils } from '@0x/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Container } from 'ts/components/ui/container'; +import { Input } from 'ts/components/ui/input'; +import { Text } from 'ts/components/ui/text'; + +export interface ConfigGeneratorAddressInputProps { + value?: string; + onChange?: (address: string) => void; +} + +export interface ConfigGeneratorAddressInputState { + errMsg: string; +} + +export class ConfigGeneratorAddressInput extends React.Component< + ConfigGeneratorAddressInputProps, + ConfigGeneratorAddressInputState +> { + public state = { + errMsg: '', + }; + 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" + value={this.props.value} + onChange={this._handleChange} + placeholder="0xe99...aa8da4" + border={border} + /> + <Container marginTop="5px" isHidden={!hasError} height="25px"> + <Text fontSize="14px" fontColor={colors.grey} fontStyle="italic"> + {errMsg} + </Text> + </Container> + </Container> + ); + } + + private readonly _handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => { + const address = event.target.value; + const isValidAddress = addressUtils.isAddress(address.toLowerCase()) || address === ''; + const errMsg = isValidAddress ? '' : 'Please enter a valid Ethereum address'; + this.setState({ + errMsg, + }); + this.props.onChange(address); + }; +} diff --git a/packages/website/ts/pages/instant/configurator.tsx b/packages/website/ts/pages/instant/configurator.tsx new file mode 100644 index 000000000..5700bdc1d --- /dev/null +++ b/packages/website/ts/pages/instant/configurator.tsx @@ -0,0 +1,99 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; +import { ActionLink } from 'ts/pages/instant/action_link'; +import { CodeDemo } from 'ts/pages/instant/code_demo'; +import { ConfigGenerator } from 'ts/pages/instant/config_generator'; +import { colors } from 'ts/style/colors'; + +import { ZeroExInstantBaseConfig } from '../../../../instant/src/types'; + +export interface ConfiguratorProps { + hash: string; +} + +export interface ConfiguratorState { + instantConfig: ZeroExInstantBaseConfig; +} + +export class Configurator extends React.Component<ConfiguratorProps> { + public state: ConfiguratorState = { + instantConfig: { + orderSource: 'https://api.radarrelay.com/0x/v2/', + availableAssetDatas: [], + affiliateInfo: { + feeRecipient: '', + feePercentage: 0.01, + }, + }, + }; + public render(): React.ReactNode { + const { hash } = this.props; + const codeToDisplay = this._generateCodeDemoCode(); + return ( + <Container + className="flex justify-center py4 px3" + id={hash} + backgroundColor={colors.instantTertiaryBackground} + > + <Container className="mx3"> + <Container className="mb3"> + <Text fontSize="20px" lineHeight="28px" fontColor={colors.white} fontWeight={500}> + 0x Instant Configurator + </Text> + </Container> + <ConfigGenerator value={this.state.instantConfig} onConfigChange={this._handleConfigChange} /> + </Container> + <Container className="mx3" height="550px"> + <Container className="mb3 flex justify-between"> + <Text fontSize="20px" lineHeight="28px" fontColor={colors.white} fontWeight={500}> + Code Snippet + </Text> + <ActionLink displayText="Explore the Docs" linkSrc="/docs/instant" color={colors.grey} /> + </Container> + <CodeDemo key={codeToDisplay}>{codeToDisplay}</CodeDemo> + </Container> + </Container> + ); + } + private readonly _handleConfigChange = (config: ZeroExInstantBaseConfig) => { + this.setState({ + instantConfig: config, + }); + }; + private readonly _generateCodeDemoCode = (): string => { + const { instantConfig } = this.state; + return `<head> + <script src="https://instant.0xproject.com/instant.js"></script> +</head> +<body> + <script> + zeroExInstant.render({ + liquiditySource: '${instantConfig.orderSource}',${ + !_.isUndefined(instantConfig.affiliateInfo) && instantConfig.affiliateInfo.feeRecipient + ? `\n affiliateInfo: { + feeRecipient: '${instantConfig.affiliateInfo.feeRecipient}', + feePercentage: ${instantConfig.affiliateInfo.feePercentage} + }` + : '' + }${ + !_.isUndefined(instantConfig.availableAssetDatas) + ? `\n availableAssetDatas: ${this._renderAvailableAssetDatasString( + instantConfig.availableAssetDatas, + )}` + : '' + } + }, 'body'); + </script> +</body>`; + }; + private readonly _renderAvailableAssetDatasString = (availableAssetDatas: string[]): string => { + const stringAvailableAssetDatas = availableAssetDatas.map(assetData => `'${assetData}'`); + if (availableAssetDatas.length < 2) { + return `[${stringAvailableAssetDatas.join(', ')}]`; + } + return `[\n\t\t${stringAvailableAssetDatas.join(', \n\t\t')}\n ]`; + }; +} diff --git a/packages/website/ts/pages/instant/features.tsx b/packages/website/ts/pages/instant/features.tsx new file mode 100644 index 000000000..230a8496b --- /dev/null +++ b/packages/website/ts/pages/instant/features.tsx @@ -0,0 +1,116 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Container } from 'ts/components/ui/container'; +import { Image } from 'ts/components/ui/image'; +import { Text } from 'ts/components/ui/text'; +import { ActionLink, ActionLinkProps } from 'ts/pages/instant/action_link'; +import { colors } from 'ts/style/colors'; +import { ScreenWidths } from 'ts/types'; +import { utils } from 'ts/utils/utils'; + +export interface FeatureProps { + screenWidth: ScreenWidths; + onGetStartedClick: () => void; +} + +export const Features = (props: FeatureProps) => { + const isSmallScreen = props.screenWidth === ScreenWidths.Sm; + const getStartedLinkInfo = { + displayText: 'Get started', + onClick: props.onGetStartedClick, + }; + const exploreTheDocsLinkInfo = { + displayText: 'Explore the docs', + linkSrc: `${utils.getCurrentBaseUrl()}/wiki#Get-Started`, + }; + const tokenLinkInfos = isSmallScreen ? [getStartedLinkInfo] : [getStartedLinkInfo, exploreTheDocsLinkInfo]; + return ( + <Container backgroundColor={colors.instantSecondaryBackground} className="py3 flex flex-column px3"> + <FeatureItem + imgSrc="images/instant/feature_1.svg" + title="Support ERC-20 and ERC-721 tokens" + description="Seamlessly integrate token purchasing into your product experience by offering digital assets ranging from in-game items to stablecoins." + linkInfos={tokenLinkInfos} + screenWidth={props.screenWidth} + /> + <FeatureItem + imgSrc="images/instant/feature_2.svg" + title="Generate revenue for your business" + description="With just a few lines of code, you can earn up to 5% in affiliate fees on every transaction from your crypto wallet or dApp." + linkInfos={[ + { + displayText: 'Learn about affiliate fees', + linkSrc: `${utils.getCurrentBaseUrl()}/wiki#Learn-About-Affiliate-Fees`, + }, + ]} + screenWidth={props.screenWidth} + /> + <FeatureItem + imgSrc="images/instant/feature_3.svg" + title="Easy and Flexible Integration" + description="Use our out-of-the-box design or customize the user interface by integrating the AssetBuyer engine. You can also tap into 0x networked liquidity or choose your own liquidity pool." + linkInfos={[ + { + displayText: 'Explore AssetBuyer', + linkSrc: `${utils.getCurrentBaseUrl()}/docs/asset-buyer`, + }, + ]} + screenWidth={props.screenWidth} + /> + </Container> + ); +}; + +interface FeatureItemProps { + imgSrc: string; + title: string; + description: string; + linkInfos: ActionLinkProps[]; + screenWidth: ScreenWidths; +} + +const FeatureItem = (props: FeatureItemProps) => { + const { imgSrc, title, description, linkInfos, screenWidth } = props; + const isLargeScreen = screenWidth === ScreenWidths.Lg; + const maxWidth = isLargeScreen ? '500px' : undefined; + const image = ( + <Container className="center" minWidth="435px" maxHeight="225px"> + <Image src={imgSrc} additionalStyle={{ filter: 'drop-shadow(0px 4px 4px rgba(0,0,0,.25))' }} /> + </Container> + ); + const info = ( + <Container maxWidth={maxWidth}> + <Text fontSize="24px" lineHeight="34px" fontColor={colors.white} fontWeight={500}> + {title} + </Text> + <Container marginTop="28px"> + <Text fontFamily="Roboto Mono" fontSize="14px" lineHeight="2em" fontColor={colors.grey500}> + {description} + </Text> + </Container> + <Container className="flex" marginTop="28px"> + {_.map(linkInfos, linkInfo => ( + <Container key={linkInfo.displayText} marginRight="32px"> + <ActionLink {...linkInfo} /> + </Container> + ))} + </Container> + </Container> + ); + return ( + <Container className="flex flex-column items-center py4 px3"> + {isLargeScreen ? ( + <Container className="flex"> + {image} + <Container marginLeft="115px">{info}</Container> + </Container> + ) : ( + <Container className="flex flex-column items-center" width="100%"> + {image} + <Container marginTop="48px">{info}</Container> + </Container> + )} + </Container> + ); +}; diff --git a/packages/website/ts/pages/instant/instant.tsx b/packages/website/ts/pages/instant/instant.tsx new file mode 100644 index 000000000..fa6bd1c33 --- /dev/null +++ b/packages/website/ts/pages/instant/instant.tsx @@ -0,0 +1,87 @@ +import { utils as sharedUtils } from '@0x/react-shared'; +import * as _ from 'lodash'; +import * as React from 'react'; +import * as DocumentTitle from 'react-document-title'; + +import { Footer } from 'ts/components/footer'; +import { MetaTags } from 'ts/components/meta_tags'; +import { TopBar } from 'ts/components/top_bar/top_bar'; +import { Container } from 'ts/components/ui/container'; +import { Configurator } from 'ts/pages/instant/configurator'; +import { Features } from 'ts/pages/instant/features'; +import { Introducing0xInstant } from 'ts/pages/instant/introducing_0x_instant'; +import { NeedMore } from 'ts/pages/instant/need_more'; +import { Screenshots } from 'ts/pages/instant/screenshots'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { colors } from 'ts/style/colors'; +import { ScreenWidths } from 'ts/types'; +import { Translate } from 'ts/utils/translate'; +import { utils } from 'ts/utils/utils'; + +export interface InstantProps { + location: Location; + translate: Translate; + dispatcher: Dispatcher; + screenWidth: ScreenWidths; +} + +export interface InstantState {} + +const CONFIGURATOR_HASH = 'configure'; +const THROTTLE_TIMEOUT = 100; +const DOCUMENT_TITLE = '0x Instant'; +const DOCUMENT_DESCRIPTION = '0x Instant'; + +export class Instant extends React.Component<InstantProps, InstantState> { + // TODO: consolidate this small screen scaffolding into one place (its being used in portal and docs as well) + private readonly _throttledScreenWidthUpdate: () => void; + public constructor(props: InstantProps) { + super(props); + this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); + } + public componentDidMount(): void { + window.addEventListener('resize', this._throttledScreenWidthUpdate); + window.scrollTo(0, 0); + } + public render(): React.ReactNode { + return ( + <Container overflowX="hidden"> + <MetaTags title={DOCUMENT_TITLE} description={DOCUMENT_DESCRIPTION} /> + <DocumentTitle title={DOCUMENT_TITLE} /> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + style={{ backgroundColor: colors.instantPrimaryBackground, position: 'relative' }} + translate={this.props.translate} + isNightVersion={true} + /> + <Container backgroundColor={colors.instantPrimaryBackground} /> + <Introducing0xInstant screenWidth={this.props.screenWidth} onCTAClick={this._onGetStartedClick} /> + <Screenshots screenWidth={this.props.screenWidth} /> + <Features screenWidth={this.props.screenWidth} onGetStartedClick={this._onGetStartedClick} /> + {!this._isSmallScreen() && <Configurator hash={CONFIGURATOR_HASH} />} + <NeedMore screenWidth={this.props.screenWidth} /> + <Footer translate={this.props.translate} dispatcher={this.props.dispatcher} /> + </Container> + ); + } + private readonly _onGetStartedClick = () => { + if (this._isSmallScreen()) { + utils.openUrl(`${utils.getCurrentBaseUrl()}/wiki#Get-Started`); + } else { + this._scrollToConfigurator(); + } + }; + private _isSmallScreen(): boolean { + const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; + return isSmallScreen; + } + private _scrollToConfigurator(): void { + sharedUtils.setUrlHash(CONFIGURATOR_HASH); + sharedUtils.scrollToHash(CONFIGURATOR_HASH, ''); + } + private _updateScreenWidth(): void { + const newScreenWidth = utils.getScreenWidth(); + this.props.dispatcher.updateScreenWidth(newScreenWidth); + } +} diff --git a/packages/website/ts/pages/instant/introducing_0x_instant.tsx b/packages/website/ts/pages/instant/introducing_0x_instant.tsx new file mode 100644 index 000000000..da3f09faa --- /dev/null +++ b/packages/website/ts/pages/instant/introducing_0x_instant.tsx @@ -0,0 +1,57 @@ +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 new file mode 100644 index 000000000..e6d5c3694 --- /dev/null +++ b/packages/website/ts/pages/instant/need_more.tsx @@ -0,0 +1,62 @@ +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'; +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(`${utils.getCurrentBaseUrl()}/wiki#Get-Started`); +}; diff --git a/packages/website/ts/pages/instant/screenshots.tsx b/packages/website/ts/pages/instant/screenshots.tsx new file mode 100644 index 000000000..7dcf17fd1 --- /dev/null +++ b/packages/website/ts/pages/instant/screenshots.tsx @@ -0,0 +1,35 @@ +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> + ); +}; |