diff options
Diffstat (limited to 'packages/website/ts/pages')
25 files changed, 3995 insertions, 0 deletions
diff --git a/packages/website/ts/pages/about/about.tsx b/packages/website/ts/pages/about/about.tsx new file mode 100644 index 000000000..8859fb00a --- /dev/null +++ b/packages/website/ts/pages/about/about.tsx @@ -0,0 +1,253 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as DocumentTitle from 'react-document-title'; +import RaisedButton from 'material-ui/RaisedButton'; +import {colors} from 'material-ui/styles'; +import {Styles, ProfileInfo} from 'ts/types'; +import {utils} from 'ts/utils/utils'; +import {Link} from 'react-router-dom'; +import {Footer} from 'ts/components/footer'; +import {TopBar} from 'ts/components/top_bar'; +import {Question} from 'ts/pages/faq/question'; +import {configs} from 'ts/utils/configs'; +import {constants} from 'ts/utils/constants'; +import {Profile} from 'ts/pages/about/profile'; + +const CUSTOM_BACKGROUND_COLOR = '#F0F0F0'; +const CUSTOM_GRAY = '#4C4C4C'; +const CUSTOM_LIGHT_GRAY = '#A2A2A2'; + +const teamRow1: ProfileInfo[] = [ + { + name: 'Will Warren', + title: 'Co-founder & CEO', + description: `Smart contract R&D. Previously applied physics at Los Alamos \ + Nat Lab. Mechanical engineering at UC San Diego. PhD dropout.`, + image: '/images/team/will.jpg', + linkedIn: 'https://www.linkedin.com/in/will-warren-92aab62b/', + github: 'https://github.com/willwarren89', + medium: 'https://medium.com/@willwarren89', + }, + { + name: 'Amir Bandeali', + title: 'Co-founder & CTO', + description: `Smart contract R&D. Previously fixed income trader at DRW. \ + Finance at University of Illinois, Urbana-Champaign.`, + image: '/images/team/amir.jpeg', + linkedIn: 'https://www.linkedin.com/in/abandeali1/', + github: 'https://github.com/abandeali1', + medium: 'https://medium.com/@abandeali1', + }, + { + name: 'Fabio Berger', + title: 'Senior Engineer', + description: `Full-stack blockchain engineer. Previously software engineer \ + at Airtable and founder of WealthLift. Computer science at Duke.`, + image: '/images/team/fabio.jpg', + linkedIn: 'https://www.linkedin.com/in/fabio-berger-03ab261a/', + github: 'https://github.com/fabioberger', + medium: 'https://medium.com/@fabioberger', + }, + { + name: 'Alex Xu', + title: 'Director of Operations', + description: `Strategy and operations. Previously digital marketing at Google \ + and vendor management at Amazon. Economics at UC San Diego.`, + image: '/images/team/alex.jpg', + linkedIn: 'https://www.linkedin.com/in/alex-xu/', + github: '', + medium: '', + }, +]; + +const teamRow2: ProfileInfo[] = [ + { + name: 'Leonid Logvinov', + title: 'Engineer', + description: `Full-stack blockchain engineer. Previously blockchain engineer \ + at Neufund. Computer science at University of Warsaw.`, + image: '/images/team/leonid.png', + linkedIn: 'https://www.linkedin.com/in/leonidlogvinov/', + github: 'https://github.com/LogvinovLeon', + medium: '', + }, + { + name: 'Ben Burns', + title: 'Designer', + description: `Product, motion, and graphic designer. Previously designer \ + at Airtable and Apple. Digital Design at University of Cincinnati.`, + image: '/images/team/ben.jpg', + linkedIn: 'https://www.linkedin.com/in/ben-burns-30170478/', + github: '', + medium: '', + }, + { + name: 'Philippe Castonguay', + title: 'Dev Relations Manager', + description: `Developer relations. Previously computational neuroscience \ + research at Janelia. Statistics at Western University. MA Dropout.`, + image: '/images/team/philippe.png', + linkedIn: '', + github: 'https://github.com/PhABC', + medium: '', + }, + { + name: 'Brandon Millman', + title: 'Senior Engineer', + description: `Full-stack engineer. Previously senior software engineer at \ + Twitter. Electrical and Computer Engineering at Duke.`, + image: '/images/team/brandon.png', + linkedIn: 'https://www.linkedin.com/company-beta/17942619/', + }, +]; + +const advisors: ProfileInfo[] = [ + { + name: 'Fred Ehrsam', + description: 'Co-founder of Coinbase. Previously FX trader at Goldman Sachs.', + image: '/images/advisors/fred.jpg', + linkedIn: 'https://www.linkedin.com/in/fredehrsam/', + medium: 'https://medium.com/@FEhrsam', + twitter: 'https://twitter.com/FEhrsam', + }, + { + name: 'Olaf Carlson-Wee', + image: '/images/advisors/olaf.png', + description: 'Founder of Polychain Capital. First hire at Coinbase. Angel investor.', + linkedIn: 'https://www.linkedin.com/in/olafcw/', + angellist: 'https://angel.co/olafcw', + }, + { + name: 'Joey Krug', + description: `Co-CIO at Pantera Capital. Founder of Augur. Thiel 20 Under 20 Fellow.`, + image: '/images/advisors/joey.jpg', + linkedIn: 'https://www.linkedin.com/in/joeykrug/', + github: 'https://github.com/joeykrug', + angellist: 'https://angel.co/joeykrug', + }, + { + name: 'Linda Xie', + description: 'Co-founder of Scalar Capital. Previously PM at Coinbase.', + image: '/images/advisors/linda.jpg', + linkedIn: 'https://www.linkedin.com/in/lindaxie/', + medium: 'https://medium.com/@linda.xie', + twitter: 'https://twitter.com/ljxie', + }, +]; + +export interface AboutProps { + source: string; + location: Location; +} + +interface AboutState {} + +const styles: Styles = { + header: { + fontFamily: 'Roboto Mono', + fontSize: 36, + color: 'black', + paddingTop: 110, + }, +}; + +export class About extends React.Component<AboutProps, AboutState> { + public componentDidMount() { + window.scrollTo(0, 0); + } + public render() { + return ( + <div style={{backgroundColor: CUSTOM_BACKGROUND_COLOR}}> + <DocumentTitle title="0x About Us"/> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + style={{backgroundColor: CUSTOM_BACKGROUND_COLOR}} + /> + <div + id="about" + className="mx-auto max-width-4 py4" + style={{color: colors.grey800}} + > + <div + className="mx-auto pb4 sm-px3" + style={{maxWidth: 435}} + > + <div + style={styles.header} + > + About us: + </div> + <div + className="pt3" + style={{fontSize: 17, color: CUSTOM_GRAY, lineHeight: 1.5}} + > + Our team is a diverse and globally distributed group with backgrounds + in engineering, research, business and design. We are passionate about + decentralized technology and its potential to act as an equalizing force + in the world. + </div> + </div> + <div className="pt3 md-px4 lg-px0"> + <div className="clearfix pb3"> + {this.renderProfiles(teamRow1)} + </div> + <div className="clearfix"> + {this.renderProfiles(teamRow2)} + </div> + </div> + <div className="pt3 pb2"> + <div + className="pt2 pb3 sm-center md-pl4 lg-pl0 md-ml3" + style={{color: CUSTOM_LIGHT_GRAY, fontSize: 24, fontFamily: 'Roboto Mono'}} + > + Advisors: + </div> + <div className="clearfix"> + {this.renderProfiles(advisors)} + </div> + </div> + <div className="mx-auto py4 sm-px3" style={{maxWidth: 308}}> + <div + className="pb2" + style={{fontSize: 30, color: CUSTOM_GRAY, fontFamily: 'Roboto Mono', letterSpacing: 7.5}} + > + WE'RE HIRING + </div> + <div + className="pb4 mb4" + style={{fontSize: 16, color: CUSTOM_GRAY, lineHeight: 1.5, letterSpacing: '0.5px'}} + > + We are seeking outstanding candidates to{' '} + <a + href={constants.ANGELLIST_URL} + target="_blank" + style={{color: 'black'}} + > + join our team + </a> + . We value passion, diversity and unique perspectives. + </div> + </div> + </div> + <Footer location={this.props.location} /> + </div> + ); + } + private renderProfiles(profiles: ProfileInfo[]) { + const numIndiv = profiles.length; + const colSize = utils.getColSize(profiles.length); + return _.map(profiles, profile => { + return ( + <div + key={`profile-${profile.name}`} + > + <Profile + colSize={colSize} + profileInfo={profile} + /> + </div> + ); + }); + } +} diff --git a/packages/website/ts/pages/about/profile.tsx b/packages/website/ts/pages/about/profile.tsx new file mode 100644 index 000000000..6c48a8553 --- /dev/null +++ b/packages/website/ts/pages/about/profile.tsx @@ -0,0 +1,99 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {colors} from 'material-ui/styles'; +import {utils} from 'ts/utils/utils'; +import {Element as ScrollElement} from 'react-scroll'; +import {Styles, ProfileInfo} from 'ts/types'; + +const IMAGE_DIMENSION = 149; +const styles: Styles = { + subheader: { + textTransform: 'uppercase', + fontSize: 32, + margin: 0, + }, + imageContainer: { + width: IMAGE_DIMENSION, + height: IMAGE_DIMENSION, + boxShadow: 'rgba(0, 0, 0, 0.19) 2px 5px 10px', + }, +}; + +interface ProfileProps { + colSize: number; + profileInfo: ProfileInfo; +} + +export function Profile(props: ProfileProps) { + return ( + <div + className={`lg-col md-col lg-col-${props.colSize} md-col-6`} + > + <div + style={{maxWidth: 300}} + className="mx-auto lg-px3 md-px3 sm-px4 sm-pb3" + > + <div + className="circle overflow-hidden mx-auto" + style={styles.imageContainer} + > + <img + width={IMAGE_DIMENSION} + src={props.profileInfo.image} + /> + </div> + <div + className="center" + style={{fontSize: 18, fontWeight: 'bold', paddingTop: 20}} + > + {props.profileInfo.name} + </div> + {!_.isUndefined(props.profileInfo.title) && + <div + className="pt1 center" + style={{fontSize: 14, fontFamily: 'Roboto Mono', color: '#818181'}} + > + {props.profileInfo.title.toUpperCase()} + </div> + } + <div + style={{minHeight: 60, lineHeight: 1.4}} + className="pt1 pb2 mx-auto lg-h6 md-h6 sm-h5 sm-center" + > + {props.profileInfo.description} + </div> + <div className="flex pb3 mx-auto sm-hide xs-hide" style={{width: 180, opacity: 0.5}}> + {renderSocialMediaIcons(props.profileInfo)} + </div> + </div> + </div> + ); +} + +function renderSocialMediaIcons(profileInfo: ProfileInfo) { + const icons = [ + renderSocialMediaIcon('zmdi-github-box', profileInfo.github), + renderSocialMediaIcon('zmdi-linkedin-box', profileInfo.linkedIn), + renderSocialMediaIcon('zmdi-twitter-box', profileInfo.twitter), + ]; + return icons; +} + +function renderSocialMediaIcon(iconName: string, url: string) { + if (_.isEmpty(url)) { + return null; + } + + return ( + <div key={url} className="pr1"> + <a + href={url} + style={{color: 'inherit'}} + target="_blank" + className="text-decoration-none" + > + <i className={`zmdi ${iconName}`} style={{...styles.socalIcon}} /> + </a> + </div> + ); +} diff --git a/packages/website/ts/pages/documentation/comment.tsx b/packages/website/ts/pages/documentation/comment.tsx new file mode 100644 index 000000000..78bbdc069 --- /dev/null +++ b/packages/website/ts/pages/documentation/comment.tsx @@ -0,0 +1,24 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as ReactMarkdown from 'react-markdown'; +import {MarkdownCodeBlock} from 'ts/pages/shared/markdown_code_block'; + +interface CommentProps { + comment: string; + className?: string; +} + +const defaultProps = { + className: '', +}; + +export const Comment: React.SFC<CommentProps> = (props: CommentProps) => { + return ( + <div className={`${props.className} comment`}> + <ReactMarkdown + source={props.comment} + renderers={{CodeBlock: MarkdownCodeBlock}} + /> + </div> + ); +}; diff --git a/packages/website/ts/pages/documentation/custom_enum.tsx b/packages/website/ts/pages/documentation/custom_enum.tsx new file mode 100644 index 000000000..aca8af832 --- /dev/null +++ b/packages/website/ts/pages/documentation/custom_enum.tsx @@ -0,0 +1,31 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {utils} from 'ts/utils/utils'; +import {CustomType} from 'ts/types'; + +const STRING_ENUM_CODE_PREFIX = ' strEnum('; + +interface CustomEnumProps { + type: CustomType; +} + +// This component renders custom string enums that was a work-around for versions of +// TypeScript <2.4.0 that did not support them natively. We keep it around to support +// older versions of 0x.js <0.9.0 +export function CustomEnum(props: CustomEnumProps) { + const type = props.type; + if (!_.startsWith(type.defaultValue, STRING_ENUM_CODE_PREFIX)) { + utils.consoleLog('We do not yet support `Variable` types that are not strEnums'); + return null; + } + // Remove the prefix and postfix, leaving only the strEnum values without quotes. + const enumValues = type.defaultValue.slice(10, -3).replace(/'/g, ''); + return ( + <span> + {`{`} + {'\t'}{enumValues} + <br /> + {`}`} + </span> + ); +} diff --git a/packages/website/ts/pages/documentation/enum.tsx b/packages/website/ts/pages/documentation/enum.tsx new file mode 100644 index 000000000..9364a5d31 --- /dev/null +++ b/packages/website/ts/pages/documentation/enum.tsx @@ -0,0 +1,26 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {utils} from 'ts/utils/utils'; +import {TypeDocNode, EnumValue} from 'ts/types'; + +const STRING_ENUM_CODE_PREFIX = ' strEnum('; + +interface EnumProps { + values: EnumValue[]; +} + +export function Enum(props: EnumProps) { + const values = _.map(props.values, (value, i) => { + const isLast = i === props.values.length - 1; + const defaultValueIfAny = !_.isUndefined(value.defaultValue) ? ` = ${value.defaultValue}` : ''; + return `\n\t${value.name}${defaultValueIfAny},`; + }); + return ( + <span> + {`{`} + {values} + <br /> + {`}`} + </span> + ); +} diff --git a/packages/website/ts/pages/documentation/event_definition.tsx b/packages/website/ts/pages/documentation/event_definition.tsx new file mode 100644 index 000000000..58271e98f --- /dev/null +++ b/packages/website/ts/pages/documentation/event_definition.tsx @@ -0,0 +1,80 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {constants} from 'ts/utils/constants'; +import {utils} from 'ts/utils/utils'; +import {Event, EventArg, HeaderSizes} from 'ts/types'; +import {Type} from 'ts/pages/documentation/type'; +import {AnchorTitle} from 'ts/pages/shared/anchor_title'; + +const KEYWORD_COLOR = '#a81ca6'; +const CUSTOM_GREEN = 'rgb(77, 162, 75)'; + +interface EventDefinitionProps { + event: Event; +} + +interface EventDefinitionState { + shouldShowAnchor: boolean; +} + +export class EventDefinition extends React.Component<EventDefinitionProps, EventDefinitionState> { + constructor(props: EventDefinitionProps) { + super(props); + this.state = { + shouldShowAnchor: false, + }; + } + public render() { + const event = this.props.event; + return ( + <div + id={event.name} + className="pb2" + style={{overflow: 'hidden', width: '100%'}} + onMouseOver={this.setAnchorVisibility.bind(this, true)} + onMouseOut={this.setAnchorVisibility.bind(this, false)} + > + <AnchorTitle + headerSize={HeaderSizes.H3} + title={`Event ${event.name}`} + id={event.name} + shouldShowAnchor={this.state.shouldShowAnchor} + /> + <div style={{fontSize: 16}}> + <pre> + <code className="hljs"> + {this.renderEventCode()} + </code> + </pre> + </div> + </div> + ); + } + private renderEventCode() { + const indexed = <span style={{color: CUSTOM_GREEN}}> indexed</span>; + const eventArgs = _.map(this.props.event.eventArgs, (eventArg: EventArg) => { + return ( + <span key={`eventArg-${eventArg.name}`}> + {eventArg.name}{eventArg.isIndexed ? indexed : ''}: <Type type={eventArg.type} />, + </span> + ); + }); + const argList = _.reduce(eventArgs, (prev: React.ReactNode, curr: React.ReactNode) => { + return [prev, '\n\t', curr]; + }); + return ( + <span> + {`{`} + <br /> + {'\t'}{argList} + <br /> + {`}`} + </span> + ); + } + private setAnchorVisibility(shouldShowAnchor: boolean) { + this.setState({ + shouldShowAnchor, + }); + } +} diff --git a/packages/website/ts/pages/documentation/interface.tsx b/packages/website/ts/pages/documentation/interface.tsx new file mode 100644 index 000000000..9e40b8901 --- /dev/null +++ b/packages/website/ts/pages/documentation/interface.tsx @@ -0,0 +1,54 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {CustomType, TypeDocTypes} from 'ts/types'; +import {Type} from 'ts/pages/documentation/type'; +import {MethodSignature} from 'ts/pages/documentation/method_signature'; + +interface InterfaceProps { + type: CustomType; +} + +export function Interface(props: InterfaceProps) { + const type = props.type; + const properties = _.map(type.children, property => { + return ( + <span key={`property-${property.name}-${property.type}-${type.name}`}> + {property.name}:{' '} + {property.type.typeDocType !== TypeDocTypes.Reflection ? + <Type type={property.type} /> : + <MethodSignature + method={property.type.method} + shouldHideMethodName={true} + shouldUseArrowSyntax={true} + /> + }, + </span> + ); + }); + const hasIndexSignature = !_.isUndefined(type.indexSignature); + if (hasIndexSignature) { + const is = type.indexSignature; + const param = ( + <span key={`indexSigParams-${is.keyName}-${is.keyType}-${type.name}`}> + {is.keyName}: <Type type={is.keyType} /> + </span> + ); + properties.push(( + <span key={`indexSignature-${type.name}-${is.keyType.name}`}> + [{param}]: {is.valueName}, + </span> + )); + } + const propertyList = _.reduce(properties, (prev: React.ReactNode, curr: React.ReactNode) => { + return [prev, '\n\t', curr]; + }); + return ( + <span> + {`{`} + <br /> + {'\t'}{propertyList} + <br /> + {`}`} + </span> + ); +} diff --git a/packages/website/ts/pages/documentation/method_block.tsx b/packages/website/ts/pages/documentation/method_block.tsx new file mode 100644 index 000000000..6fead2f47 --- /dev/null +++ b/packages/website/ts/pages/documentation/method_block.tsx @@ -0,0 +1,174 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as ReactMarkdown from 'react-markdown'; +import {Chip} from 'material-ui/Chip'; +import {colors} from 'material-ui/styles'; +import { + TypeDocNode, + Styles, + TypeDefinitionByName, + TypescriptMethod, + SolidityMethod, + Parameter, + HeaderSizes, +} from 'ts/types'; +import {utils} from 'ts/utils/utils'; +import {SourceLink} from 'ts/pages/documentation/source_link'; +import {MethodSignature} from 'ts/pages/documentation/method_signature'; +import {AnchorTitle} from 'ts/pages/shared/anchor_title'; +import {Comment} from 'ts/pages/documentation/comment'; +import {typeDocUtils} from 'ts/utils/typedoc_utils'; + +interface MethodBlockProps { + method: SolidityMethod|TypescriptMethod; + libraryVersion: string; + typeDefinitionByName: TypeDefinitionByName; +} + +interface MethodBlockState { + shouldShowAnchor: boolean; +} + +const styles: Styles = { + chip: { + fontSize: 13, + backgroundColor: colors.lightBlueA700, + color: 'white', + height: 11, + borderRadius: 14, + marginTop: 19, + lineHeight: 0.8, + }, +}; + +export class MethodBlock extends React.Component<MethodBlockProps, MethodBlockState> { + constructor(props: MethodBlockProps) { + super(props); + this.state = { + shouldShowAnchor: false, + }; + } + public render() { + const method = this.props.method; + if (typeDocUtils.isPrivateOrProtectedProperty(method.name)) { + return null; + } + + return ( + <div + id={method.name} + style={{overflow: 'hidden', width: '100%'}} + className="pb4" + onMouseOver={this.setAnchorVisibility.bind(this, true)} + onMouseOut={this.setAnchorVisibility.bind(this, false)} + > + {!method.isConstructor && + <div className="flex"> + {(method as TypescriptMethod).isStatic && + this.renderChip('Static') + } + {(method as SolidityMethod).isConstant && + this.renderChip('Constant') + } + {(method as SolidityMethod).isPayable && + this.renderChip('Payable') + } + <AnchorTitle + headerSize={HeaderSizes.H3} + title={method.name} + id={method.name} + shouldShowAnchor={this.state.shouldShowAnchor} + /> + </div> + } + <code className="hljs"> + <MethodSignature + method={method} + typeDefinitionByName={this.props.typeDefinitionByName} + /> + </code> + {(method as TypescriptMethod).source && + <SourceLink + version={this.props.libraryVersion} + source={(method as TypescriptMethod).source} + /> + } + {method.comment && + <Comment + comment={method.comment} + className="py2" + /> + } + {method.parameters && !_.isEmpty(method.parameters) && + <div> + <h4 + className="pb1 thin" + style={{borderBottom: '1px solid #e1e8ed'}} + > + ARGUMENTS + </h4> + {this.renderParameterDescriptions(method.parameters)} + </div> + } + {method.returnComment && + <div className="pt1 comment"> + <h4 + className="pb1 thin" + style={{borderBottom: '1px solid #e1e8ed'}} + > + RETURNS + </h4> + <Comment + comment={method.returnComment} + /> + </div> + } + </div> + ); + } + private renderChip(text: string) { + return ( + <div + className="p1 mr1" + style={styles.chip} + > + {text} + </div> + ); + } + private renderParameterDescriptions(parameters: Parameter[]) { + const descriptions = _.map(parameters, parameter => { + const isOptional = parameter.isOptional; + return ( + <div + key={`param-description-${parameter.name}`} + className="flex pb1 mb2" + style={{borderBottom: '1px solid #f0f4f7'}} + > + <div className="col lg-col-1 md-col-1 sm-hide xs-hide" /> + <div className="col lg-col-3 md-col-3 sm-col-12 col-12"> + <div className="bold"> + {parameter.name} + </div> + <div className="pt1" style={{color: colors.grey500, fontSize: 14}}> + {isOptional && 'optional'} + </div> + </div> + <div className="col lg-col-8 md-col-8 sm-col-12 col-12"> + {parameter.comment && + <Comment + comment={parameter.comment} + /> + } + </div> + </div> + ); + }); + return descriptions; + } + private setAnchorVisibility(shouldShowAnchor: boolean) { + this.setState({ + shouldShowAnchor, + }); + } +} diff --git a/packages/website/ts/pages/documentation/method_signature.tsx b/packages/website/ts/pages/documentation/method_signature.tsx new file mode 100644 index 000000000..3b5d2ce78 --- /dev/null +++ b/packages/website/ts/pages/documentation/method_signature.tsx @@ -0,0 +1,62 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {TypescriptMethod, SolidityMethod, TypeDefinitionByName, Parameter} from 'ts/types'; +import {Type} from 'ts/pages/documentation/type'; + +interface MethodSignatureProps { + method: TypescriptMethod|SolidityMethod; + shouldHideMethodName?: boolean; + shouldUseArrowSyntax?: boolean; + typeDefinitionByName?: TypeDefinitionByName; +} + +const defaultProps = { + shouldHideMethodName: false, + shouldUseArrowSyntax: false, +}; + +export const MethodSignature: React.SFC<MethodSignatureProps> = (props: MethodSignatureProps) => { + const parameters = renderParameters(props.method, props.typeDefinitionByName); + const paramString = _.reduce(parameters, (prev: React.ReactNode, curr: React.ReactNode) => { + return [prev, ', ', curr]; + }); + const methodName = props.shouldHideMethodName ? '' : props.method.name; + const typeParameterIfExists = _.isUndefined((props.method as TypescriptMethod).typeParameter) ? + undefined : + renderTypeParameter(props.method, props.typeDefinitionByName); + return ( + <span> + {props.method.callPath}{methodName}{typeParameterIfExists}({paramString}) + {props.shouldUseArrowSyntax ? ' => ' : ': '} + {' '} + {props.method.returnType && + <Type type={props.method.returnType} typeDefinitionByName={props.typeDefinitionByName}/> + } + </span> + ); +}; + +function renderParameters(method: TypescriptMethod|SolidityMethod, typeDefinitionByName?: TypeDefinitionByName) { + const parameters = method.parameters; + const params = _.map(parameters, (p: Parameter) => { + const isOptional = p.isOptional; + return ( + <span key={`param-${p.type}-${p.name}`}> + {p.name}{isOptional && '?'}: <Type type={p.type} typeDefinitionByName={typeDefinitionByName}/> + </span> + ); + }); + return params; +} + +function renderTypeParameter(method: TypescriptMethod, typeDefinitionByName?: TypeDefinitionByName) { + const typeParameter = method.typeParameter; + const typeParam = ( + <span> + {`<${typeParameter.name} extends `} + <Type type={typeParameter.type} typeDefinitionByName={typeDefinitionByName}/> + {`>`} + </span> + ); + return typeParam; +} diff --git a/packages/website/ts/pages/documentation/smart_contracts_documentation.tsx b/packages/website/ts/pages/documentation/smart_contracts_documentation.tsx new file mode 100644 index 000000000..3e97829c4 --- /dev/null +++ b/packages/website/ts/pages/documentation/smart_contracts_documentation.tsx @@ -0,0 +1,401 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import DocumentTitle = require('react-document-title'); +import findVersions = require('find-versions'); +import semverSort = require('semver-sort'); +import {colors} from 'material-ui/styles'; +import CircularProgress from 'material-ui/CircularProgress'; +import { + scroller, +} from 'react-scroll'; +import {Dispatcher} from 'ts/redux/dispatcher'; +import { + SmartContractsDocSections, + Styles, + DoxityDocObj, + TypeDefinitionByName, + DocAgnosticFormat, + SolidityMethod, + Property, + CustomType, + MenuSubsectionsBySection, + Event, + Docs, + AddressByContractName, + Networks, + EtherscanLinkSuffixes, +} from 'ts/types'; +import {TopBar} from 'ts/components/top_bar'; +import {utils} from 'ts/utils/utils'; +import {docUtils} from 'ts/utils/doc_utils'; +import {constants} from 'ts/utils/constants'; +import {MethodBlock} from 'ts/pages/documentation/method_block'; +import {SourceLink} from 'ts/pages/documentation/source_link'; +import {Type} from 'ts/pages/documentation/type'; +import {TypeDefinition} from 'ts/pages/documentation/type_definition'; +import {MarkdownSection} from 'ts/pages/shared/markdown_section'; +import {Comment} from 'ts/pages/documentation/comment'; +import {Badge} from 'ts/components/ui/badge'; +import {EventDefinition} from 'ts/pages/documentation/event_definition'; +import {AnchorTitle} from 'ts/pages/shared/anchor_title'; +import {SectionHeader} from 'ts/pages/shared/section_header'; +import {NestedSidebarMenu} from 'ts/pages/shared/nested_sidebar_menu'; +import {doxityUtils} from 'ts/utils/doxity_utils'; +/* tslint:disable:no-var-requires */ +const IntroMarkdown = require('md/docs/smart_contracts/introduction'); +/* tslint:enable:no-var-requires */ + +const SCROLL_TO_TIMEOUT = 500; +const CUSTOM_PURPLE = '#690596'; +const CUSTOM_RED = '#e91751'; +const CUSTOM_TURQUOIS = '#058789'; +const DOC_JSON_ROOT = constants.S3_SMART_CONTRACTS_DOCUMENTATION_JSON_ROOT; + +const sectionNameToMarkdown = { + [SmartContractsDocSections.Introduction]: IntroMarkdown, +}; +const networkNameToColor: {[network: string]: string} = { + [Networks.kovan]: CUSTOM_PURPLE, + [Networks.ropsten]: CUSTOM_RED, + [Networks.mainnet]: CUSTOM_TURQUOIS, +}; + +export interface SmartContractsDocumentationAllProps { + source: string; + location: Location; + dispatcher: Dispatcher; + docsVersion: string; + availableDocVersions: string[]; +} + +interface SmartContractsDocumentationState { + docAgnosticFormat?: DocAgnosticFormat; +} + +const styles: Styles = { + mainContainers: { + position: 'absolute', + top: 60, + left: 0, + bottom: 0, + right: 0, + overflowZ: 'hidden', + overflowY: 'scroll', + minHeight: 'calc(100vh - 60px)', + WebkitOverflowScrolling: 'touch', + }, + menuContainer: { + borderColor: colors.grey300, + maxWidth: 330, + marginLeft: 20, + }, +}; + +export class SmartContractsDocumentation extends + React.Component<SmartContractsDocumentationAllProps, SmartContractsDocumentationState> { + constructor(props: SmartContractsDocumentationAllProps) { + super(props); + this.state = { + docAgnosticFormat: undefined, + }; + } + public componentWillMount() { + const pathName = this.props.location.pathname; + const lastSegment = pathName.substr(pathName.lastIndexOf('/') + 1); + const versions = findVersions(lastSegment); + const preferredVersionIfExists = versions.length > 0 ? versions[0] : undefined; + this.fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists); + } + public render() { + const menuSubsectionsBySection = _.isUndefined(this.state.docAgnosticFormat) + ? {} + : this.getMenuSubsectionsBySection(this.state.docAgnosticFormat); + return ( + <div> + <DocumentTitle title="0x Smart Contract Documentation"/> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + docsVersion={this.props.docsVersion} + availableDocVersions={this.props.availableDocVersions} + menuSubsectionsBySection={menuSubsectionsBySection} + shouldFullWidth={true} + doc={Docs.SmartContracts} + /> + {_.isUndefined(this.state.docAgnosticFormat) ? + <div + className="col col-12" + style={styles.mainContainers} + > + <div + className="relative sm-px2 sm-pt2 sm-m1" + style={{height: 122, top: '50%', transform: 'translateY(-50%)'}} + > + <div className="center pb2"> + <CircularProgress size={40} thickness={5} /> + </div> + <div className="center pt2" style={{paddingBottom: 11}}>Loading documentation...</div> + </div> + </div> : + <div + className="mx-auto flex" + style={{color: colors.grey800, height: 43}} + > + <div className="relative col md-col-3 lg-col-3 lg-pl0 md-pl1 sm-hide xs-hide"> + <div + className="border-right absolute" + style={{...styles.menuContainer, ...styles.mainContainers}} + > + <NestedSidebarMenu + selectedVersion={this.props.docsVersion} + versions={this.props.availableDocVersions} + topLevelMenu={constants.menuSmartContracts} + menuSubsectionsBySection={menuSubsectionsBySection} + doc={Docs.SmartContracts} + /> + </div> + </div> + <div className="relative col lg-col-9 md-col-9 sm-col-12 col-12"> + <div + id="documentation" + style={styles.mainContainers} + className="absolute" + > + <div id="smartContractsDocs" /> + <h1 className="md-pl2 sm-pl3"> + <a href={constants.GITHUB_CONTRACTS_URL} target="_blank"> + 0x Smart Contracts + </a> + </h1> + {this.renderDocumentation()} + </div> + </div> + </div> + } + </div> + ); + } + private renderDocumentation(): React.ReactNode { + const subMenus = _.values(constants.menuSmartContracts); + const orderedSectionNames = _.flatten(subMenus); + // Since smart contract method params are all base types, no need to pass + // down the typeDefinitionByName + const typeDefinitionByName = {}; + const sections = _.map(orderedSectionNames, this.renderSection.bind(this, typeDefinitionByName)); + + return sections; + } + private renderSection(typeDefinitionByName: TypeDefinitionByName, sectionName: string): React.ReactNode { + const docSection = this.state.docAgnosticFormat[sectionName]; + + const markdownFileIfExists = sectionNameToMarkdown[sectionName]; + if (!_.isUndefined(markdownFileIfExists)) { + return ( + <MarkdownSection + key={`markdown-section-${sectionName}`} + sectionName={sectionName} + markdownContent={markdownFileIfExists} + /> + ); + } + + if (_.isUndefined(docSection)) { + return null; + } + + const sortedProperties = _.sortBy(docSection.properties, 'name'); + const propertyDefs = _.map(sortedProperties, this.renderProperty.bind(this)); + + const sortedMethods = _.sortBy(docSection.methods, 'name'); + const methodDefs = _.map(sortedMethods, method => { + const isConstructor = false; + return this.renderMethodBlocks(method, sectionName, isConstructor, typeDefinitionByName); + }); + + const sortedEvents = _.sortBy(docSection.events, 'name'); + const eventDefs = _.map(sortedEvents, (event: Event, i: number) => { + return ( + <EventDefinition + key={`event-${event.name}-${i}`} + event={event} + /> + ); + }); + return ( + <div + key={`section-${sectionName}`} + className="py2 pr3 md-pl2 sm-pl3" + > + <div className="flex"> + <div style={{marginRight: 7}}> + <SectionHeader sectionName={sectionName} /> + </div> + {this.renderNetworkBadges(sectionName)} + </div> + {docSection.comment && + <Comment + comment={docSection.comment} + /> + } + {docSection.constructors.length > 0 && + <div> + <h2 className="thin">Constructor</h2> + {this.renderConstructors(docSection.constructors, typeDefinitionByName)} + </div> + } + {docSection.properties.length > 0 && + <div> + <h2 className="thin">Properties</h2> + <div>{propertyDefs}</div> + </div> + } + {docSection.methods.length > 0 && + <div> + <h2 className="thin">Methods</h2> + <div>{methodDefs}</div> + </div> + } + {docSection.events.length > 0 && + <div> + <h2 className="thin">Events</h2> + <div>{eventDefs}</div> + </div> + } + </div> + ); + } + private renderNetworkBadges(sectionName: string) { + const networkToAddressByContractName = constants.contractAddresses[this.props.docsVersion]; + const badges = _.map(networkToAddressByContractName, + (addressByContractName: AddressByContractName, networkName: string) => { + const contractAddress = addressByContractName[sectionName]; + const linkIfExists = utils.getEtherScanLinkIfExists( + contractAddress, constants.networkIdByName[networkName], EtherscanLinkSuffixes.address, + ); + return ( + <a + key={`badge-${networkName}-${sectionName}`} + href={linkIfExists} + target="_blank" + style={{color: 'white', textDecoration: 'none'}} + > + <Badge + title={networkName} + backgroundColor={networkNameToColor[networkName]} + /> + </a> + ); + }); + return badges; + } + private renderConstructors(constructors: SolidityMethod[], + typeDefinitionByName: TypeDefinitionByName): React.ReactNode { + const constructorDefs = _.map(constructors, constructor => { + return this.renderMethodBlocks( + constructor, SmartContractsDocSections.zeroEx, constructor.isConstructor, typeDefinitionByName, + ); + }); + return ( + <div> + {constructorDefs} + </div> + ); + } + private renderProperty(property: Property): React.ReactNode { + return ( + <div + key={`property-${property.name}-${property.type.name}`} + className="pb3" + > + <code className="hljs"> + {property.name}: <Type type={property.type} /> + </code> + {property.source && + <SourceLink + version={this.props.docsVersion} + source={property.source} + /> + } + {property.comment && + <Comment + comment={property.comment} + className="py2" + /> + } + </div> + ); + } + private renderMethodBlocks(method: SolidityMethod, sectionName: string, isConstructor: boolean, + typeDefinitionByName: TypeDefinitionByName): React.ReactNode { + return ( + <MethodBlock + key={`method-${method.name}`} + method={method} + typeDefinitionByName={typeDefinitionByName} + libraryVersion={this.props.docsVersion} + /> + ); + } + private scrollToHash(): void { + const hashWithPrefix = this.props.location.hash; + let hash = hashWithPrefix.slice(1); + if (_.isEmpty(hash)) { + hash = 'smartContractsDocs'; // scroll to the top + } + + scroller.scrollTo(hash, {duration: 0, offset: 0, containerId: 'documentation'}); + } + private getMenuSubsectionsBySection(docAgnosticFormat?: DocAgnosticFormat): MenuSubsectionsBySection { + const menuSubsectionsBySection = {} as MenuSubsectionsBySection; + if (_.isUndefined(docAgnosticFormat)) { + return menuSubsectionsBySection; + } + + const docSections = _.keys(SmartContractsDocSections); + _.each(docSections, sectionName => { + const docSection = docAgnosticFormat[sectionName]; + if (_.isUndefined(docSection)) { + return; // no-op + } + + if (sectionName === SmartContractsDocSections.types) { + const sortedTypesNames = _.sortBy(docSection.types, 'name'); + const typeNames = _.map(sortedTypesNames, t => t.name); + menuSubsectionsBySection[sectionName] = typeNames; + } else { + const sortedEventNames = _.sortBy(docSection.events, 'name'); + const eventNames = _.map(sortedEventNames, m => m.name); + const sortedMethodNames = _.sortBy(docSection.methods, 'name'); + const methodNames = _.map(sortedMethodNames, m => m.name); + menuSubsectionsBySection[sectionName] = [...methodNames, ...eventNames]; + } + }); + return menuSubsectionsBySection; + } + private async fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists?: string): Promise<void> { + const versionToFileName = await docUtils.getVersionToFileNameAsync(DOC_JSON_ROOT); + const versions = _.keys(versionToFileName); + this.props.dispatcher.updateAvailableDocVersions(versions); + const sortedVersions = semverSort.desc(versions); + const latestVersion = sortedVersions[0]; + + let versionToFetch = latestVersion; + if (!_.isUndefined(preferredVersionIfExists)) { + const preferredVersionFileNameIfExists = versionToFileName[preferredVersionIfExists]; + if (!_.isUndefined(preferredVersionFileNameIfExists)) { + versionToFetch = preferredVersionIfExists; + } + } + this.props.dispatcher.updateCurrentDocsVersion(versionToFetch); + + const versionFileNameToFetch = versionToFileName[versionToFetch]; + const versionDocObj = await docUtils.getJSONDocFileAsync(versionFileNameToFetch, DOC_JSON_ROOT); + const docAgnosticFormat = doxityUtils.convertToDocAgnosticFormat(versionDocObj as DoxityDocObj); + + this.setState({ + docAgnosticFormat, + }, () => { + this.scrollToHash(); + }); + } +} diff --git a/packages/website/ts/pages/documentation/source_link.tsx b/packages/website/ts/pages/documentation/source_link.tsx new file mode 100644 index 000000000..2fb69e2f0 --- /dev/null +++ b/packages/website/ts/pages/documentation/source_link.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import {colors} from 'material-ui/styles'; +import {Source} from 'ts/types'; +import {constants} from 'ts/utils/constants'; + +interface SourceLinkProps { + source: Source; + version: string; +} + +export function SourceLink(props: SourceLinkProps) { + const source = props.source; + const githubUrl = constants.GITHUB_0X_JS_URL; + const sourceCodeUrl = `${githubUrl}/blob/v${props.version}/${source.fileName}#L${source.line}`; + return ( + <div className="pt2" style={{fontSize: 14}}> + <a + href={sourceCodeUrl} + target="_blank" + className="underline" + style={{color: colors.grey500}} + > + Source + </a> + </div> + ); +} diff --git a/packages/website/ts/pages/documentation/type.tsx b/packages/website/ts/pages/documentation/type.tsx new file mode 100644 index 000000000..af18f97c2 --- /dev/null +++ b/packages/website/ts/pages/documentation/type.tsx @@ -0,0 +1,187 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {Link as ScrollLink} from 'react-scroll'; +import * as ReactTooltip from 'react-tooltip'; +import {colors} from 'material-ui/styles'; +import {typeDocUtils} from 'ts/utils/typedoc_utils'; +import {constants} from 'ts/utils/constants'; +import {Type as TypeDef, TypeDocTypes, TypeDefinitionByName} from 'ts/types'; +import {utils} from 'ts/utils/utils'; +import {TypeDefinition} from 'ts/pages/documentation/type_definition'; + +const BUILT_IN_TYPE_COLOR = '#e69d00'; +const STRING_LITERAL_COLOR = '#4da24b'; + +// Some types reference other libraries. For these types, we want to link the user to the relevant documentation. +const typeToUrl: {[typeName: string]: string} = { + Web3: constants.WEB3_DOCS_URL, + Provider: constants.WEB3_PROVIDER_DOCS_URL, + BigNumber: constants.BIGNUMBERJS_GITHUB_URL, +}; + +const typeToSection: {[typeName: string]: string} = { + ExchangeWrapper: 'exchange', + TokenWrapper: 'token', + TokenRegistryWrapper: 'tokenRegistry', + EtherTokenWrapper: 'etherToken', + ProxyWrapper: 'proxy', + TokenTransferProxyWrapper: 'proxy', +}; + +interface TypeProps { + type: TypeDef; + typeDefinitionByName?: TypeDefinitionByName; +} + +// The return type needs to be `any` here so that we can recursively define <Type /> components within +// <Type /> components (e.g when rendering the union type). +export function Type(props: TypeProps): any { + const type = props.type; + const isIntrinsic = type.typeDocType === TypeDocTypes.Intrinsic; + const isReference = type.typeDocType === TypeDocTypes.Reference; + const isArray = type.typeDocType === TypeDocTypes.Array; + const isStringLiteral = type.typeDocType === TypeDocTypes.StringLiteral; + let typeNameColor = 'inherit'; + let typeName: string|React.ReactNode; + let typeArgs: React.ReactNode[] = []; + switch (type.typeDocType) { + case TypeDocTypes.Intrinsic: + case TypeDocTypes.Unknown: + typeName = type.name; + typeNameColor = BUILT_IN_TYPE_COLOR; + break; + + case TypeDocTypes.Reference: + typeName = type.name; + typeArgs = _.map(type.typeArguments, (arg: TypeDef) => { + if (arg.typeDocType === TypeDocTypes.Array) { + const key = `type-${arg.elementType.name}-${arg.elementType.typeDocType}`; + return ( + <span> + <Type + key={key} + type={arg.elementType} + typeDefinitionByName={props.typeDefinitionByName} + />[] + </span> + ); + } else { + const subType = ( + <Type + key={`type-${arg.name}-${arg.value}-${arg.typeDocType}`} + type={arg} + typeDefinitionByName={props.typeDefinitionByName} + /> + ); + return subType; + } + }); + break; + + case TypeDocTypes.StringLiteral: + typeName = `'${type.value}'`; + typeNameColor = STRING_LITERAL_COLOR; + break; + + case TypeDocTypes.Array: + typeName = type.elementType.name; + break; + + case TypeDocTypes.Union: + const unionTypes = _.map(type.types, t => { + return ( + <Type + key={`type-${t.name}-${t.value}-${t.typeDocType}`} + type={t} + typeDefinitionByName={props.typeDefinitionByName} + /> + ); + }); + typeName = _.reduce(unionTypes, (prev: React.ReactNode, curr: React.ReactNode) => { + return [prev, '|', curr]; + }); + break; + + case TypeDocTypes.TypeParameter: + typeName = type.name; + break; + + default: + throw utils.spawnSwitchErr('type.typeDocType', type.typeDocType); + } + // HACK: Normalize BigNumber to simply BigNumber. For some reason the type + // name is unpredictably one or the other. + if (typeName === 'BigNumber') { + typeName = 'BigNumber'; + } + const commaSeparatedTypeArgs = _.reduce(typeArgs, (prev: React.ReactNode, curr: React.ReactNode) => { + return [prev, ', ', curr]; + }); + + const typeNameUrlIfExists = typeToUrl[(typeName as string)]; + const sectionNameIfExists = typeToSection[(typeName as string)]; + if (!_.isUndefined(typeNameUrlIfExists)) { + typeName = ( + <a + href={typeNameUrlIfExists} + target="_blank" + className="text-decoration-none" + style={{color: colors.lightBlueA700}} + > + {typeName} + </a> + ); + } else if ((isReference || isArray) && + (typeDocUtils.isPublicType(typeName as string) || + !_.isUndefined(sectionNameIfExists))) { + const id = Math.random().toString(); + const typeDefinitionAnchorId = _.isUndefined(sectionNameIfExists) ? typeName : sectionNameIfExists; + let typeDefinition; + if (props.typeDefinitionByName) { + typeDefinition = props.typeDefinitionByName[typeName as string]; + } + typeName = ( + <ScrollLink + to={typeDefinitionAnchorId} + offset={0} + duration={constants.DOCS_SCROLL_DURATION_MS} + containerId={constants.DOCS_CONTAINER_ID} + > + {_.isUndefined(typeDefinition) || utils.isUserOnMobile() ? + <span + onClick={utils.setUrlHash.bind(null, typeDefinitionAnchorId)} + style={{color: colors.lightBlueA700, cursor: 'pointer'}} + > + {typeName} + </span> : + <span + data-tip={true} + data-for={id} + onClick={utils.setUrlHash.bind(null, typeDefinitionAnchorId)} + style={{color: colors.lightBlueA700, cursor: 'pointer', display: 'inline-block'}} + > + {typeName} + <ReactTooltip + type="light" + effect="solid" + id={id} + className="typeTooltip" + > + <TypeDefinition customType={typeDefinition} shouldAddId={false} /> + </ReactTooltip> + </span> + } + </ScrollLink> + ); + } + return ( + <span> + <span style={{color: typeNameColor}}>{typeName}</span> + {isArray && '[]'}{!_.isEmpty(typeArgs) && + <span> + {'<'}{commaSeparatedTypeArgs}{'>'} + </span> + } + </span> + ); +} diff --git a/packages/website/ts/pages/documentation/type_definition.tsx b/packages/website/ts/pages/documentation/type_definition.tsx new file mode 100644 index 000000000..bcb07be8e --- /dev/null +++ b/packages/website/ts/pages/documentation/type_definition.tsx @@ -0,0 +1,135 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {constants} from 'ts/utils/constants'; +import {utils} from 'ts/utils/utils'; +import {KindString, CustomType, TypeDocTypes, CustomTypeChild, HeaderSizes} from 'ts/types'; +import {Type} from 'ts/pages/documentation/type'; +import {Interface} from 'ts/pages/documentation/interface'; +import {CustomEnum} from 'ts/pages/documentation/custom_enum'; +import {Enum} from 'ts/pages/documentation/enum'; +import {MethodSignature} from 'ts/pages/documentation/method_signature'; +import {AnchorTitle} from 'ts/pages/shared/anchor_title'; +import {Comment} from 'ts/pages/documentation/comment'; +import {typeDocUtils} from 'ts/utils/typedoc_utils'; + +const KEYWORD_COLOR = '#a81ca6'; + +interface TypeDefinitionProps { + customType: CustomType; + shouldAddId?: boolean; +} + +interface TypeDefinitionState { + shouldShowAnchor: boolean; +} + +export class TypeDefinition extends React.Component<TypeDefinitionProps, TypeDefinitionState> { + public static defaultProps: Partial<TypeDefinitionProps> = { + shouldAddId: true, + }; + constructor(props: TypeDefinitionProps) { + super(props); + this.state = { + shouldShowAnchor: false, + }; + } + public render() { + const customType = this.props.customType; + if (!typeDocUtils.isPublicType(customType.name)) { + return null; // no-op + } + + let typePrefix: string; + let codeSnippet: React.ReactNode; + switch (customType.kindString) { + case KindString.Interface: + typePrefix = 'Interface'; + codeSnippet = ( + <Interface + type={customType} + /> + ); + break; + + case KindString.Variable: + typePrefix = 'Enum'; + codeSnippet = ( + <CustomEnum + type={customType} + /> + ); + break; + + case KindString.Enumeration: + typePrefix = 'Enum'; + const enumValues = _.map(customType.children, (c: CustomTypeChild) => { + return { + name: c.name, + defaultValue: c.defaultValue, + }; + }); + codeSnippet = ( + <Enum + values={enumValues} + /> + ); + break; + + case KindString['Type alias']: + typePrefix = 'Type Alias'; + codeSnippet = ( + <span> + <span style={{color: KEYWORD_COLOR}}>type</span> {customType.name} ={' '} + {customType.type.typeDocType !== TypeDocTypes.Reflection ? + <Type type={customType.type} /> : + <MethodSignature + method={customType.type.method} + shouldHideMethodName={true} + shouldUseArrowSyntax={true} + /> + } + </span> + ); + break; + + default: + throw utils.spawnSwitchErr('type.kindString', customType.kindString); + } + + const typeDefinitionAnchorId = customType.name; + return ( + <div + id={this.props.shouldAddId ? typeDefinitionAnchorId : ''} + className="pb2" + style={{overflow: 'hidden', width: '100%'}} + onMouseOver={this.setAnchorVisibility.bind(this, true)} + onMouseOut={this.setAnchorVisibility.bind(this, false)} + > + <AnchorTitle + headerSize={HeaderSizes.H3} + title={`${typePrefix} ${customType.name}`} + id={this.props.shouldAddId ? typeDefinitionAnchorId : ''} + shouldShowAnchor={this.state.shouldShowAnchor} + /> + <div style={{fontSize: 16}}> + <pre> + <code className="hljs"> + {codeSnippet} + </code> + </pre> + </div> + {customType.comment && + <Comment + comment={customType.comment} + className="py2" + /> + } + </div> + ); + } + private setAnchorVisibility(shouldShowAnchor: boolean) { + this.setState({ + shouldShowAnchor, + }); + } +} diff --git a/packages/website/ts/pages/documentation/zero_ex_js_documentation.tsx b/packages/website/ts/pages/documentation/zero_ex_js_documentation.tsx new file mode 100644 index 000000000..c26fb7d0a --- /dev/null +++ b/packages/website/ts/pages/documentation/zero_ex_js_documentation.tsx @@ -0,0 +1,340 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as ReactMarkdown from 'react-markdown'; +import DocumentTitle = require('react-document-title'); +import findVersions = require('find-versions'); +import semverSort = require('semver-sort'); +import {colors} from 'material-ui/styles'; +import MenuItem from 'material-ui/MenuItem'; +import CircularProgress from 'material-ui/CircularProgress'; +import Paper from 'material-ui/Paper'; +import { + Link as ScrollLink, + Element as ScrollElement, + scroller, +} from 'react-scroll'; +import {Dispatcher} from 'ts/redux/dispatcher'; +import { + KindString, + TypeDocNode, + ZeroExJsDocSections, + Styles, + ScreenWidths, + TypeDefinitionByName, + DocAgnosticFormat, + TypescriptMethod, + Property, + CustomType, + Docs, +} from 'ts/types'; +import {TopBar} from 'ts/components/top_bar'; +import {utils} from 'ts/utils/utils'; +import {docUtils} from 'ts/utils/doc_utils'; +import {constants} from 'ts/utils/constants'; +import {Loading} from 'ts/components/ui/loading'; +import {MethodBlock} from 'ts/pages/documentation/method_block'; +import {SourceLink} from 'ts/pages/documentation/source_link'; +import {Type} from 'ts/pages/documentation/type'; +import {TypeDefinition} from 'ts/pages/documentation/type_definition'; +import {MarkdownSection} from 'ts/pages/shared/markdown_section'; +import {Comment} from 'ts/pages/documentation/comment'; +import {AnchorTitle} from 'ts/pages/shared/anchor_title'; +import {SectionHeader} from 'ts/pages/shared/section_header'; +import {NestedSidebarMenu} from 'ts/pages/shared/nested_sidebar_menu'; +import {typeDocUtils} from 'ts/utils/typedoc_utils'; +/* tslint:disable:no-var-requires */ +const IntroMarkdown = require('md/docs/0xjs/introduction'); +const InstallationMarkdown = require('md/docs/0xjs/installation'); +const AsyncMarkdown = require('md/docs/0xjs/async'); +const ErrorsMarkdown = require('md/docs/0xjs/errors'); +const versioningMarkdown = require('md/docs/0xjs/versioning'); +/* tslint:enable:no-var-requires */ + +const SCROLL_TO_TIMEOUT = 500; +const DOC_JSON_ROOT = constants.S3_0XJS_DOCUMENTATION_JSON_ROOT; + +const sectionNameToMarkdown = { + [ZeroExJsDocSections.introduction]: IntroMarkdown, + [ZeroExJsDocSections.installation]: InstallationMarkdown, + [ZeroExJsDocSections.async]: AsyncMarkdown, + [ZeroExJsDocSections.errors]: ErrorsMarkdown, + [ZeroExJsDocSections.versioning]: versioningMarkdown, +}; + +export interface ZeroExJSDocumentationPassedProps { + source: string; + location: Location; +} + +export interface ZeroExJSDocumentationAllProps { + source: string; + location: Location; + dispatcher: Dispatcher; + docsVersion: string; + availableDocVersions: string[]; +} + +interface ZeroExJSDocumentationState { + docAgnosticFormat?: DocAgnosticFormat; +} + +const styles: Styles = { + mainContainers: { + position: 'absolute', + top: 60, + left: 0, + bottom: 0, + right: 0, + overflowZ: 'hidden', + overflowY: 'scroll', + minHeight: 'calc(100vh - 60px)', + WebkitOverflowScrolling: 'touch', + }, + menuContainer: { + borderColor: colors.grey300, + maxWidth: 330, + marginLeft: 20, + }, +}; + +export class ZeroExJSDocumentation extends React.Component<ZeroExJSDocumentationAllProps, ZeroExJSDocumentationState> { + constructor(props: ZeroExJSDocumentationAllProps) { + super(props); + this.state = { + docAgnosticFormat: undefined, + }; + } + public componentWillMount() { + const pathName = this.props.location.pathname; + const lastSegment = pathName.substr(pathName.lastIndexOf('/') + 1); + const versions = findVersions(lastSegment); + const preferredVersionIfExists = versions.length > 0 ? versions[0] : undefined; + this.fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists); + } + public render() { + const menuSubsectionsBySection = _.isUndefined(this.state.docAgnosticFormat) + ? {} + : typeDocUtils.getMenuSubsectionsBySection(this.state.docAgnosticFormat); + return ( + <div> + <DocumentTitle title="0x.js Documentation"/> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + docsVersion={this.props.docsVersion} + availableDocVersions={this.props.availableDocVersions} + menuSubsectionsBySection={menuSubsectionsBySection} + shouldFullWidth={true} + doc={Docs.ZeroExJs} + /> + {_.isUndefined(this.state.docAgnosticFormat) ? + <div + className="col col-12" + style={styles.mainContainers} + > + <div + className="relative sm-px2 sm-pt2 sm-m1" + style={{height: 122, top: '50%', transform: 'translateY(-50%)'}} + > + <div className="center pb2"> + <CircularProgress size={40} thickness={5} /> + </div> + <div className="center pt2" style={{paddingBottom: 11}}>Loading documentation...</div> + </div> + </div> : + <div + className="mx-auto flex" + style={{color: colors.grey800, height: 43}} + > + <div className="relative col md-col-3 lg-col-3 lg-pl0 md-pl1 sm-hide xs-hide"> + <div + className="border-right absolute" + style={{...styles.menuContainer, ...styles.mainContainers}} + > + <NestedSidebarMenu + selectedVersion={this.props.docsVersion} + versions={this.props.availableDocVersions} + topLevelMenu={typeDocUtils.getFinal0xjsMenu(this.props.docsVersion)} + menuSubsectionsBySection={menuSubsectionsBySection} + doc={Docs.ZeroExJs} + /> + </div> + </div> + <div className="relative col lg-col-9 md-col-9 sm-col-12 col-12"> + <div + id="documentation" + style={styles.mainContainers} + className="absolute" + > + <div id="zeroExJSDocs" /> + <h1 className="md-pl2 sm-pl3"> + <a href={constants.GITHUB_0X_JS_URL} target="_blank"> + 0x.js + </a> + </h1> + {this.renderDocumentation()} + </div> + </div> + </div> + } + </div> + ); + } + private renderDocumentation(): React.ReactNode { + const typeDocSection = this.state.docAgnosticFormat[ZeroExJsDocSections.types]; + const typeDefinitionByName = _.keyBy(typeDocSection.types, 'name'); + + const subMenus = _.values(constants.menu0xjs); + const orderedSectionNames = _.flatten(subMenus); + const sections = _.map(orderedSectionNames, this.renderSection.bind(this, typeDefinitionByName)); + + return sections; + } + private renderSection(typeDefinitionByName: TypeDefinitionByName, sectionName: string): React.ReactNode { + const docSection = this.state.docAgnosticFormat[sectionName]; + + const markdownFileIfExists = sectionNameToMarkdown[sectionName]; + if (!_.isUndefined(markdownFileIfExists)) { + return ( + <MarkdownSection + key={`markdown-section-${sectionName}`} + sectionName={sectionName} + markdownContent={markdownFileIfExists} + /> + ); + } + + if (_.isUndefined(docSection)) { + return null; + } + + const typeDefs = _.map(docSection.types, customType => { + return ( + <TypeDefinition + key={`type-${customType.name}`} + customType={customType} + /> + ); + }); + const propertyDefs = _.map(docSection.properties, this.renderProperty.bind(this)); + const methodDefs = _.map(docSection.methods, method => { + const isConstructor = false; + return this.renderMethodBlocks(method, sectionName, isConstructor, typeDefinitionByName); + }); + return ( + <div + key={`section-${sectionName}`} + className="py2 pr3 md-pl2 sm-pl3" + > + <SectionHeader sectionName={sectionName} /> + <Comment + comment={docSection.comment} + /> + {sectionName === ZeroExJsDocSections.zeroEx && docSection.constructors.length > 0 && + <div> + <h2 className="thin">Constructor</h2> + {this.renderZeroExConstructors(docSection.constructors, typeDefinitionByName)} + </div> + } + {docSection.properties.length > 0 && + <div> + <h2 className="thin">Properties</h2> + <div>{propertyDefs}</div> + </div> + } + {docSection.methods.length > 0 && + <div> + <h2 className="thin">Methods</h2> + <div>{methodDefs}</div> + </div> + } + {typeDefs.length > 0 && + <div> + <div>{typeDefs}</div> + </div> + } + </div> + ); + } + private renderZeroExConstructors(constructors: TypescriptMethod[], + typeDefinitionByName: TypeDefinitionByName): React.ReactNode { + const constructorDefs = _.map(constructors, constructor => { + return this.renderMethodBlocks( + constructor, ZeroExJsDocSections.zeroEx, constructor.isConstructor, typeDefinitionByName, + ); + }); + return ( + <div> + {constructorDefs} + </div> + ); + } + private renderProperty(property: Property): React.ReactNode { + return ( + <div + key={`property-${property.name}-${property.type.name}`} + className="pb3" + > + <code className="hljs"> + {property.name}: <Type type={property.type} /> + </code> + <SourceLink + version={this.props.docsVersion} + source={property.source} + /> + {property.comment && + <Comment + comment={property.comment} + className="py2" + /> + } + </div> + ); + } + private renderMethodBlocks(method: TypescriptMethod, sectionName: string, isConstructor: boolean, + typeDefinitionByName: TypeDefinitionByName): React.ReactNode { + return ( + <MethodBlock + key={`method-${method.name}-${!_.isUndefined(method.source) ? method.source.line : ''}`} + method={method} + typeDefinitionByName={typeDefinitionByName} + libraryVersion={this.props.docsVersion} + /> + ); + } + private scrollToHash(): void { + const hashWithPrefix = this.props.location.hash; + let hash = hashWithPrefix.slice(1); + if (_.isEmpty(hash)) { + hash = 'zeroExJSDocs'; // scroll to the top + } + + scroller.scrollTo(hash, {duration: 0, offset: 0, containerId: 'documentation'}); + } + private async fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists?: string): Promise<void> { + const versionToFileName = await docUtils.getVersionToFileNameAsync(DOC_JSON_ROOT); + const versions = _.keys(versionToFileName); + this.props.dispatcher.updateAvailableDocVersions(versions); + const sortedVersions = semverSort.desc(versions); + const latestVersion = sortedVersions[0]; + + let versionToFetch = latestVersion; + if (!_.isUndefined(preferredVersionIfExists)) { + const preferredVersionFileNameIfExists = versionToFileName[preferredVersionIfExists]; + if (!_.isUndefined(preferredVersionFileNameIfExists)) { + versionToFetch = preferredVersionIfExists; + } + } + this.props.dispatcher.updateCurrentDocsVersion(versionToFetch); + + const versionFileNameToFetch = versionToFileName[versionToFetch]; + const versionDocObj = await docUtils.getJSONDocFileAsync(versionFileNameToFetch, DOC_JSON_ROOT); + const docAgnosticFormat = typeDocUtils.convertToDocAgnosticFormat((versionDocObj as TypeDocNode)); + + this.setState({ + docAgnosticFormat, + }, () => { + this.scrollToHash(); + }); + } +} diff --git a/packages/website/ts/pages/faq/faq.tsx b/packages/website/ts/pages/faq/faq.tsx new file mode 100644 index 000000000..3c65d1042 --- /dev/null +++ b/packages/website/ts/pages/faq/faq.tsx @@ -0,0 +1,497 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as DocumentTitle from 'react-document-title'; +import RaisedButton from 'material-ui/RaisedButton'; +import {colors} from 'material-ui/styles'; +import {Styles, FAQSection, FAQQuestion, WebsitePaths} from 'ts/types'; +import {Link} from 'react-router-dom'; +import {Footer} from 'ts/components/footer'; +import {TopBar} from 'ts/components/top_bar'; +import {Question} from 'ts/pages/faq/question'; +import {configs} from 'ts/utils/configs'; +import {constants} from 'ts/utils/constants'; + +export interface FAQProps { + source: string; + location: Location; +} + +interface FAQState {} + +const styles: Styles = { + thin: { + fontWeight: 100, + }, +}; + +const sections: FAQSection[] = [ + { + name: '0x Protocol', + questions: [ + { + prompt: 'What is 0x?', + answer: ( + <div> + At its core, 0x is an open and non-rent seeking protocol that facilitates trustless, + low friction exchange of Ethereum-based assets. Developers can use 0x as a platform + to build exchange applications on top of{' '} + (<a href={`${configs.BASE_URL}${WebsitePaths.ZeroExJs}#introduction`} target="blank">0x.js</a> + {' '}is a Javascript library for interacting with the 0x protocol). For end users, 0x will be + the infrastructure of a wide variety of user-facing applications i.e.{' '} + <a href={`${configs.BASE_URL}${WebsitePaths.Portal}`} target="blank">0x Portal</a>, + a decentralized application that facilitates trustless trading of Ethereum-based tokens + between known counterparties. + </div> + ), + }, + { + prompt: 'What problem does 0x solve?', + answer: ( + <div> + In the two years since the Ethereum blockchain’s genesis block, numerous decentralized + applications (dApps) have created Ethereum smart contracts for peer-to-peer exchange. + Rapid iteration and a lack of best practices have left the blockchain scattered with + proprietary and application-specific implementations. As a result, end users are + exposed to numerous smart contracts of varying quality and security, with unique + configuration processes and learning curves, all of which implement the same + functionality. This approach imposes unnecessary costs on the network by fragmenting + end users according to the particular dApp each user happens to be using, eliminating + valuable network effects around liquidity. 0x is the solution to this problem by + acting as modular, unopinionated building blocks that may be assembled and reconfigured. + </div> + ), + }, + { + prompt: 'How is 0x different from a centralized exchange like Poloniex or ShapeShift?', + answer: ( + <div> + <ul> + <li> + 0x is a protocol for exchange, not a user-facing exchange application. + </li> + <li> + 0x is decentralized and trustless; there is no central party which can be + hacked, run away with customer funds or be subjected to government regulations. + Hacks of Mt. Gox, Shapeshift and Bitfinex have demonstrated that these types of + systemic risks are palpable. + </li> + <li> + Rather than a proprietary system that exists to extract rent for its owners, + 0x is public infrastructure that is funded by a globally distributed community + of stakeholders. While the protocol is free to use, it enables for-profit + user-facing exchange applications to be built on top of the protocol. + </li> + </ul> + </div> + ), + }, + { + prompt: 'If 0x protocol is free to use, where do transaction fees come in?', + answer: ( + <div> + 0x protocol uses off-chain order books to massively reduce friction costs for + market makers and ensure that the blockchain is only used for trade settlement. + Hosting and maintaining an off-chain order book is a service; to incent “Relayers” + to provide this service they must be able to charge transaction fees on trading + activity. Relayers are free to set their transaction fees to any value they desire. + We expect Relayers to be highly competitive and transaction fees to approach an + efficient economic equilibrium over time. + </div> + ), + }, + { + prompt: 'What are the differences between 0x protocol and state channels?', + answer: ( + <div> + <div> + Participants in a state channel pass cryptographically signed messages back and + forth, accumulating intermediate state changes without publishing them to the + canonical chain until the channel is closed. State channels are ideal for “bar tab” + applications where numerous intermediate state changes may be accumulated off-chain + before being settled by a final on-chain transaction (i.e. day trading, poker, + turn-based games). + </div> + <ul> + <li> + While state channels drastically reduce the number of on-chain transactions + for specific use cases, numerous on-chain transactions and a security deposit + are required to open and safely close a state channel making them less efficient + than 0x for executing one-time trades. + </li> + <li> + State channels are isolated from the Ethereum blockchain meaning that + they cannot interact with smart contracts. 0x is designed to be integrated + directly into smart contracts so trades can be executed programmatically + in a single line of Solidity code. + </li> + </ul> + </div> + ), + }, + { + prompt: 'What types of digital assets are supported by 0x?', + answer: ( + <div> + 0x supports all Ethereum-based assets that adhere to the ERC20 token standard. + There are many ERC20 tokens, worth a combined $2.2B, and more tokens are created + each month. We believe that, by 2020, thousands of assets will be tokenized and + moved onto the Ethereum blockchain including traditional securities such as equities, + bonds and derivatives, fiat currencies and scarce digital goods such as video game + items. In the future, cross-blockchain solutions such as{' '} + <a href="https://cosmos.network/" target="_blank">Cosmos</a> and{' '} + <a href="http://polkadot.io/" target="_blank">Polkadot</a> will allow cryptocurrencies + to freely move between blockchains and, naturally, currencies such as Bitcoin will + end up being represented as ERC20 tokens on the Ethereum blockchain. + </div> + ), + }, + { + prompt: '0x is open source: what prevents someone from forking the protocol?', + answer: ( + <div> + Ethereum and Bitcoin are both open source protocols. Each protocol has been forked, + but the resulting clone networks have seen little adoption (as measured by transaction + count or market cap). This is because users have little to no incentive to switch + over to a clone network if the original has initial network effects and a talented + developer team behind it. + An exception is in the case that a protocol includes a controversial feature such as + a method of rent extraction or a monetary policy that favors one group of users over + another (Zcash developer subsidy - for better or worse - resulted in Zclassic). + Perceived inequality can provide a strong enough incentive that users will fork the + original protocol’s codebase and spin up a new network that eliminates the controversial + feature. In the case of 0x, there is no rent extraction and no users are given + special permissions. + + 0x protocol is upgradable. Cutting-edge technical capabilities can be integrated + into 0x via decentralized governance (see section below), eliminating incentives + to fork off of the original protocol and sacrifice the network effects surrounding + liquidity that result from the shared protocol and settlement layer. + </div> + ), + }, + ], + }, + { + name: '0x Token (ZRX)', + questions: [ + { + prompt: 'Explain how the 0x protocol token (zrx) works.', + answer: ( + <div> + <div> + 0x protocol token (ZRX) is utilized in two ways: 1) to solve the{' '} + <a href="https://en.wikipedia.org/wiki/Coordination_game" target="_blank"> + coordination problem + </a> and drive network effects around liquidity, creating a feedback loop + where early adopters of the protocol benefit from wider adoption and 2) to + be used for decentralized governance over 0x protocol's update mechanism. + In more detail: + </div> + <ul> + <li> + ZRX tokens are used by Makers and Takers (market participants that generate + and consume orders, respectively) to pay transaction fees to Relayers + (entities that host and maintain public order books). + </li> + <li> + ZRX tokens are used for decentralized governance over 0x protocol’s update + mechanism which allows its underlying smart contracts to be replaced and + improved over time. An update mechanism is needed because 0x is built upon + Ethereum’s rapidly evolving technology stack, decentralized governance is + needed because 0x protocol’s smart contracts will have access to user funds + and numerous dApps will need to plug into 0x smart contracts. Decentralized + governance ensures that this update process is secure and minimizes disruption + to the network. + </li> + </ul> + </div> + ), + }, + { + prompt: 'Why must transaction fees be denominated in 0x token (ZRX) rather than ETH?', + answer: ( + <div> + 0x protocol’s decentralized update mechanism is analogous to proof-of-stake. + To maximize the alignment of stakeholder and end user incentives, the staked + asset must provide utility within the protocol. + </div> + ), + }, + { + prompt: 'How will decentralized governance work?', + answer: ( + <div> + Decentralized governance is an ongoing focus of research; it will involve token + voting with ZRX. Ultimately the solution will maximize security while also maximizing + the protocol’s ability to absorb new innovations. Until the governance structure is + formalized and encoded within 0x DAO, a multi-sig will be used as a placeholder. + </div> + ), + }, + ], + }, + { + name: 'ZRX Token Launch and Fund Use', + questions: [ + { + prompt: 'What is the total supply of ZRX tokens?', + answer: ( + <div> + 1,000,000,000 ZRX. Fixed supply. + </div> + ), + }, + { + prompt: 'When is the Token Launch? will there be a pre-sale?', + answer: ( + <div> + The token launch will be on August 15th, 2017. There will not be a pre-sale. + </div> + ), + }, + { + prompt: 'What will the token launch proceeds be used for?', + answer: ( + <div> + 100% of the proceeds raised in the token launch will be used to fund the development + of free and open source software, tools and infrastructure that support the protocol + and surrounding ecosystem. Check out our{' '} + <a + href="https://docs.google.com/document/d/1_RVa-_bkU92fWRsC8eNy4vYjcTt-WC8GtqyyjbTd-oY" + target="_blank" + > + development roadmap + </a>. + </div> + ), + }, + { + prompt: 'What will be the initial distribution of ZRX tokens?', + answer: ( + <div> + <div className="center" style={{width: '100%'}}> + <img + style={{width: 350}} + src="/images/zrx_pie_chart.png" + /> + </div> + <div className="py1"> + <div className="bold pb1"> + Token Launch (50%) + </div> + <div> + ZRX is inherently a governance token that plays a critical role in the + process of upgrading 0x protocol. We are fully committed to formulating + a functional and theoretically sound governance model and we plan to dedicate + significant resources to R&D. + </div> + </div> + <div className="py1"> + <div className="bold pb1"> + Retained by 0x (15%) + </div> + <div> + The 0x core development team will be able to sustain itself for approximately + five years using funds raised through the token launch. If 0x protocol + proves to be as foundational a technology as we believe it to be, the + retained ZRX tokens will allow the 0x core development team to sustain + operations beyond the first 5 years. + </div> + </div> + <div className="py1"> + <div className="bold pb1"> + Developer Fund (15%) + </div> + <div> + The Developer Fund will be used to make targeted capital injections + into high potential projects and teams that are attempting to grow + the 0x ecosystem, strategic partnerships, hackathon prizes and community + development activities. + </div> + </div> + <div className="py1"> + <div className="bold pb1"> + Founding Team (10%) + </div> + <div> + The founding team’s allocation of ZRX will vest over a traditional 4 + year vesting schedule with a one year cliff. We believe this should + be standard practice for any team that is committed to making their + project a long term success. + </div> + </div> + <div className="py1"> + <div className="bold pb1"> + Early Backers & Advisors (10%) + </div> + <div> + Our backers and advisors have provided capital, resources and guidance + that have allowed us to fill out our team, setup a robust legal entity + and build a fully functional product before launching a token. As a result, + we have a proven track record and can offer a token that holds genuine utility. + </div> + </div> + </div> + ), + }, + { + prompt: 'Can I mine ZRX tokens?', + answer: ( + <div> + No, the total supply of ZRX tokens is fixed and there is no continuous issuance + model. Users that facilitate trading over 0x protocol by operating a Relayer + earn transaction fees denominated in ZRX; as more trading activity is generated, + more transaction fees are earned. + </div> + ), + }, + { + prompt: 'Will there be a lockup period for ZRX tokens sold in the token launch?', + answer: ( + <div> + No, ZRX tokens sold in the token launch will immediately be liquid. + </div> + ), + }, + { + prompt: 'Will there be a lockup period for tokens allocated to the founding team?', + answer: ( + <div> + Yes. ZRX tokens allocated to founders, advisors and staff members will be released + over a 4 year vesting schedule with a 25% cliff upon completion of the initial + token launch and 25% released each subsequent year in monthly installments. Staff + members hired after the token launch will have a 4 year vesting schedule with a + one year cliff. + </div> + ), + }, + { + prompt: 'Which cryptocurrencies will be accepted in the token launch?', + answer: ( + <div>ETH.</div> + ), + }, + { + prompt: 'When will 0x be live?', + answer: ( + <div> + An alpha version of 0x has been live on our private test network since January + 2017. Version 1.0 of 0x protocol will be deployed to the canonical Ethereum + blockchain after a round of security audits and prior to the public token launch. + 0x will be using the 0x protocol during our token launch. + </div> + ), + }, + { + prompt: 'Where can I find a development roadmap?', + answer: ( + <div> + Check it out{' '} + <a + href="https://drive.google.com/open?id=14IP1N8mt3YdsAoqYTyruMnZswpklUs3THyS1VXx71fo" + target="_blank" + > + here + </a>. + </div> + ), + }, + ], + }, + { + name: 'Team', + questions: [ + { + prompt: 'Where is 0x based?', + answer: ( + <div> + 0x was founded in SF and is driven by a diverse group of contributors. + </div> + ), + }, + { + prompt: 'How can I get involved?', + answer: ( + <div> + Join our <a href={constants.ZEROEX_CHAT_URL} target="_blank">Rocket.chat</a>! + As an open source project, 0x will rely on a worldwide community of passionate + developers to contribute proposals, ideas and code. + </div> + ), + }, + { + prompt: 'Why the name 0x?', + answer: ( + <div> + 0x is the prefix for hexadecimal numeric constants including Ethereum addresses. + In a more abstract context, as the first open protocol for exchange 0x represents + the beginning of the end for the exchange industry’s rent seeking oligopoly: + zero exchange. + </div> + ), + }, + { + prompt: 'How do you pronounce 0x?', + answer: ( + <div> + We pronounce 0x as “zero-ex,” but you are free to pronounce it however you please. + </div> + ), + }, + ], + }, +]; + +export class FAQ extends React.Component<FAQProps, FAQState> { + public componentDidMount() { + window.scrollTo(0, 0); + } + public render() { + return ( + <div> + <DocumentTitle title="0x FAQ"/> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + /> + <div + id="faq" + className="mx-auto max-width-4 pt4" + style={{color: colors.grey800}} + > + <h1 className="center" style={{...styles.thin}}>0x FAQ</h1> + <div className="sm-px2 md-px2 lg-px0 pb4"> + {this.renderSections()} + </div> + </div> + <Footer location={this.props.location} /> + </div> + ); + } + private renderSections() { + const renderedSections = _.map(sections, (section: FAQSection, i: number) => { + const isFirstSection = i === 0; + return ( + <div key={section.name}> + <h3>{section.name}</h3> + {this.renderQuestions(section.questions, isFirstSection)} + </div> + ); + }); + return renderedSections; + } + private renderQuestions(questions: FAQQuestion[], isFirstSection: boolean) { + const renderedQuestions = _.map(questions, (question: FAQQuestion, i: number) => { + const isFirstQuestion = i === 0; + return ( + <Question + key={question.prompt} + prompt={question.prompt} + answer={question.answer} + shouldDisplayExpanded={isFirstSection && isFirstQuestion} + /> + ); + }); + return renderedQuestions; + } +} diff --git a/packages/website/ts/pages/faq/question.tsx b/packages/website/ts/pages/faq/question.tsx new file mode 100644 index 000000000..4ed198b91 --- /dev/null +++ b/packages/website/ts/pages/faq/question.tsx @@ -0,0 +1,52 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {Card, CardHeader, CardText} from 'material-ui/Card'; + +export interface QuestionProps { + prompt: string; + answer: React.ReactNode; + shouldDisplayExpanded: boolean; +} + +interface QuestionState { + isExpanded: boolean; +} + +export class Question extends React.Component<QuestionProps, QuestionState> { + constructor(props: QuestionProps) { + super(props); + this.state = { + isExpanded: props.shouldDisplayExpanded, + }; + } + public render() { + return ( + <div + className="py1" + > + <Card + initiallyExpanded={this.props.shouldDisplayExpanded} + onExpandChange={this.onExchangeChange.bind(this)} + > + <CardHeader + title={this.props.prompt} + style={{borderBottom: this.state.isExpanded ? '1px solid rgba(0, 0, 0, 0.19)' : 'none'}} + titleStyle={{color: 'rgb(66, 66, 66)'}} + actAsExpander={true} + showExpandableButton={true} + /> + <CardText expandable={true}> + <div style={{lineHeight: 1.4}}> + {this.props.answer} + </div> + </CardText> + </Card> + </div> + ); + } + private onExchangeChange() { + this.setState({ + isExpanded: !this.state.isExpanded, + }); + } +} diff --git a/packages/website/ts/pages/landing/landing.tsx b/packages/website/ts/pages/landing/landing.tsx new file mode 100644 index 000000000..32ea86736 --- /dev/null +++ b/packages/website/ts/pages/landing/landing.tsx @@ -0,0 +1,843 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import DocumentTitle = require('react-document-title'); +import {Link} from 'react-router-dom'; +import RaisedButton from 'material-ui/RaisedButton'; +import {colors} from 'material-ui/styles'; +import {configs} from 'ts/utils/configs'; +import {constants} from 'ts/utils/constants'; +import {Styles, WebsitePaths, ScreenWidths} from 'ts/types'; +import {utils} from 'ts/utils/utils'; +import {TopBar} from 'ts/components/top_bar'; +import {Footer} from 'ts/components/footer'; + +interface BoxContent { + title: string; + description: string; + imageUrl: string; + classNames: string; +} +interface AssetType { + title: string; + imageUrl: string; + style?: React.CSSProperties; +} +interface UseCase { + imageUrl: string; + type: string; + description: string; + classNames: string; + style?: React.CSSProperties; + projectIconUrls: string[]; +} +interface Project { + logoFileName: string; + projectUrl: string; +} + +const THROTTLE_TIMEOUT = 100; +const CUSTOM_HERO_BACKGROUND_COLOR = '#404040'; +const CUSTOM_PROJECTS_BACKGROUND_COLOR = '#343333'; +const CUSTOM_WHITE_BACKGROUND = 'rgb(245, 245, 245)'; +const CUSTOM_WHITE_TEXT = '#E4E4E4'; +const CUSTOM_GRAY_TEXT = '#919191'; + +const boxContents: BoxContent[] = [ + { + title: 'Trustless exchange', + description: 'Built on Ethereum\'s distributed network with no centralized \ + point of failure and no down time, each trade is settled atomically \ + and without counterparty risk.', + imageUrl: '/images/landing/distributed_network.png', + classNames: '', + }, + { + title: 'Shared liquidity', + description: 'By sharing a standard API, relayers can easily aggregate liquidity pools, \ + creating network effects around liquidity that compound as more relayers come online.', + imageUrl: '/images/landing/liquidity.png', + classNames: 'mx-auto', + }, + { + title: 'Open source', + description: '0x is open source, permissionless and free to use. Trade directly with a known \ + counterparty for free or pay a relayer some ZRX tokens to access their liquidity \ + pool.', + imageUrl: '/images/landing/open_source.png', + classNames: 'right', + }, +]; + +const projects: Project[] = [ + { + logoFileName: 'ethfinex-top.png', + projectUrl: constants.ETHFINEX_URL, + }, + { + logoFileName: 'radar_relay_top.png', + projectUrl: constants.RADAR_RELAY_URL, + }, + { + logoFileName: 'paradex_top.png', + projectUrl: constants.PARADEX_URL, + }, + { + logoFileName: 'the_ocean.png', + projectUrl: constants.OCEAN_URL, + }, + { + logoFileName: 'dydx.png', + projectUrl: constants.DYDX_URL, + }, + { + logoFileName: 'melonport.png', + projectUrl: constants.MELONPORT_URL, + }, + { + logoFileName: 'maker.png', + projectUrl: constants.MAKER_URL, + }, + { + logoFileName: 'dharma.png', + projectUrl: constants.DHARMA_URL, + }, + { + logoFileName: 'lendroid.png', + projectUrl: constants.LENDROID_URL, + }, + { + logoFileName: 'district0x.png', + projectUrl: constants.DISTRICT_0X_URL, + }, + { + logoFileName: 'aragon.png', + projectUrl: constants.ARAGON_URL, + }, + { + logoFileName: 'blocknet.png', + projectUrl: constants.BLOCKNET_URL, + }, + { + logoFileName: 'status.png', + projectUrl: constants.STATUS_URL, + }, + { + logoFileName: 'augur.png', + projectUrl: constants.AUGUR_URL, + }, + { + logoFileName: 'anx.png', + projectUrl: constants.OPEN_ANX_URL, + }, + { + logoFileName: 'auctus.png', + projectUrl: constants.AUCTUS_URL, + }, +]; + +export interface LandingProps { + location: Location; +} + +interface LandingState { + screenWidth: ScreenWidths; +} + +export class Landing extends React.Component<LandingProps, LandingState> { + private throttledScreenWidthUpdate: () => void; + constructor(props: LandingProps) { + super(props); + this.state = { + screenWidth: utils.getScreenWidth(), + }; + this.throttledScreenWidthUpdate = _.throttle(this.updateScreenWidth.bind(this), THROTTLE_TIMEOUT); + } + public componentDidMount() { + window.addEventListener('resize', this.throttledScreenWidthUpdate); + window.scrollTo(0, 0); + } + public componentWillUnmount() { + window.removeEventListener('resize', this.throttledScreenWidthUpdate); + } + public render() { + return ( + <div id="landing" className="clearfix" style={{color: colors.grey800}}> + <DocumentTitle title="0x Protocol"/> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + isNightVersion={true} + style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR, position: 'relative'}} + /> + {this.renderHero()} + {this.renderProjects()} + {this.renderTokenizationSection()} + {this.renderProtocolSection()} + {this.renderInfoBoxes()} + {this.renderBuildingBlocksSection()} + {this.renderUseCases()} + {this.renderCallToAction()} + <Footer location={this.props.location} /> + </div> + ); + } + private renderHero() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + const buttonLabelStyle: React.CSSProperties = { + textTransform: 'none', + fontSize: isSmallScreen ? 12 : 14, + fontWeight: 400, + }; + const lightButtonStyle: React.CSSProperties = { + borderRadius: 6, + border: '1px solid #D8D8D8', + lineHeight: '33px', + height: 38, + }; + const left = 'col lg-col-7 md-col-7 col-12 lg-pt4 md-pt4 sm-pt0 mt1 lg-pl4 md-pl4 sm-pl0 sm-px3 sm-center'; + return ( + <div + className="clearfix py4" + style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}} + > + <div + className="mx-auto max-width-4 clearfix" + > + <div className="lg-pt4 md-pt4 sm-pt2 lg-pb4 md-pb4 lg-my4 md-my4 sm-mt2 sm-mb4 clearfix"> + <div className="col lg-col-5 md-col-5 col-12 sm-center"> + <img + src="/images/landing/hero_chip_image.png" + height={isSmallScreen ? 300 : 395} + /> + </div> + <div + className={left} + style={{color: 'white'}} + > + <div style={{paddingLeft: isSmallScreen ? 0 : 12}}> + <div + className="sm-pb2" + style={{fontFamily: 'Roboto Mono', fontSize: isSmallScreen ? 26 : 34}} + > + Powering decentralized exchange + </div> + <div + className="pt2 h5 sm-mx-auto" + style={{maxWidth: 446, fontFamily: 'Roboto Mono', lineHeight: 1.7, fontWeight: 300}} + > + 0x is an open, permissionless protocol allowing for ERC20 tokens to + be traded on the Ethereum blockchain. + </div> + <div className="pt3 clearfix sm-mx-auto" style={{maxWidth: 342}}> + <div className="lg-pr2 md-pr2 col col-6 sm-center"> + <Link to={WebsitePaths.ZeroExJs} className="text-decoration-none"> + <RaisedButton + style={{borderRadius: 6, minWidth: 157.36}} + buttonStyle={{borderRadius: 6}} + labelStyle={buttonLabelStyle} + label="Build on 0x" + onClick={_.noop} + /> + </Link> + </div> + <div className="col col-6 sm-center"> + <a + href={constants.ZEROEX_CHAT_URL} + target="_blank" + className="text-decoration-none" + > + <RaisedButton + style={{borderRadius: 6, minWidth: 150}} + buttonStyle={lightButtonStyle} + labelColor="white" + backgroundColor={CUSTOM_HERO_BACKGROUND_COLOR} + labelStyle={buttonLabelStyle} + label="Join the community" + onClick={_.noop} + /> + </a> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + ); + } + private renderProjects() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + const isMediumScreen = this.state.screenWidth === ScreenWidths.MD; + const projectList = _.map(projects, (project: Project, i: number) => { + const colWidth = isSmallScreen ? 3 : isMediumScreen ? 4 : 2 - (i % 2); + return ( + <div + key={`project-${project.logoFileName}`} + className={`col col-${colWidth} center`} + > + <div> + <a + href={project.projectUrl} + target="_blank" + className="text-decoration-none" + > + <img + src={`/images/landing/project_logos/${project.logoFileName}`} + height={isSmallScreen ? 60 : 92} + /> + </a> + </div> + </div> + ); + }); + const titleStyle: React.CSSProperties = { + fontFamily: 'Roboto Mono', + color: '#A4A4A4', + textTransform: 'uppercase', + fontWeight: 300, + letterSpacing: 3, + }; + return ( + <div + className="clearfix py4" + style={{backgroundColor: CUSTOM_PROJECTS_BACKGROUND_COLOR}} + > + <div className="mx-auto max-width-4 clearfix sm-px3"> + <div + className="h4 pb3 md-pl3 sm-pl2" + style={titleStyle} + > + Projects building on 0x + </div> + <div className="clearfix"> + {projectList} + </div> + <div + className="pt3 mx-auto center" + style={{color: CUSTOM_GRAY_TEXT, fontFamily: 'Roboto Mono', maxWidth: 300, fontSize: 14}} + > + view the{' '} + <Link + to={`${WebsitePaths.Wiki}#List-of-Projects-Using-0x-Protocol`} + className="text-decoration-none underline" + style={{color: CUSTOM_GRAY_TEXT}} + > + full list + </Link> + </div> + </div> + </div> + ); + } + private renderTokenizationSection() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + return ( + <div + className="clearfix lg-py4 md-py4 sm-pb4 sm-pt2" + style={{backgroundColor: CUSTOM_WHITE_BACKGROUND}} + > + <div className="mx-auto max-width-4 py4 clearfix"> + {isSmallScreen && + this.renderTokenCloud() + } + <div className="col lg-col-6 md-col-6 col-12"> + <div className="mx-auto" style={{maxWidth: 385, paddingTop: 7}}> + <div + className="lg-h1 md-h1 sm-h2 sm-center sm-pt3" + style={{fontFamily: 'Roboto Mono'}} + > + The world's value is becoming tokenized + </div> + <div + className="pb2 lg-pt2 md-pt2 sm-pt3 sm-px3 h5 sm-center" + style={{fontFamily: 'Roboto Mono', lineHeight: 1.7}} + > + {isSmallScreen ? + <span> + The Ethereum blockchain is an open, borderless financial system that represents + a wide variety of assets as cryptographic tokens. In the future, most digital + assets and goods will be tokenized. + </span> : + <div> + <div> + The Ethereum blockchain is an open, borderless + financial system that represents + </div> + <div> + a wide variety of assets as cryptographic tokens. + In the future, most digital assets and goods will be tokenized. + </div> + </div> + } + </div> + <div className="flex pt1 sm-px3"> + {this.renderAssetTypes()} + </div> + </div> + </div> + {!isSmallScreen && + this.renderTokenCloud() + } + </div> + </div> + ); + } + private renderProtocolSection() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + return ( + <div + className="clearfix lg-py4 md-py4 sm-pt4" + style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}} + > + <div className="mx-auto max-width-4 lg-py4 md-py4 sm-pt4 clearfix"> + <div className="col lg-col-6 md-col-6 col-12 sm-center"> + <img + src="/images/landing/relayer_diagram.png" + height={isSmallScreen ? 326 : 426} + /> + </div> + <div + className="col lg-col-6 md-col-6 col-12 lg-pr3 md-pr3 sm-mx-auto" + style={{color: CUSTOM_WHITE_TEXT, paddingTop: 8, maxWidth: isSmallScreen ? 'none' : 445}} + > + <div + className="lg-h1 md-h1 sm-h2 pb1 sm-pt3 sm-center" + style={{fontFamily: 'Roboto Mono'}} + > + <div> + Off-chain order relay + </div> + <div> + On-chain settlement + </div> + </div> + <div + className="pb2 pt2 h5 sm-center sm-px3 sm-mx-auto" + style={{fontFamily: 'Roboto Mono', lineHeight: 1.7, fontWeight: 300, maxWidth: 445}} + > + In 0x protocol, orders are transported off-chain, massively reducing gas + costs and eliminating blockchain bloat. Relayers help broadcast orders and + collect a fee each time they facilitate a trade. Anyone can build a relayer. + </div> + <div + className="pt3 sm-mx-auto sm-px3" + style={{color: CUSTOM_GRAY_TEXT, maxWidth: isSmallScreen ? 412 : 'none'}} + > + <div className="flex" style={{fontSize: 18}}> + <div + className="lg-h4 md-h4 sm-h5" + style={{letterSpacing: isSmallScreen ? 1 : 3, fontFamily: 'Roboto Mono'}} + > + RELAYERS BUILDING ON 0X + </div> + <div className="h5" style={{marginLeft: isSmallScreen ? 26 : 49}}> + <Link + to={`${WebsitePaths.Wiki}#List-of-Projects-Using-0x-Protocol`} + className="text-decoration-none underline" + style={{color: CUSTOM_GRAY_TEXT, fontFamily: 'Roboto Mono'}} + > + view all + </Link> + </div> + </div> + <div className="lg-flex md-flex sm-clearfix pt3" style={{opacity: 0.4}}> + <div className="col col-4 sm-center"> + <img + src="/images/landing/ethfinex.png" + style={{height: isSmallScreen ? 85 : 107}} + /> + </div> + <div + className="col col-4 center" + > + <img + src="/images/landing/radar_relay.png" + style={{height: isSmallScreen ? 85 : 107}} + /> + </div> + <div className="col col-4 sm-center" style={{textAlign: 'right'}}> + <img + src="/images/landing/paradex.png" + style={{height: isSmallScreen ? 85 : 107}} + /> + </div> + </div> + </div> + </div> + </div> + </div> + ); + } + private renderBuildingBlocksSection() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + const underlineStyle: React.CSSProperties = { + height: isSmallScreen ? 18 : 23, + lineHeight: 'none', + borderBottom: '2px solid #979797', + }; + const descriptionStyle: React.CSSProperties = { + fontFamily: 'Roboto Mono', + lineHeight: isSmallScreen ? 1.5 : 2, + fontWeight: 300, + fontSize: 15, + maxWidth: isSmallScreen ? 375 : 'none', + }; + const callToActionStyle: React.CSSProperties = { + fontFamily: 'Roboto Mono', + fontSize: 15, + fontWeight: 300, + maxWidth: isSmallScreen ? 375 : 441, + }; + return ( + <div + className="clearfix lg-pt4 md-pt4" + style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}} + > + <div className="mx-auto max-width-4 lg-pt4 md-pt4 lg-mb4 md-mb4 sm-mb2 clearfix"> + {isSmallScreen && + this.renderBlockChipImage() + } + <div + className="col lg-col-6 md-col-6 col-12 lg-pr3 md-pr3 sm-px3" + style={{color: CUSTOM_WHITE_TEXT}} + > + <div + className="pb1 lg-pt4 md-pt4 sm-pt3 lg-h1 md-h1 sm-h2 sm-px3 sm-center" + style={{fontFamily: 'Roboto Mono'}} + > + A building block for dApps + </div> + <div + className="pb3 pt2 sm-mx-auto sm-center" + style={descriptionStyle} + > + 0x protocol is a pluggable building block for dApps that require exchange functionality. + Join the many developers that are already using 0x in their web applications and smart + contracts. + </div> + <div + className="sm-mx-auto sm-center" + style={callToActionStyle} + > + Learn how in our{' '} + <Link + to={WebsitePaths.ZeroExJs} + className="text-decoration-none underline" + style={{color: CUSTOM_WHITE_TEXT, fontFamily: 'Roboto Mono'}} + > + 0x.js + </Link> + {' '}and{' '} + <Link + to={WebsitePaths.SmartContracts} + className="text-decoration-none underline" + style={{color: CUSTOM_WHITE_TEXT, fontFamily: 'Roboto Mono'}} + > + smart contract + </Link> + {' '}docs + </div> + </div> + {!isSmallScreen && + this.renderBlockChipImage() + } + </div> + </div> + ); + } + private renderBlockChipImage() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + return ( + <div className="col lg-col-6 md-col-6 col-12 sm-center"> + <img + src="/images/landing/0x_chips.png" + height={isSmallScreen ? 240 : 368} + /> + </div> + ); + } + private renderTokenCloud() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + return ( + <div className="col lg-col-6 md-col-6 col-12 center"> + <img + src="/images/landing/tokenized_world.png" + height={isSmallScreen ? 280 : 364.5} + /> + </div> + ); + } + private renderAssetTypes() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + const assetTypes: AssetType[] = [ + { + title: 'Currency', + imageUrl: '/images/landing/currency.png', + }, + { + title: 'Traditional assets', + imageUrl: '/images/landing/stocks.png', + style: {paddingLeft: isSmallScreen ? 41 : 56, paddingRight: isSmallScreen ? 41 : 56}, + }, + { + title: 'Digital goods', + imageUrl: '/images/landing/digital_goods.png', + }, + ]; + const assets = _.map(assetTypes, (assetType: AssetType) => { + const style = _.isUndefined(assetType.style) ? {} : assetType.style; + return ( + <div + key={`asset-${assetType.title}`} + className="center" + style={{opacity: 0.8, ...style}} + > + <div> + <img + src={assetType.imageUrl} + height="80" + /> + </div> + <div style={{fontFamily: 'Roboto Mono', fontSize: 13.5, fontWeight: 400, opacity: 0.75}}> + {assetType.title} + </div> + </div> + ); + }); + return assets; + } + private renderLink(label: string, path: string, color: string, style?: React.CSSProperties) { + return ( + <div + style={{borderBottom: `1px solid ${color}`, paddingBottom: 1, height: 20, lineHeight: 1.7, ...style}} + > + <Link + to={path} + className="text-decoration-none" + style={{color, fontFamily: 'Roboto Mono'}} + > + {label} + </Link> + </div> + ); + } + private renderInfoBoxes() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + const boxStyle: React.CSSProperties = { + maxWidth: 252, + height: 386, + backgroundColor: '#F9F9F9', + borderRadius: 5, + padding: '10px 24px 24px', + }; + const boxes = _.map(boxContents, (boxContent: BoxContent) => { + return ( + <div + key={`box-${boxContent.title}`} + className="col lg-col-4 md-col-4 col-12 sm-pb4" + > + <div + className={`center sm-mx-auto ${!isSmallScreen && boxContent.classNames}`} + style={boxStyle} + > + <div> + <img src={boxContent.imageUrl} style={{height: 210}} /> + </div> + <div + className="h3" + style={{color: 'black', fontFamily: 'Roboto Mono'}} + > + {boxContent.title} + </div> + <div + className="pt2 pb2" + style={{fontFamily: 'Roboto Mono', fontSize: 14}} + > + {boxContent.description} + </div> + </div> + </div> + ); + + }); + return ( + <div + className="clearfix" + style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}} + > + <div + className="mx-auto py4 sm-mt2 clearfix" + style={{maxWidth: '60em'}} + > + {boxes} + </div> + </div> + ); + } + private renderUseCases() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + const isMediumScreen = this.state.screenWidth === ScreenWidths.MD; + + const useCases: UseCase[] = [ + { + imageUrl: '/images/landing/governance_icon.png', + type: 'Decentralized governance', + description: 'Decentralized organizations use tokens to represent ownership and \ + guide their governance logic. 0x allows decentralized organizations \ + to seamlessly and safely trade ownership for startup capital.', + projectIconUrls: ['/images/landing/aragon.png'], + classNames: 'lg-px2 md-px2', + }, + { + imageUrl: '/images/landing/prediction_market_icon.png', + type: 'Prediction markets', + description: 'Decentralized prediction market platforms generate sets of tokens that \ + represent a financial stake in the outcomes of real-world events. 0x allows \ + these tokens to be instantly tradable.', + projectIconUrls: ['/images/landing/augur.png'], + classNames: 'lg-px2 md-px2', + }, + { + imageUrl: '/images/landing/stable_tokens_icon.png', + type: 'Stable tokens', + description: 'Novel economic constructs such as stable coins require efficient, liquid \ + markets to succeed. 0x will facilitate the underlying economic mechanisms \ + that allow these tokens to remain stable.', + projectIconUrls: ['/images/landing/maker.png'], + classNames: 'lg-px2 md-px2', + }, + { + imageUrl: '/images/landing/loans_icon.png', + type: 'Decentralized loans', + description: 'Efficient lending requires liquid markets where investors can buy and re-sell loans. \ + 0x enables an ecosystem of lenders to self-organize and efficiently determine \ + market prices for all outstanding loans.', + projectIconUrls: ['/images/landing/dharma.png', '/images/landing/lendroid.png'], + classNames: 'lg-pr2 md-pr2 lg-col-6 md-col-6', + style: {width: 291, float: 'right', marginTop: !isSmallScreen ? 38 : 0}, + }, + { + imageUrl: '/images/landing/fund_management_icon.png', + type: 'Fund management', + description: 'Decentralized fund management limits fund managers to investing in pre-agreed \ + upon asset classes. Embedding 0x into fund management smart contracts enables \ + them to enforce these security constraints.', + projectIconUrls: ['/images/landing/melonport.png'], + classNames: 'lg-pl2 md-pl2 lg-col-6 md-col-6', + style: {width: 291, marginTop: !isSmallScreen ? 38 : 0}, + }, + ]; + + const cases = _.map(useCases, (useCase: UseCase) => { + const style = _.isUndefined(useCase.style) || isSmallScreen ? {} : useCase.style; + const useCaseBoxStyle = { + color: '#A2A2A2', + border: '1px solid #565656', + borderRadius: 4, + maxWidth: isSmallScreen ? 375 : 'none', + ...style, + }; + const typeStyle: React.CSSProperties = { + color: '#EBEBEB', + fontSize: 13, + textTransform: 'uppercase', + fontFamily: 'Roboto Mono', + fontWeight: 300, + }; + return ( + <div + key={`useCase-${useCase.type}`} + className={`col lg-col-4 md-col-4 col-12 sm-pt3 sm-px3 sm-pb3 ${useCase.classNames}`} + > + <div + className="relative p2 pb2 sm-mx-auto" + style={useCaseBoxStyle} + > + <div + className="absolute center" + style={{top: -35, width: 'calc(100% - 32px)'}} + > + <img src={useCase.imageUrl} style={{height: 50}} /> + </div> + <div className="pt2 center" style={typeStyle}> + {useCase.type} + </div> + <div + className="pt2" + style={{lineHeight: 1.5, fontSize: 14, overflow: 'hidden', height: 104}} + > + {useCase.description} + </div> + </div> + </div> + ); + }); + return ( + <div + className="clearfix pb4 lg-pt2 md-pt2 sm-pt4" + style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}} + > + <div + className="mx-auto pb4 pt3 mt1 sm-mt2 clearfix" + style={{maxWidth: '67em'}} + > + {cases} + </div> + </div> + ); + } + private renderCallToAction() { + const isSmallScreen = this.state.screenWidth === ScreenWidths.SM; + const buttonLabelStyle: React.CSSProperties = { + textTransform: 'none', + fontSize: 15, + fontWeight: 400, + }; + const lightButtonStyle: React.CSSProperties = { + borderRadius: 6, + border: '1px solid #a0a0a0', + lineHeight: '33px', + height: 49, + }; + const callToActionClassNames = 'col lg-col-8 md-col-8 col-12 lg-pr3 md-pr3 \ + lg-right-align md-right-align sm-center sm-px3 h4'; + return ( + <div + className="clearfix pb4" + style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}} + > + <div + className="mx-auto max-width-4 pb4 mb3 clearfix" + > + <div + className={callToActionClassNames} + style={{fontFamily: 'Roboto Mono', color: 'white', lineHeight: isSmallScreen ? 1.7 : 3}} + > + Get started on building the decentralized future + </div> + <div className="col lg-col-4 md-col-4 col-12 sm-center sm-pt2"> + <Link to={WebsitePaths.ZeroExJs} className="text-decoration-none"> + <RaisedButton + style={{borderRadius: 6, minWidth: 150}} + buttonStyle={lightButtonStyle} + labelColor={colors.white} + backgroundColor={CUSTOM_HERO_BACKGROUND_COLOR} + labelStyle={buttonLabelStyle} + label="Build on 0x" + onClick={_.noop} + /> + </Link> + </div> + </div> + </div> + ); + } + private updateScreenWidth() { + const newScreenWidth = utils.getScreenWidth(); + if (newScreenWidth !== this.state.screenWidth) { + this.setState({ + screenWidth: newScreenWidth, + }); + } + } +} diff --git a/packages/website/ts/pages/not_found.tsx b/packages/website/ts/pages/not_found.tsx new file mode 100644 index 000000000..ddd720c97 --- /dev/null +++ b/packages/website/ts/pages/not_found.tsx @@ -0,0 +1,46 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {Styles} from 'ts/types'; +import {Link} from 'react-router-dom'; +import {Footer} from 'ts/components/footer'; +import {TopBar} from 'ts/components/top_bar'; + +export interface NotFoundProps { + location: Location; +} + +interface NotFoundState {} + +const styles: Styles = { + thin: { + fontWeight: 100, + }, +}; + +export class NotFound extends React.Component<NotFoundProps, NotFoundState> { + public render() { + return ( + <div> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + /> + <div className="mx-auto max-width-4 py4"> + <div className="center py4"> + <div className="py4"> + <div className="py4"> + <h1 style={{...styles.thin}}>404 Not Found</h1> + <div className="py1"> + <div className="py3"> + Hm... looks like we couldn't find what you are looking for. + </div> + </div> + </div> + </div> + </div> + </div> + <Footer location={this.props.location} /> + </div> + ); + } +} diff --git a/packages/website/ts/pages/shared/anchor_title.tsx b/packages/website/ts/pages/shared/anchor_title.tsx new file mode 100644 index 000000000..dfa9401ae --- /dev/null +++ b/packages/website/ts/pages/shared/anchor_title.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import {Styles, HeaderSizes} from 'ts/types'; +import {utils} from 'ts/utils/utils'; +import {constants} from 'ts/utils/constants'; +import {Link as ScrollLink} from 'react-scroll'; + +const headerSizeToScrollOffset: {[headerSize: string]: number} = { + h2: -20, + h3: 0, +}; + +interface AnchorTitleProps { + title: string|React.ReactNode; + id: string; + headerSize: HeaderSizes; + shouldShowAnchor: boolean; +} + +interface AnchorTitleState { + isHovering: boolean; +} + +const styles: Styles = { + anchor: { + fontSize: 20, + transform: 'rotate(45deg)', + cursor: 'pointer', + }, + headers: { + WebkitMarginStart: 0, + WebkitMarginEnd: 0, + fontWeight: 'bold', + display: 'block', + }, + h1: { + fontSize: '1.8em', + WebkitMarginBefore: '0.83em', + WebkitMarginAfter: '0.83em', + }, + h2: { + fontSize: '1.5em', + WebkitMarginBefore: '0.83em', + WebkitMarginAfter: '0.83em', + }, + h3: { + fontSize: '1.17em', + WebkitMarginBefore: '1em', + WebkitMarginAfter: '1em', + }, +}; + +export class AnchorTitle extends React.Component<AnchorTitleProps, AnchorTitleState> { + constructor(props: AnchorTitleProps) { + super(props); + this.state = { + isHovering: false, + }; + } + public render() { + let opacity = 0; + if (this.props.shouldShowAnchor) { + if (this.state.isHovering) { + opacity = 0.6; + } else { + opacity = 1; + } + } + return ( + <div className="relative flex" style={{...styles[this.props.headerSize], ...styles.headers}}> + <div + className="inline-block" + style={{paddingRight: 4}} + > + {this.props.title} + </div> + <ScrollLink + to={this.props.id} + offset={headerSizeToScrollOffset[this.props.headerSize]} + duration={constants.DOCS_SCROLL_DURATION_MS} + containerId={constants.DOCS_CONTAINER_ID} + > + <i + className="zmdi zmdi-link" + onClick={utils.setUrlHash.bind(utils, this.props.id)} + style={{...styles.anchor, opacity}} + onMouseOver={this.setHoverState.bind(this, true)} + onMouseOut={this.setHoverState.bind(this, false)} + /> + </ScrollLink> + </div> + ); + } + private setHoverState(isHovering: boolean) { + this.setState({ + isHovering, + }); + } +} diff --git a/packages/website/ts/pages/shared/markdown_code_block.tsx b/packages/website/ts/pages/shared/markdown_code_block.tsx new file mode 100644 index 000000000..621e5b606 --- /dev/null +++ b/packages/website/ts/pages/shared/markdown_code_block.tsx @@ -0,0 +1,20 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as HighLight from 'react-highlight'; + +interface MarkdownCodeBlockProps { + literal: string; + language: string; +} + +export function MarkdownCodeBlock(props: MarkdownCodeBlockProps) { + return ( + <span style={{fontSize: 16}}> + <HighLight + className={props.language || 'js'} + > + {props.literal} + </HighLight> + </span> + ); +} diff --git a/packages/website/ts/pages/shared/markdown_section.tsx b/packages/website/ts/pages/shared/markdown_section.tsx new file mode 100644 index 000000000..32b55abc8 --- /dev/null +++ b/packages/website/ts/pages/shared/markdown_section.tsx @@ -0,0 +1,77 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as ReactMarkdown from 'react-markdown'; +import {Element as ScrollElement} from 'react-scroll'; +import {AnchorTitle} from 'ts/pages/shared/anchor_title'; +import {utils} from 'ts/utils/utils'; +import {MarkdownCodeBlock} from 'ts/pages/shared/markdown_code_block'; +import RaisedButton from 'material-ui/RaisedButton'; +import {HeaderSizes} from 'ts/types'; + +interface MarkdownSectionProps { + sectionName: string; + markdownContent: string; + headerSize?: HeaderSizes; + githubLink?: string; +} + +interface MarkdownSectionState { + shouldShowAnchor: boolean; +} + +export class MarkdownSection extends React.Component<MarkdownSectionProps, MarkdownSectionState> { + public static defaultProps: Partial<MarkdownSectionProps> = { + headerSize: HeaderSizes.H3, + }; + constructor(props: MarkdownSectionProps) { + super(props); + this.state = { + shouldShowAnchor: false, + }; + } + public render() { + const sectionName = this.props.sectionName; + const id = utils.getIdFromName(sectionName); + return ( + <div + className="pt2 pr3 md-pl2 sm-pl3 overflow-hidden" + onMouseOver={this.setAnchorVisibility.bind(this, true)} + onMouseOut={this.setAnchorVisibility.bind(this, false)} + > + <ScrollElement name={id}> + <div className="clearfix"> + <div className="col lg-col-8 md-col-8 sm-col-12"> + <span style={{textTransform: 'capitalize'}}> + <AnchorTitle + headerSize={this.props.headerSize} + title={sectionName} + id={id} + shouldShowAnchor={this.state.shouldShowAnchor} + /> + </span> + </div> + <div className="col col-4 sm-hide xs-hide py2 right-align"> + {!_.isUndefined(this.props.githubLink) && + <RaisedButton + href={this.props.githubLink} + target="_blank" + label="Edit on Github" + icon={<i className="zmdi zmdi-github" style={{fontSize: 23}} />} + /> + } + </div> + </div> + <ReactMarkdown + source={this.props.markdownContent} + renderers={{CodeBlock: MarkdownCodeBlock}} + /> + </ScrollElement> + </div> + ); + } + private setAnchorVisibility(shouldShowAnchor: boolean) { + this.setState({ + shouldShowAnchor, + }); + } +} diff --git a/packages/website/ts/pages/shared/nested_sidebar_menu.tsx b/packages/website/ts/pages/shared/nested_sidebar_menu.tsx new file mode 100644 index 000000000..e69506bb8 --- /dev/null +++ b/packages/website/ts/pages/shared/nested_sidebar_menu.tsx @@ -0,0 +1,163 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import MenuItem from 'material-ui/MenuItem'; +import {colors} from 'material-ui/styles'; +import {utils} from 'ts/utils/utils'; +import {constants} from 'ts/utils/constants'; +import {VersionDropDown} from 'ts/pages/shared/version_drop_down'; +import {ZeroExJsDocSections, Styles, MenuSubsectionsBySection, Docs} from 'ts/types'; +import {typeDocUtils} from 'ts/utils/typedoc_utils'; +import {Link as ScrollLink} from 'react-scroll'; + +interface NestedSidebarMenuProps { + topLevelMenu: {[topLevel: string]: string[]}; + menuSubsectionsBySection: MenuSubsectionsBySection; + shouldDisplaySectionHeaders?: boolean; + onMenuItemClick?: () => void; + selectedVersion?: string; + versions?: string[]; + doc?: Docs; + isSectionHeaderClickable?: boolean; +} + +interface NestedSidebarMenuState {} + +const styles: Styles = { + menuItemWithHeaders: { + minHeight: 0, + }, + menuItemWithoutHeaders: { + minHeight: 48, + }, + menuItemInnerDivWithHeaders: { + lineHeight: 2, + }, +}; + +export class NestedSidebarMenu extends React.Component<NestedSidebarMenuProps, NestedSidebarMenuState> { + public static defaultProps: Partial<NestedSidebarMenuProps> = { + shouldDisplaySectionHeaders: true, + onMenuItemClick: _.noop, + }; + public render() { + const navigation = _.map(this.props.topLevelMenu, (menuItems: string[], sectionName: string) => { + const finalSectionName = sectionName.replace(/-/g, ' '); + if (this.props.shouldDisplaySectionHeaders) { + const id = utils.getIdFromName(sectionName); + return ( + <div + key={`section-${sectionName}`} + className="py1" + > + <ScrollLink + to={id} + offset={-20} + duration={constants.DOCS_SCROLL_DURATION_MS} + containerId={constants.DOCS_CONTAINER_ID} + > + <div + style={{color: colors.grey500, cursor: 'pointer'}} + className="pb1" + > + {finalSectionName.toUpperCase()} + </div> + </ScrollLink> + {this.renderMenuItems(menuItems)} + </div> + ); + } else { + return ( + <div key={`section-${sectionName}`} > + {this.renderMenuItems(menuItems)} + </div> + ); + } + }); + return ( + <div> + {!_.isUndefined(this.props.versions) && + !_.isUndefined(this.props.selectedVersion) && + !_.isUndefined(this.props.doc) && + <VersionDropDown + selectedVersion={this.props.selectedVersion} + versions={this.props.versions} + doc={this.props.doc} + /> + } + {navigation} + </div> + ); + } + private renderMenuItems(menuItemNames: string[]): React.ReactNode[] { + const menuItemStyles = this.props.shouldDisplaySectionHeaders ? + styles.menuItemWithHeaders : + styles.menuItemWithoutHeaders; + const menuItemInnerDivStyles = this.props.shouldDisplaySectionHeaders ? + styles.menuItemInnerDivWithHeaders : {}; + const menuItems = _.map(menuItemNames, menuItemName => { + const id = utils.getIdFromName(menuItemName); + return ( + <div key={menuItemName}> + <ScrollLink + key={`menuItem-${menuItemName}`} + to={id} + offset={-10} + duration={constants.DOCS_SCROLL_DURATION_MS} + containerId={constants.DOCS_CONTAINER_ID} + > + <MenuItem + onTouchTap={this.onMenuItemClick.bind(this, menuItemName)} + style={menuItemStyles} + innerDivStyle={menuItemInnerDivStyles} + > + <span style={{textTransform: 'capitalize'}}> + {menuItemName} + </span> + </MenuItem> + </ScrollLink> + {this.renderMenuItemSubsections(menuItemName)} + </div> + ); + }); + return menuItems; + } + private renderMenuItemSubsections(menuItemName: string): React.ReactNode { + if (_.isUndefined(this.props.menuSubsectionsBySection[menuItemName])) { + return null; + } + return this.renderMenuSubsectionsBySection(menuItemName, this.props.menuSubsectionsBySection[menuItemName]); + } + private renderMenuSubsectionsBySection(menuItemName: string, entityNames: string[]): React.ReactNode { + return ( + <ul style={{margin: 0, listStyleType: 'none', paddingLeft: 0}} key={menuItemName}> + {_.map(entityNames, entityName => { + const id = utils.getIdFromName(entityName); + return ( + <li key={`menuItem-${entityName}`}> + <ScrollLink + to={id} + offset={0} + duration={constants.DOCS_SCROLL_DURATION_MS} + containerId={constants.DOCS_CONTAINER_ID} + onTouchTap={this.onMenuItemClick.bind(this, entityName)} + > + <MenuItem + onTouchTap={this.onMenuItemClick.bind(this, menuItemName)} + style={{minHeight: 35}} + innerDivStyle={{paddingLeft: 36, fontSize: 14, lineHeight: '35px'}} + > + {entityName} + </MenuItem> + </ScrollLink> + </li> + ); + })} + </ul> + ); + } + private onMenuItemClick(menuItemName: string): void { + const id = utils.getIdFromName(menuItemName); + utils.setUrlHash(id); + this.props.onMenuItemClick(); + } +} diff --git a/packages/website/ts/pages/shared/section_header.tsx b/packages/website/ts/pages/shared/section_header.tsx new file mode 100644 index 000000000..5937be13b --- /dev/null +++ b/packages/website/ts/pages/shared/section_header.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import {Element as ScrollElement} from 'react-scroll'; +import {AnchorTitle} from 'ts/pages/shared/anchor_title'; +import {utils} from 'ts/utils/utils'; +import {HeaderSizes} from 'ts/types'; + +interface SectionHeaderProps { + sectionName: string; + headerSize?: HeaderSizes; +} + +interface SectionHeaderState { + shouldShowAnchor: boolean; +} + +export class SectionHeader extends React.Component<SectionHeaderProps, SectionHeaderState> { + public static defaultProps: Partial<SectionHeaderProps> = { + headerSize: HeaderSizes.H2, + }; + constructor(props: SectionHeaderProps) { + super(props); + this.state = { + shouldShowAnchor: false, + }; + } + public render() { + const sectionName = this.props.sectionName.replace(/-/g, ' '); + const id = utils.getIdFromName(sectionName); + return ( + <div + onMouseOver={this.setAnchorVisibility.bind(this, true)} + onMouseOut={this.setAnchorVisibility.bind(this, false)} + > + <ScrollElement name={id}> + <AnchorTitle + headerSize={this.props.headerSize} + title={<span style={{textTransform: 'capitalize'}}>{sectionName}</span>} + id={id} + shouldShowAnchor={this.state.shouldShowAnchor} + /> + </ScrollElement> + </div> + ); + } + private setAnchorVisibility(shouldShowAnchor: boolean) { + this.setState({ + shouldShowAnchor, + }); + } +} diff --git a/packages/website/ts/pages/shared/version_drop_down.tsx b/packages/website/ts/pages/shared/version_drop_down.tsx new file mode 100644 index 000000000..f29547c9c --- /dev/null +++ b/packages/website/ts/pages/shared/version_drop_down.tsx @@ -0,0 +1,46 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import MenuItem from 'material-ui/MenuItem'; +import DropDownMenu from 'material-ui/DropDownMenu'; +import {constants} from 'ts/utils/constants'; +import {Docs} from 'ts/types'; + +interface VersionDropDownProps { + selectedVersion: string; + versions: string[]; + doc: Docs; +} + +interface VersionDropDownState {} + +export class VersionDropDown extends React.Component<VersionDropDownProps, VersionDropDownState> { + public render() { + return ( + <div className="mx-auto" style={{width: 120}}> + <DropDownMenu + maxHeight={300} + value={this.props.selectedVersion} + onChange={this.updateSelectedVersion.bind(this)} + > + {this.renderDropDownItems()} + </DropDownMenu> + </div> + ); + } + private renderDropDownItems() { + const items = _.map(this.props.versions, version => { + return ( + <MenuItem + key={version} + value={version} + primaryText={`v${version}`} + /> + ); + }); + return items; + } + private updateSelectedVersion(e: any, index: number, value: string) { + const docPath = constants.docToPath[this.props.doc]; + window.location.href = `${docPath}/${value}${window.location.hash}`; + } +} diff --git a/packages/website/ts/pages/wiki/wiki.tsx b/packages/website/ts/pages/wiki/wiki.tsx new file mode 100644 index 000000000..0e6fc98ab --- /dev/null +++ b/packages/website/ts/pages/wiki/wiki.tsx @@ -0,0 +1,210 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import DocumentTitle = require('react-document-title'); +import {colors} from 'material-ui/styles'; +import CircularProgress from 'material-ui/CircularProgress'; +import { + scroller, +} from 'react-scroll'; +import {Styles, Article, ArticlesBySection} from 'ts/types'; +import {TopBar} from 'ts/components/top_bar'; +import {HeaderSizes, WebsitePaths} from 'ts/types'; +import {utils} from 'ts/utils/utils'; +import {constants} from 'ts/utils/constants'; +import {configs} from 'ts/utils/configs'; +import {NestedSidebarMenu} from 'ts/pages/shared/nested_sidebar_menu'; +import {SectionHeader} from 'ts/pages/shared/section_header'; +import {MarkdownSection} from 'ts/pages/shared/markdown_section'; + +const WIKI_NOT_READY_BACKOUT_TIMEOUT_MS = 5000; + +export interface WikiProps { + source: string; + location: Location; +} + +interface WikiState { + articlesBySection: ArticlesBySection; +} + +const styles: Styles = { + mainContainers: { + position: 'absolute', + top: 60, + left: 0, + bottom: 0, + right: 0, + overflowZ: 'hidden', + overflowY: 'scroll', + minHeight: 'calc(100vh - 60px)', + WebkitOverflowScrolling: 'touch', + }, + menuContainer: { + borderColor: colors.grey300, + maxWidth: 330, + marginLeft: 20, + }, +}; + +export class Wiki extends React.Component<WikiProps, WikiState> { + private wikiBackoffTimeoutId: number; + constructor(props: WikiProps) { + super(props); + this.state = { + articlesBySection: undefined, + }; + } + public componentWillMount() { + this.fetchArticlesBySectionAsync(); + } + public componentWillUnmount() { + clearTimeout(this.wikiBackoffTimeoutId); + } + public render() { + const menuSubsectionsBySection = _.isUndefined(this.state.articlesBySection) + ? {} + : this.getMenuSubsectionsBySection(this.state.articlesBySection); + return ( + <div> + <DocumentTitle title="0x Protocol Wiki"/> + <TopBar + blockchainIsLoaded={false} + location={this.props.location} + menuSubsectionsBySection={menuSubsectionsBySection} + shouldFullWidth={true} + /> + {_.isUndefined(this.state.articlesBySection) ? + <div + className="col col-12" + style={styles.mainContainers} + > + <div + className="relative sm-px2 sm-pt2 sm-m1" + style={{height: 122, top: '50%', transform: 'translateY(-50%)'}} + > + <div className="center pb2"> + <CircularProgress size={40} thickness={5} /> + </div> + <div className="center pt2" style={{paddingBottom: 11}}>Loading wiki...</div> + </div> + </div> : + <div + className="mx-auto flex" + style={{color: colors.grey800, height: 43}} + > + <div className="relative col md-col-3 lg-col-3 lg-pl0 md-pl1 sm-hide xs-hide"> + <div + className="border-right absolute pt2" + style={{...styles.menuContainer, ...styles.mainContainers}} + > + <NestedSidebarMenu + topLevelMenu={menuSubsectionsBySection} + menuSubsectionsBySection={menuSubsectionsBySection} + isSectionHeaderClickable={true} + /> + </div> + </div> + <div className="relative col lg-col-9 md-col-9 sm-col-12 col-12"> + <div + id="documentation" + style={styles.mainContainers} + className="absolute" + > + <div id="0xProtocolWiki" /> + <h1 className="md-pl2 sm-pl3"> + <a href={constants.GITHUB_WIKI_URL} target="_blank"> + 0x Protocol Wiki + </a> + </h1> + <div id="wiki"> + {this.renderWikiArticles()} + </div> + </div> + </div> + </div> + } + </div> + ); + } + private renderWikiArticles(): React.ReactNode { + const sectionNames = _.keys(this.state.articlesBySection); + const sections = _.map(sectionNames, sectionName => this.renderSection(sectionName)); + return sections; + } + private renderSection(sectionName: string) { + const articles = this.state.articlesBySection[sectionName]; + const renderedArticles = _.map(articles, (article: Article) => { + const githubLink = `${constants.GITHUB_WIKI_URL}/edit/master/${sectionName}/${article.fileName}`; + return ( + <div key={`markdown-section-${article.title}`}> + <MarkdownSection + sectionName={article.title} + markdownContent={article.content} + headerSize={HeaderSizes.H2} + githubLink={githubLink} + /> + <div className="mb4 mt3 p3 center" style={{backgroundColor: '#f9f5ef'}}> + See a way to make this article better?{' '} + <a + href={githubLink} + target="_blank" + > + Edit here → + </a> + </div> + </div> + ); + }); + return ( + <div + key={`section-${sectionName}`} + className="py2 pr3 md-pl2 sm-pl3" + > + <SectionHeader sectionName={sectionName} headerSize={HeaderSizes.H1} /> + {renderedArticles} + </div> + ); + } + private scrollToHash(): void { + const hashWithPrefix = this.props.location.hash; + let hash = hashWithPrefix.slice(1); + if (_.isEmpty(hash)) { + hash = '0xProtocolWiki'; // scroll to the top + } + + scroller.scrollTo(hash, {duration: 0, offset: 0, containerId: 'documentation'}); + } + private async fetchArticlesBySectionAsync(): Promise<void> { + const endpoint = `${configs.BACKEND_BASE_URL}${WebsitePaths.Wiki}`; + const response = await fetch(endpoint); + if (response.status === constants.HTTP_NO_CONTENT_STATUS_CODE) { + // We need to backoff and try fetching again later + this.wikiBackoffTimeoutId = window.setTimeout(() => { + this.fetchArticlesBySectionAsync(); + }, WIKI_NOT_READY_BACKOUT_TIMEOUT_MS); + return; + } + if (response.status !== 200) { + // TODO: Show the user an error message when the wiki fail to load + const errMsg = await response.text(); + utils.consoleLog(`Failed to load wiki: ${response.status} ${errMsg}`); + return; + } + const articlesBySection = await response.json(); + this.setState({ + articlesBySection, + }, () => { + this.scrollToHash(); + }); + } + private getMenuSubsectionsBySection(articlesBySection: ArticlesBySection) { + const sectionNames = _.keys(articlesBySection); + const menuSubsectionsBySection: {[section: string]: string[]} = {}; + for (const sectionName of sectionNames) { + const articles = articlesBySection[sectionName]; + const articleNames = _.map(articles, article => article.title); + menuSubsectionsBySection[sectionName] = articleNames; + } + return menuSubsectionsBySection; + } +} |