diff options
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_provider.tsx | 28 | ||||
-rw-r--r-- | packages/instant/src/types.ts | 18 | ||||
-rw-r--r-- | packages/website/package.json | 4 | ||||
-rw-r--r-- | packages/website/public/index.html | 217 | ||||
-rw-r--r-- | packages/website/ts/components/ui/check_mark.tsx | 31 | ||||
-rw-r--r-- | packages/website/ts/components/ui/container.tsx | 39 | ||||
-rw-r--r-- | packages/website/ts/components/ui/input.tsx | 6 | ||||
-rw-r--r-- | packages/website/ts/components/ui/multi_select.tsx | 66 | ||||
-rw-r--r-- | packages/website/ts/components/ui/select.tsx | 170 | ||||
-rw-r--r-- | packages/website/ts/pages/documentation/developers_page.tsx | 4 | ||||
-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 | 93 | ||||
-rw-r--r-- | packages/website/ts/pages/instant/features.tsx | 44 | ||||
-rw-r--r-- | yarn.lock | 87 |
17 files changed, 1196 insertions, 154 deletions
diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index dae9124c6..bdc531617 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -11,7 +11,14 @@ import { asyncData } from '../redux/async_data'; import { DEFAULT_STATE, DefaultState, State } from '../redux/reducer'; import { store, Store } from '../redux/store'; import { fonts } from '../style/fonts'; -import { AccountState, AffiliateInfo, AssetMetaData, Network, OrderSource, QuoteFetchOrigin } from '../types'; +import { + AccountState, + AffiliateInfo, + AssetMetaData, + Network, + QuoteFetchOrigin, + ZeroExInstantBaseConfig, +} from '../types'; import { analytics, disableAnalytics } from '../util/analytics'; import { assetUtils } from '../util/asset'; import { errorFlasher } from '../util/error_flasher'; @@ -21,24 +28,7 @@ import { Heartbeater } from '../util/heartbeater'; import { generateAccountHeartbeater, generateBuyQuoteHeartbeater } from '../util/heartbeater_factory'; import { providerStateFactory } from '../util/provider_state_factory'; -export type ZeroExInstantProviderProps = ZeroExInstantProviderRequiredProps & - Partial<ZeroExInstantProviderOptionalProps>; - -export interface ZeroExInstantProviderRequiredProps { - orderSource: OrderSource; -} - -export interface ZeroExInstantProviderOptionalProps { - provider: Provider; - walletDisplayName: string; - availableAssetDatas: string[]; - defaultAssetBuyAmount: number; - defaultSelectedAssetData: string; - additionalAssetMetaDataMap: ObjectMap<AssetMetaData>; - networkId: Network; - affiliateInfo: AffiliateInfo; - shouldDisableAnalyticsTracking: boolean; -} +export type ZeroExInstantProviderProps = ZeroExInstantBaseConfig; export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> { private readonly _store: Store; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 2d73ba29e..e65961e95 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -177,3 +177,21 @@ export enum ProviderType { Cipher = 'CIPHER', Fallback = 'FALLBACK', } + +export interface ZeroExInstantRequiredBaseConfig { + orderSource: OrderSource; +} + +export interface ZeroExInstantOptionalBaseConfig { + provider: Provider; + walletDisplayName: string; + availableAssetDatas: string[]; + defaultAssetBuyAmount: number; + defaultSelectedAssetData: string; + additionalAssetMetaDataMap: ObjectMap<AssetMetaData>; + networkId: Network; + affiliateInfo: AffiliateInfo; + shouldDisableAnalyticsTracking: boolean; +} + +export type ZeroExInstantBaseConfig = ZeroExInstantRequiredBaseConfig & Partial<ZeroExInstantOptionalBaseConfig>; diff --git a/packages/website/package.json b/packages/website/package.json index dc10c7b1c..c02e1eee2 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -20,6 +20,8 @@ "author": "Fabio Berger", "license": "Apache-2.0", "dependencies": { + "@0x/asset-buyer": "^3.0.2", + "@0x/contract-addresses": "^2.0.0", "@0x/contract-wrappers": "^4.1.1", "@0x/json-schemas": "^2.1.2", "@0x/order-utils": "^3.0.4", @@ -54,6 +56,7 @@ "react-popper": "^1.0.0-beta.6", "react-redux": "^5.0.3", "react-scroll": "0xproject/react-scroll#pr-330-and-replace-state", + "react-syntax-highlighter": "^10.1.1", "react-tooltip": "^3.2.7", "react-typist": "^2.0.4", "redux": "^3.6.0", @@ -83,6 +86,7 @@ "@types/react-helmet": "^5.0.6", "@types/react-redux": "^4.4.37", "@types/react-scroll": "1.5.3", + "@types/react-syntax-highlighter": "^0.0.8", "@types/react-tap-event-plugin": "0.0.30", "@types/redux": "^3.6.0", "@types/web3-provider-engine": "^14.0.0", diff --git a/packages/website/public/index.html b/packages/website/public/index.html index a8a61f8ad..538eca6d9 100644 --- a/packages/website/public/index.html +++ b/packages/website/public/index.html @@ -1,95 +1,132 @@ <!DOCTYPE html> <html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="description" content="An Open Protocol For Decentralized Exchange On The Ethereum Blockchain" /> + <meta property="og:type" content="website" /> + <meta property="og:title" content="0x" /> + <meta + property="og:description" + content="An Open Protocol For Decentralized Exchange On The Ethereum Blockchain" + /> + <meta property="og:image" content="/images/og_image.png" /> + <title>0x: The Protocol for Trading Tokens</title> + <link rel="icon" type="image/png" href="/images/favicon/favicon-2-32x32.png" sizes="32x32" /> + <link rel="icon" type="image/png" href="/images/favicon/favicon-2-16x16.png" sizes="16x16" /> + <link rel="stylesheet" href="/css/material-design-iconic-font.min.css" /> + <link rel="stylesheet" href="/css/roboto.css" /> + <link rel="stylesheet" href="/css/roboto_mono.css" /> + <link rel="stylesheet" href="/css/basscss_responsive_custom.css" /> + <link rel="stylesheet" href="/css/basscss_responsive_padding.css" /> + <link rel="stylesheet" href="/css/basscss_responsive_margin.css" /> + <link rel="stylesheet" href="/css/basscss_responsive_type_scale.css" /> + </head> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta name="description" content="An Open Protocol For Decentralized Exchange On The Ethereum Blockchain" /> - <meta property="og:type" content="website" /> - <meta property="og:title" content="0x" /> - <meta property="og:description" content="An Open Protocol For Decentralized Exchange On The Ethereum Blockchain" /> - <meta property="og:image" content="/images/og_image.png" /> - <title>0x: The Protocol for Trading Tokens</title> - <link rel="icon" type="image/png" href="/images/favicon/favicon-2-32x32.png" sizes="32x32" /> - <link rel="icon" type="image/png" href="/images/favicon/favicon-2-16x16.png" sizes="16x16" /> - <link rel="stylesheet" href="/css/github-gist.css"> - <link rel="stylesheet" href="/css/material-design-iconic-font.min.css"> - <link rel="stylesheet" href="/css/roboto.css"> - <link rel="stylesheet" href="/css/roboto_mono.css"> - <link rel="stylesheet" href="/css/basscss_responsive_custom.css"> - <link rel="stylesheet" href="/css/basscss_responsive_padding.css"> - <link rel="stylesheet" href="/css/basscss_responsive_margin.css"> - <link rel="stylesheet" href="/css/basscss_responsive_type_scale.css"> -</head> + <body style="margin: 0px; min-width: 355px;"> + <!-- Heap SDK --> + <script type="text/javascript"> + (window.heap = window.heap || []), + (heap.load = function(e, t) { + (window.heap.appid = e), (window.heap.config = t = t || {}); + var r = t.forceSSL || 'https:' === document.location.protocol, + a = document.createElement('script'); + (a.type = 'text/javascript'), + (a.async = !0), + (a.src = (r ? 'https:' : 'http:') + '//cdn.heapanalytics.com/js/heap-' + e + '.js'); + var n = document.getElementsByTagName('script')[0]; + n.parentNode.insertBefore(a, n); + for ( + var o = function(e) { + return function() { + heap.push([e].concat(Array.prototype.slice.call(arguments, 0))); + }; + }, + p = [ + 'addEventProperties', + 'addUserProperties', + 'clearEventProperties', + 'identify', + 'resetIdentity', + 'removeEventProperty', + 'setEventProperties', + 'track', + 'unsetEventProperty', + ], + c = 0; + c < p.length; + c++ + ) + heap[p[c]] = o(p[c]); + }); + heap.load('410099666'); + </script> + <!-- End Heap SDK --> + <!-- Global site tag (gtag.js) - Google Analytics --> + <script async src="https://www.googletagmanager.com/gtag/js?id=UA-98720122-1"></script> + <script> + window.dataLayer = window.dataLayer || []; + function gtag() { + dataLayer.push(arguments); + } + gtag('js', new Date()); -<body style="margin: 0px; min-width: 355px;"> - <!-- Heap SDK --> - <script type="text/javascript"> - window.heap = window.heap || [], heap.load = function (e, t) { window.heap.appid = e, window.heap.config = t = t || {}; var r = t.forceSSL || "https:" === document.location.protocol, a = document.createElement("script"); a.type = "text/javascript", a.async = !0, a.src = (r ? "https:" : "http:") + "//cdn.heapanalytics.com/js/heap-" + e + ".js"; var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(a, n); for (var o = function (e) { return function () { heap.push([e].concat(Array.prototype.slice.call(arguments, 0))) } }, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], c = 0; c < p.length; c++)heap[p[c]] = o(p[c]) }; - heap.load("410099666"); - </script> - <!-- End Heap SDK --> - <!-- Global site tag (gtag.js) - Google Analytics --> - <script async src="https://www.googletagmanager.com/gtag/js?id=UA-98720122-1"></script> - <script> - window.dataLayer = window.dataLayer || []; - function gtag() { - dataLayer.push(arguments); - } - gtag('js', new Date()); + gtag('config', 'UA-98720122-1'); + </script> + <!-- End Google Analytics --> + <!-- Facebook SDK --> + <div id="fb-root"></div> + <script> + (function(d, s, id) { + var js, + fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) return; + js = d.createElement(s); + js.id = id; + js.src = '//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8&appId=1687545238205192'; + fjs.parentNode.insertBefore(js, fjs); + })(document, 'script', 'facebook-jssdk'); + </script> + <div id="app"></div> + <!-- End Facebook SDK --> + <!-- Twitter SDK --> + <script> + window.twttr = (function(d, s, id) { + var js, + fjs = d.getElementsByTagName(s)[0], + t = window.twttr || {}; + if (d.getElementById(id)) return t; + js = d.createElement(s); + js.id = id; + js.src = 'https://platform.twitter.com/widgets.js'; + fjs.parentNode.insertBefore(js, fjs); - gtag('config', 'UA-98720122-1'); - </script> - <!-- End Google Analytics --> - <!-- Facebook SDK --> - <div id="fb-root"></div> - <script> - (function (d, s, id) { - var js, - fjs = d.getElementsByTagName(s)[0]; - if (d.getElementById(id)) return; - js = d.createElement(s); - js.id = id; - js.src = '//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8&appId=1687545238205192'; - fjs.parentNode.insertBefore(js, fjs); - })(document, 'script', 'facebook-jssdk'); - </script> - <div id="app"></div> - <!-- End Facebook SDK --> - <!-- Twitter SDK --> - <script> - window.twttr = (function (d, s, id) { - var js, - fjs = d.getElementsByTagName(s)[0], - t = window.twttr || {}; - if (d.getElementById(id)) return t; - js = d.createElement(s); - js.id = id; - js.src = 'https://platform.twitter.com/widgets.js'; - fjs.parentNode.insertBefore(js, fjs); - - t._e = []; - t.ready = function (f) { - t._e.push(f); - }; - return t; - })(document, 'script', 'twitter-wjs'); - </script> - <!-- End Twitter SDK --> - <!-- Hotjar Tracking Code for https://0xproject.com/ --> - <script> - (function (h, o, t, j, a, r) { - h.hj = h.hj || function () { (h.hj.q = h.hj.q || []).push(arguments) }; - h._hjSettings = { hjid: 935597, hjsv: 6 }; - a = o.getElementsByTagName('head')[0]; - r = o.createElement('script'); r.async = 1; - r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv; - a.appendChild(r); - })(window, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv='); - </script> - <!-- End Hotjar Tracking Code --> - <!-- Main --> - <script type="text/javascript" crossorigin="anonymous" src="/bundle.js" charset="utf-8"></script> -</body> - -</html>
\ No newline at end of file + t._e = []; + t.ready = function(f) { + t._e.push(f); + }; + return t; + })(document, 'script', 'twitter-wjs'); + </script> + <!-- End Twitter SDK --> + <!-- Hotjar Tracking Code for https://0xproject.com/ --> + <script> + (function(h, o, t, j, a, r) { + h.hj = + h.hj || + function() { + (h.hj.q = h.hj.q || []).push(arguments); + }; + h._hjSettings = { hjid: 935597, hjsv: 6 }; + a = o.getElementsByTagName('head')[0]; + r = o.createElement('script'); + r.async = 1; + r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv; + a.appendChild(r); + })(window, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv='); + </script> + <!-- End Hotjar Tracking Code --> + <!-- Main --> + <script type="text/javascript" crossorigin="anonymous" src="/bundle.js" charset="utf-8"></script> + </body> +</html> diff --git a/packages/website/ts/components/ui/check_mark.tsx b/packages/website/ts/components/ui/check_mark.tsx new file mode 100644 index 000000000..86e427c8b --- /dev/null +++ b/packages/website/ts/components/ui/check_mark.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; + +import { colors } from '@0x/react-shared'; + +export interface CheckMarkProps { + color?: string; + isChecked?: boolean; +} + +export const CheckMark: React.StatelessComponent<CheckMarkProps> = ({ color, isChecked }) => ( + <svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle + cx="8.5" + cy="8.5" + r="8.5" + fill={isChecked ? color : 'white'} + stroke={isChecked ? undefined : '#CCCCCC'} + /> + <path + d="M2.5 4.5L1.79289 5.20711L2.5 5.91421L3.20711 5.20711L2.5 4.5ZM-0.707107 2.70711L1.79289 5.20711L3.20711 3.79289L0.707107 1.29289L-0.707107 2.70711ZM3.20711 5.20711L7.70711 0.707107L6.29289 -0.707107L1.79289 3.79289L3.20711 5.20711Z" + transform="translate(5 6.5)" + fill="white" + /> + </svg> +); + +CheckMark.displayName = 'Check'; + +CheckMark.defaultProps = { + color: colors.mediumBlue, +}; diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index 7eab2a50f..4b76ce8be 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -1,11 +1,15 @@ import { TextAlignProperty } from 'csstype'; +import { darken } from 'polished'; import * as React from 'react'; +import { styled } from 'ts/style/theme'; + type StringOrNum = string | number; export type ContainerTag = 'div' | 'span'; export interface ContainerProps { + margin?: string; marginTop?: StringOrNum; marginBottom?: StringOrNum; marginRight?: StringOrNum; @@ -17,10 +21,13 @@ export interface ContainerProps { paddingLeft?: StringOrNum; backgroundColor?: string; background?: string; + border?: string; + borderTop?: string; borderRadius?: StringOrNum; borderBottomLeftRadius?: StringOrNum; borderBottomRightRadius?: StringOrNum; borderBottom?: StringOrNum; + borderColor?: string; maxWidth?: StringOrNum; maxHeight?: StringOrNum; width?: StringOrNum; @@ -42,10 +49,26 @@ export interface ContainerProps { id?: string; onClick?: (event: React.MouseEvent<HTMLElement>) => void; overflowX?: 'scroll' | 'hidden' | 'auto' | 'visible'; + overflowY?: 'scroll' | 'hidden' | 'auto' | 'visible'; + shouldDarkenOnHover?: boolean; + hasBoxShadow?: boolean; + shouldAddBoxShadowOnHover?: boolean; } -export const Container: React.StatelessComponent<ContainerProps> = props => { - const { children, className, Tag, isHidden, id, onClick, ...style } = props; +export const PlainContainer: React.StatelessComponent<ContainerProps> = props => { + const { + children, + className, + Tag, + isHidden, + id, + onClick, + shouldDarkenOnHover, + shouldAddBoxShadowOnHover, + hasBoxShadow, + // tslint:disable-next-line:trailing-comma + ...style + } = props; const visibility = isHidden ? 'hidden' : undefined; return ( <Tag id={id} style={{ ...style, visibility }} className={className} onClick={onClick}> @@ -54,6 +77,18 @@ export const Container: React.StatelessComponent<ContainerProps> = props => { ); }; +export const Container = styled(PlainContainer)` + box-sizing: border-box; + ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; + &:hover { + ${props => + props.shouldDarkenOnHover + ? `background-color: ${props.backgroundColor ? darken(0.05, props.backgroundColor) : 'none'} !important` + : ''}; + ${props => (props.shouldAddBoxShadowOnHover ? 'box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)' : '')}; + } +`; + Container.defaultProps = { Tag: 'div', }; diff --git a/packages/website/ts/components/ui/input.tsx b/packages/website/ts/components/ui/input.tsx index e5f4f6c70..1f56c814f 100644 --- a/packages/website/ts/components/ui/input.tsx +++ b/packages/website/ts/components/ui/input.tsx @@ -8,6 +8,7 @@ export interface InputProps { width?: string; fontSize?: string; fontColor?: string; + border?: string; placeholderColor?: string; placeholder?: string; backgroundColor?: string; @@ -23,9 +24,11 @@ export const Input = styled(PlainInput)` width: ${props => props.width}; padding: 0.8em 1.2em; border-radius: 3px; + box-sizing: border-box; font-family: 'Roboto Mono'; color: ${props => props.fontColor}; - border: none; + border: ${props => props.border}; + outline: none; background-color: ${props => props.backgroundColor}; &::placeholder { color: ${props => props.placeholderColor}; @@ -38,6 +41,7 @@ Input.defaultProps = { fontColor: colors.darkestGrey, placeholderColor: colors.darkGrey, fontSize: '12px', + border: 'none', }; Input.displayName = 'Input'; diff --git a/packages/website/ts/components/ui/multi_select.tsx b/packages/website/ts/components/ui/multi_select.tsx new file mode 100644 index 000000000..2cf44cae1 --- /dev/null +++ b/packages/website/ts/components/ui/multi_select.tsx @@ -0,0 +1,66 @@ +import { colors } from '@0x/react-shared'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Container } from './container'; + +export interface MultiSelectItemConfig { + value: string; + renderItemContent: (isSelected: boolean) => React.ReactNode; + onClick?: () => void; +} + +export interface MultiSelectProps { + selectedValues?: string[]; + items: MultiSelectItemConfig[]; + backgroundColor?: string; + height?: string; +} + +export class MultiSelect extends React.Component<MultiSelectProps> { + public static defaultProps = { + backgroundColor: colors.white, + }; + public render(): React.ReactNode { + const { items, backgroundColor, selectedValues, height } = this.props; + return ( + <Container + backgroundColor={backgroundColor} + borderRadius="4px" + width="100%" + height={height} + overflowY="scroll" + > + {_.map(items, item => ( + <MultiSelectItem + key={item.value} + renderItemContent={item.renderItemContent} + backgroundColor={backgroundColor} + onClick={item.onClick} + isSelected={_.isUndefined(selectedValues) || _.includes(selectedValues, item.value)} + /> + ))} + </Container> + ); + } +} + +export interface MultiSelectItemProps { + renderItemContent: (isSelected: boolean) => React.ReactNode; + isSelected?: boolean; + onClick?: () => void; + backgroundColor?: string; +} + +export const MultiSelectItem: React.StatelessComponent<MultiSelectItemProps> = ({ + renderItemContent, + isSelected, + onClick, + backgroundColor, +}) => ( + <Container cursor="pointer" shouldDarkenOnHover={true} onClick={onClick} backgroundColor={backgroundColor}> + <Container borderBottom={`1px solid ${colors.lightestGrey}`} margin="0px 15px" padding="10px 0px"> + {renderItemContent(isSelected)} + </Container> + </Container> +); diff --git a/packages/website/ts/components/ui/select.tsx b/packages/website/ts/components/ui/select.tsx new file mode 100644 index 000000000..743b082b0 --- /dev/null +++ b/packages/website/ts/components/ui/select.tsx @@ -0,0 +1,170 @@ +import { colors } from '@0x/react-shared'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { zIndex } from 'ts/style/z_index'; + +import { Container } from './container'; +import { Overlay } from './overlay'; +import { Text } from './text'; + +export interface SelectItemConfig { + text: string; + onClick?: () => void; +} + +export interface SelectProps { + value: string; + label?: string; + items: SelectItemConfig[]; + onOpen?: () => void; + border?: string; + fontSize?: string; + iconSize?: number; + textColor?: string; + labelColor?: string; + backgroundColor?: string; +} + +export interface SelectState { + isOpen: boolean; +} + +export class Select extends React.Component<SelectProps, SelectState> { + public static defaultProps = { + items: [] as SelectItemConfig[], + textColor: colors.black, + backgroundColor: colors.white, + fontSize: '16px', + iconSize: 25, + }; + public state: SelectState = { + isOpen: false, + }; + public render(): React.ReactNode { + const { value, label, items, border, textColor, labelColor, backgroundColor, fontSize, iconSize } = this.props; + const { isOpen } = this.state; + const hasItems = !_.isEmpty(items); + const borderRadius = isOpen ? '4px 4px 0px 0px' : '4px'; + return ( + <React.Fragment> + {isOpen && ( + <Overlay + style={{ + zIndex: zIndex.overlay, + backgroundColor: 'rgba(255, 255, 255, 0)', + }} + onClick={this._closeDropdown} + /> + )} + <Container position="relative"> + <Container + cursor={hasItems ? 'pointer' : undefined} + onClick={this._handleDropdownClick} + borderRadius={borderRadius} + hasBoxShadow={isOpen} + border={border} + backgroundColor={backgroundColor} + padding="0.8em" + width="100%" + > + <Container className="flex justify-between"> + <Text fontSize={fontSize} fontColor={textColor}> + {value} + </Text> + <Container> + {label && ( + <Text fontSize={fontSize} fontColor={labelColor}> + {label} + </Text> + )} + {hasItems && ( + <Container marginLeft="5px" display="inline-block" position="relative" bottom="2px"> + <i + className="zmdi zmdi-chevron-down" + style={{ fontSize: iconSize, color: colors.darkGrey }} + /> + </Container> + )} + </Container> + </Container> + </Container> + {isOpen && ( + <Container + width="100%" + position="absolute" + onClick={this._closeDropdown} + zIndex={zIndex.aboveOverlay} + hasBoxShadow={true} + > + {_.map(items, (item, index) => ( + <SelectItem + key={item.text} + {...item} + isLast={index === items.length - 1} + backgroundColor={backgroundColor} + textColor={textColor} + border={border} + /> + ))} + </Container> + )} + </Container> + </React.Fragment> + ); + } + private readonly _handleDropdownClick = (): void => { + if (_.isEmpty(this.props.items)) { + return; + } + const isOpen = !this.state.isOpen; + this.setState({ + isOpen, + }); + + if (isOpen && this.props.onOpen) { + this.props.onOpen(); + } + }; + private readonly _closeDropdown = (): void => { + this.setState({ + isOpen: false, + }); + }; +} + +export interface SelectItemProps extends SelectItemConfig { + text: string; + onClick?: () => void; + isLast: boolean; + backgroundColor?: string; + border?: string; + textColor?: string; + fontSize?: string; +} + +export const SelectItem: React.StatelessComponent<SelectItemProps> = ({ + text, + onClick, + isLast, + border, + backgroundColor, + textColor, + fontSize, +}) => ( + <Container + onClick={onClick} + cursor="pointer" + backgroundColor={backgroundColor} + padding="0.8em" + borderTop="0" + border={border} + shouldDarkenOnHover={true} + borderRadius={isLast ? '0px 0px 4px 4px' : undefined} + width="100%" + > + <Text fontSize={fontSize} fontColor={textColor}> + {text} + </Text> + </Container> +); diff --git a/packages/website/ts/pages/documentation/developers_page.tsx b/packages/website/ts/pages/documentation/developers_page.tsx index a84be7bfe..fcca2b6ad 100644 --- a/packages/website/ts/pages/documentation/developers_page.tsx +++ b/packages/website/ts/pages/documentation/developers_page.tsx @@ -2,6 +2,7 @@ import { colors, constants as sharedConstants, utils as sharedUtils } from '@0x/ import * as _ from 'lodash'; import * as React from 'react'; import DocumentTitle from 'react-document-title'; +import { Helmet } from 'react-helmet'; import { DocsLogo } from 'ts/components/documentation/docs_logo'; import { DocsTopBar } from 'ts/components/documentation/docs_top_bar'; import { Container } from 'ts/components/ui/container'; @@ -146,6 +147,9 @@ export class DevelopersPage extends React.Component<DevelopersPageProps, Develop } 50%, ${colors.white} 100%)`} > <DocumentTitle title="0x Docs" /> + <Helmet> + <link rel="stylesheet" href="/css/github-gist.css" /> + </Helmet> <Container className="flex mx-auto" height="100vh"> <Container className="sm-hide xs-hide relative" 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 index c836739bb..5700bdc1d 100644 --- a/packages/website/ts/pages/instant/configurator.tsx +++ b/packages/website/ts/pages/instant/configurator.tsx @@ -1,12 +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 const Configurator = (props: ConfiguratorProps) => ( - <Container id={props.hash} height="400px" backgroundColor={colors.instantTertiaryBackground} /> -); +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 index 9c1668c1c..230a8496b 100644 --- a/packages/website/ts/pages/instant/features.tsx +++ b/packages/website/ts/pages/instant/features.tsx @@ -4,6 +4,7 @@ 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'; @@ -61,17 +62,11 @@ export const Features = (props: FeatureProps) => { ); }; -interface LinkInfo { - displayText: string; - linkSrc?: string; - onClick?: () => void; -} - interface FeatureItemProps { imgSrc: string; title: string; description: string; - linkInfos: LinkInfo[]; + linkInfos: ActionLinkProps[]; screenWidth: ScreenWidths; } @@ -95,36 +90,11 @@ const FeatureItem = (props: FeatureItemProps) => { </Text> </Container> <Container className="flex" marginTop="28px"> - {_.map(linkInfos, linkInfo => { - const onClick = (event: React.MouseEvent<HTMLElement>) => { - if (!_.isUndefined(linkInfo.onClick)) { - linkInfo.onClick(); - } else if (!_.isUndefined(linkInfo.linkSrc)) { - utils.openUrl(linkInfo.linkSrc); - } - }; - return ( - <Container - key={linkInfo.linkSrc} - className="flex items-center" - marginRight="32px" - onClick={onClick} - cursor="pointer" - > - <Container> - <Text fontSize="16px" fontColor={colors.white}> - {linkInfo.displayText} - </Text> - </Container> - <Container paddingTop="1px" paddingLeft="6px"> - <i - className="zmdi zmdi-chevron-right bold" - style={{ fontSize: 16, color: colors.white }} - /> - </Container> - </Container> - ); - })} + {_.map(linkInfos, linkInfo => ( + <Container key={linkInfo.displayText} marginRight="32px"> + <ActionLink {...linkInfo} /> + </Container> + ))} </Container> </Container> ); @@ -1531,6 +1531,12 @@ dependencies: "@types/react" "*" +"@types/react-syntax-highlighter@^0.0.8": + version "0.0.8" + resolved "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-0.0.8.tgz#ed44e2ead992c513327bcf2ede5eda7daa981421" + dependencies: + "@types/react" "*" + "@types/react-tap-event-plugin@0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/react-tap-event-plugin/-/react-tap-event-plugin-0.0.30.tgz#123f35080412f489b6770c5a65c159ff96654cb5" @@ -4090,6 +4096,12 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^1.0.0: + version "1.0.5" + resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.5.tgz#b13793131d9ea2d2431cf5b507ddec258f0ce0db" + dependencies: + trim "0.0.1" + commander@2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -6367,6 +6379,12 @@ fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" +fault@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa" + dependencies: + format "^0.2.2" + faye-websocket@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" @@ -6665,6 +6683,10 @@ format-util@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/format-util/-/format-util-1.0.3.tgz#032dca4a116262a12c43f4c3ec8566416c5b2d95" +format@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -7516,6 +7538,19 @@ hash.js@1.1.3, hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" +hast-util-parse-selector@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.1.tgz#4ddbae1ae12c124e3eb91b581d2556441766f0ab" + +hastscript@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-5.0.0.tgz#fee10382c1bc4ba3f1be311521d368c047d2c43a" + dependencies: + comma-separated-tokens "^1.0.0" + hast-util-parse-selector "^2.2.0" + property-information "^5.0.1" + space-separated-tokens "^1.0.0" + hawk@3.1.3, hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -7564,7 +7599,7 @@ heap@~0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" -highlight.js@^9.0.0, highlight.js@^9.11.0: +highlight.js@^9.0.0, highlight.js@^9.11.0, highlight.js@~9.12.0: version "9.12.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" @@ -9925,6 +9960,13 @@ lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" +lowlight@~1.9.1: + version "1.9.2" + resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.9.2.tgz#0b9127e3cec2c3021b7795dd81005c709a42fdd1" + dependencies: + fault "^1.0.2" + highlight.js "~9.12.0" + lru-cache@2: version "2.7.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" @@ -11497,6 +11539,17 @@ parse-entities@^1.1.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +parse-entities@^1.1.2: + version "1.2.0" + resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.0.tgz#9deac087661b2e36814153cb78d7e54a4c5fd6f4" + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-filepath@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" @@ -12093,7 +12146,7 @@ pretty-hrtime@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" -prismjs@^1.15.0: +prismjs@^1.15.0, prismjs@^1.8.4, prismjs@~1.15.0: version "1.15.0" resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.15.0.tgz#8801d332e472091ba8def94976c8877ad60398d9" optionalDependencies: @@ -12193,6 +12246,12 @@ prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, loose-envify "^1.3.1" object-assign "^4.1.1" +property-information@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/property-information/-/property-information-5.0.1.tgz#c3b09f4f5750b1634c0b24205adbf78f18bdf94f" + dependencies: + xtend "^4.0.1" + proto-list@~1.2.1: version "1.2.4" resolved "http://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -12656,6 +12715,16 @@ react-side-effect@^1.0.2, react-side-effect@^1.1.0: exenv "^1.2.1" shallowequal "^1.0.1" +react-syntax-highlighter@^10.1.1: + version "10.1.1" + resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-10.1.1.tgz#1bf7ad4f2f16d2978b04594407b670671b4d3316" + dependencies: + babel-runtime "^6.18.0" + highlight.js "~9.12.0" + lowlight "~1.9.1" + prismjs "^1.8.4" + refractor "^2.4.1" + react-tabs@^2.0.0: version "2.2.2" resolved "https://registry.npmjs.org/react-tabs/-/react-tabs-2.2.2.tgz#2f2935da379889484751d1df47c1b639e5ee835d" @@ -12981,6 +13050,14 @@ redux@^3.6.0: loose-envify "^1.1.0" symbol-observable "^1.0.3" +refractor@^2.4.1: + version "2.6.2" + resolved "https://registry.npmjs.org/refractor/-/refractor-2.6.2.tgz#8e0877ab8864165275aafeea5d9c8eebe871552f" + dependencies: + hastscript "^5.0.0" + parse-entities "^1.1.2" + prismjs "~1.15.0" + regenerate@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" @@ -14203,6 +14280,12 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" +space-separated-tokens@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412" + dependencies: + trim "0.0.1" + sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" |