1import { css } from '@emotion/core'; 2import { theme } from '@expo/styleguide'; 3import some from 'lodash/some'; 4import Router from 'next/router'; 5import * as React from 'react'; 6 7import * as Utilities from '~/common/utilities'; 8import * as WindowUtils from '~/common/window'; 9import DocumentationFooter from '~/components/DocumentationFooter'; 10import DocumentationHeader from '~/components/DocumentationHeader'; 11import DocumentationNestedScrollLayout from '~/components/DocumentationNestedScrollLayout'; 12import DocumentationPageContext from '~/components/DocumentationPageContext'; 13import DocumentationSidebar from '~/components/DocumentationSidebar'; 14import DocumentationSidebarRight, { 15 SidebarRightComponentType, 16} from '~/components/DocumentationSidebarRight'; 17import Head from '~/components/Head'; 18import { H1 } from '~/components/base/headings'; 19import navigation from '~/constants/navigation'; 20import * as Constants from '~/constants/theme'; 21import { VERSIONS } from '~/constants/versions'; 22import { NavigationRoute, Url } from '~/types/common'; 23 24const STYLES_DOCUMENT = css` 25 background: ${theme.background.default}; 26 margin: 0 auto; 27 padding: 40px 56px; 28 29 hr { 30 border-top: 1px solid ${theme.border.default}; 31 border-bottom: 0px; 32 } 33 34 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 35 padding: 20px 16px 48px 16px; 36 } 37`; 38 39const HIDDEN_ON_MOBILE = css` 40 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 41 display: none; 42 } 43`; 44 45const HIDDEN_ON_DESKTOP = css` 46 @media screen and (min-width: ${Constants.breakpoints.mobile}) { 47 display: none; 48 } 49`; 50 51type Props = { 52 url: Url; 53 title: string; 54 asPath: string; 55 sourceCodeUrl?: string; 56 tocVisible: boolean; 57 /* If the page should not show up in the Algolia Docsearch results */ 58 hideFromSearch?: boolean; 59}; 60 61type State = { 62 isMenuActive: boolean; 63 isMobileSearchActive: boolean; 64}; 65 66export default class DocumentationPage extends React.Component<Props, State> { 67 state = { 68 isMenuActive: false, 69 isMobileSearchActive: false, 70 }; 71 72 private layoutRef = React.createRef<DocumentationNestedScrollLayout>(); 73 private sidebarRightRef = React.createRef<SidebarRightComponentType>(); 74 75 componentDidMount() { 76 Router.events.on('routeChangeStart', () => { 77 if (this.layoutRef.current) { 78 window.__sidebarScroll = this.layoutRef.current.getSidebarScrollTop(); 79 } 80 window.NProgress.start(); 81 }); 82 83 Router.events.on('routeChangeComplete', () => { 84 window.NProgress.done(); 85 }); 86 87 Router.events.on('routeChangeError', () => { 88 window.NProgress.done(); 89 }); 90 91 window.addEventListener('resize', this.handleResize); 92 } 93 94 componentWillUnmount() { 95 window.removeEventListener('resize', this.handleResize); 96 } 97 98 private handleResize = () => { 99 if (WindowUtils.getViewportSize().width >= Constants.breakpoints.mobileValue) { 100 window.scrollTo(0, 0); 101 } 102 }; 103 104 private handleSetVersion = (version: string) => { 105 let newPath = Utilities.replaceVersionInUrl(this.props.url.pathname, version); 106 107 if (!newPath.endsWith('/')) { 108 newPath += '/'; 109 } 110 111 // note: we can do this without validating if the page exists or not. 112 // the error page will redirect users to the versioned-index page when a page doesn't exists. 113 Router.push(newPath); 114 }; 115 116 private handleShowMenu = () => { 117 this.setState({ 118 isMenuActive: true, 119 }); 120 this.handleHideSearch(); 121 }; 122 123 private handleHideMenu = () => { 124 this.setState({ 125 isMenuActive: false, 126 }); 127 }; 128 129 private handleToggleSearch = () => { 130 this.setState(prevState => ({ 131 isMobileSearchActive: !prevState.isMobileSearchActive, 132 })); 133 }; 134 135 private handleHideSearch = () => { 136 this.setState({ 137 isMobileSearchActive: false, 138 }); 139 }; 140 141 private isReferencePath = () => { 142 return this.props.url.pathname.startsWith('/versions'); 143 }; 144 145 private isGeneralPath = () => { 146 return some(navigation.generalDirectories, name => 147 this.props.url.pathname.startsWith(`/${name}`) 148 ); 149 }; 150 151 private isGettingStartedPath = () => { 152 return ( 153 this.props.url.pathname === '/' || 154 some(navigation.startingDirectories, name => this.props.url.pathname.startsWith(`/${name}`)) 155 ); 156 }; 157 158 private isEasPath = () => { 159 return some(navigation.easDirectories, name => this.props.url.pathname.startsWith(`/${name}`)); 160 }; 161 162 private isPreviewPath = () => { 163 return some(navigation.previewDirectories, name => 164 this.props.url.pathname.startsWith(`/${name}`) 165 ); 166 }; 167 168 private getCanonicalUrl = () => { 169 if (this.isReferencePath()) { 170 return `https://docs.expo.io${Utilities.replaceVersionInUrl( 171 this.props.url.pathname, 172 'latest' 173 )}`; 174 } else { 175 return `https://docs.expo.io/${this.props.url.pathname}`; 176 } 177 }; 178 179 private getAlgoliaTag = () => { 180 if (this.props.hideFromSearch === true) { 181 return null; 182 } 183 184 return this.isReferencePath() ? this.getVersion() : 'none'; 185 }; 186 187 private getVersion = () => { 188 let version = (this.props.asPath || this.props.url.pathname).split(`/`)[2]; 189 if (!version || !VERSIONS.includes(version)) { 190 version = 'latest'; 191 } 192 return version; 193 }; 194 195 private getRoutes = (): NavigationRoute[] => { 196 if (this.isReferencePath()) { 197 const version = this.getVersion(); 198 return navigation.reference[version]; 199 } else { 200 return navigation[this.getActiveTopLevelSection()]; 201 } 202 }; 203 204 private getActiveTopLevelSection = () => { 205 if (this.isReferencePath()) { 206 return 'reference'; 207 } else if (this.isGeneralPath()) { 208 return 'general'; 209 } else if (this.isGettingStartedPath()) { 210 return 'starting'; 211 } else if (this.isEasPath()) { 212 return 'eas'; 213 } else if (this.isPreviewPath()) { 214 return 'preview'; 215 } 216 217 return 'general'; 218 }; 219 220 render() { 221 const sidebarScrollPosition = process.browser ? window.__sidebarScroll : 0; 222 223 const version = this.getVersion(); 224 const routes = this.getRoutes(); 225 226 const isReferencePath = this.isReferencePath(); 227 228 const headerElement = ( 229 <DocumentationHeader 230 activeSection={this.getActiveTopLevelSection()} 231 version={version} 232 isMenuActive={this.state.isMenuActive} 233 isMobileSearchActive={this.state.isMobileSearchActive} 234 isAlgoliaSearchHidden={this.state.isMenuActive} 235 onShowMenu={this.handleShowMenu} 236 onHideMenu={this.handleHideMenu} 237 onToggleSearch={this.handleToggleSearch} 238 /> 239 ); 240 241 const sidebarElement = ( 242 <DocumentationSidebar 243 url={this.props.url} 244 asPath={this.props.asPath} 245 routes={routes} 246 version={version} 247 onSetVersion={this.handleSetVersion} 248 isVersionSelectorHidden={!isReferencePath} 249 /> 250 ); 251 252 const handleContentScroll = (contentScrollPosition: number) => { 253 window.requestAnimationFrame(() => { 254 if (this.sidebarRightRef && this.sidebarRightRef.current) { 255 this.sidebarRightRef.current.handleContentScroll(contentScrollPosition); 256 } 257 }); 258 }; 259 260 const sidebarRight = <DocumentationSidebarRight ref={this.sidebarRightRef} />; 261 262 const algoliaTag = this.getAlgoliaTag(); 263 264 return ( 265 <DocumentationNestedScrollLayout 266 ref={this.layoutRef} 267 header={headerElement} 268 sidebar={sidebarElement} 269 sidebarRight={sidebarRight} 270 tocVisible={this.props.tocVisible} 271 isMenuActive={this.state.isMenuActive} 272 isMobileSearchActive={this.state.isMobileSearchActive} 273 onContentScroll={handleContentScroll} 274 sidebarScrollPosition={sidebarScrollPosition}> 275 <Head title={`${this.props.title} - Expo Documentation`}> 276 {algoliaTag !== null && <meta name="docsearch:version" content={algoliaTag} />} 277 <meta property="og:title" content={`${this.props.title} - Expo Documentation`} /> 278 <meta property="og:type" content="website" /> 279 <meta property="og:image" content="https://docs.expo.io/static/images/og.png" /> 280 <meta property="og:image:url" content="https://docs.expo.io/static/images/og.png" /> 281 <meta 282 property="og:image:secure_url" 283 content="https://docs.expo.io/static/images/og.png" 284 /> 285 <meta property="og:locale" content="en_US" /> 286 <meta property="og:site_name" content="Expo Documentation" /> 287 <meta 288 property="og:description" 289 content="Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React." 290 /> 291 292 <meta name="twitter:site" content="@expo" /> 293 <meta name="twitter:card" content="summary" /> 294 <meta property="twitter:title" content={`${this.props.title} - Expo Documentation`} /> 295 <meta 296 name="twitter:description" 297 content="Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React." 298 /> 299 <meta property="twitter:image" content="https://docs.expo.io/static/images/twitter.png" /> 300 301 {(version === 'unversioned' || this.isPreviewPath()) && ( 302 <meta name="robots" content="noindex" /> 303 )} 304 {version !== 'unversioned' && <link rel="canonical" href={this.getCanonicalUrl()} />} 305 </Head> 306 307 {!this.state.isMenuActive ? ( 308 <div css={STYLES_DOCUMENT}> 309 <H1>{this.props.title}</H1> 310 <DocumentationPageContext.Provider value={{ version }}> 311 {this.props.children} 312 </DocumentationPageContext.Provider> 313 <DocumentationFooter 314 title={this.props.title} 315 url={this.props.url} 316 asPath={this.props.asPath} 317 sourceCodeUrl={this.props.sourceCodeUrl} 318 /> 319 </div> 320 ) : ( 321 <div> 322 <div css={[STYLES_DOCUMENT, HIDDEN_ON_MOBILE]}> 323 <H1>{this.props.title}</H1> 324 <DocumentationPageContext.Provider value={{ version }}> 325 {this.props.children} 326 </DocumentationPageContext.Provider> 327 <DocumentationFooter 328 title={this.props.title} 329 asPath={this.props.asPath} 330 sourceCodeUrl={this.props.sourceCodeUrl} 331 /> 332 </div> 333 <div css={HIDDEN_ON_DESKTOP}> 334 <DocumentationSidebar 335 url={this.props.url} 336 asPath={this.props.asPath} 337 routes={routes} 338 version={version} 339 onSetVersion={this.handleSetVersion} 340 isVersionSelectorHidden={!isReferencePath} 341 /> 342 </div> 343 </div> 344 )} 345 </DocumentationNestedScrollLayout> 346 ); 347 } 348} 349