Skip to content

Commit

Permalink
Merge pull request #834 from iterative/docs-ssr
Browse files Browse the repository at this point in the history
Docs SSR
  • Loading branch information
shcheklein authored Dec 9, 2019
2 parents eb0da40 + b307efa commit 43170d3
Show file tree
Hide file tree
Showing 16 changed files with 413 additions and 536 deletions.
17 changes: 17 additions & 0 deletions pages/_document.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ export default class Page extends Document {
type="text/css"
href="/static/fonts/fonts.css"
/>
<link
rel="stylesheet"
type="text/css"
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/3.0.1/github-markdown.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/docsearch.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/css/perfect-scrollbar.min.css"
/>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/docsearch.min.js"
/>
{this.props.styleTags}
</Head>
<body>
Expand Down
312 changes: 122 additions & 190 deletions pages/doc.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
/* global docsearch:readonly */

import React, { Component } from 'react'
import React, { useCallback, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import Error from 'next/error'
import Router from 'next/router'
// components
import Page from '../src/Page'
import { HeadInjector } from '../src/Documentation/HeadInjector'
import Hamburger from '../src/Hamburger'
import SearchForm from '../src/SearchForm'
import SidebarMenu from '../src/Documentation/SidebarMenu/SidebarMenu'
import Loader from '../src/Loader/Loader'
import Page404 from '../src/Page404'
import Markdown from '../src/Documentation/Markdown/Markdown'
import RightPanel from '../src/Documentation/RightPanel/RightPanel'
// utils
import fetch from 'isomorphic-fetch'
import kebabCase from 'lodash.kebabcase'
// constants
import { HEADER } from '../src/consts'
// sidebar data and helpers
import sidebar, { getItemByPath } from '../src/Documentation/SidebarMenu/helper'
// styles
Expand All @@ -25,216 +24,149 @@ import { media } from '../src/styles'
const ROOT_ELEMENT = 'bodybag'
const SIDEBAR_MENU = 'sidebar-menu'

export default class Documentation extends Component {
constructor() {
super()
this.state = {
currentItem: {},
headings: [],
isLoading: false,
isMenuOpen: false,
isSmoothScrollEnabled: true,
search: false,
markdown: '',
pageNotFound: false
}
const parseHeadings = text => {
const headingRegex = /\n(## \s*)(.*)/g
const matches = []
let match
do {
match = headingRegex.exec(text)
if (match)
matches.push({
text: match[2],
slug: kebabCase(match[2])
})
} while (match)

return matches
}

export default function Documentation({ item, headings, markdown, errorCode }) {
if (errorCode) {
return <Error statusCode={errorCode} />
}

componentDidMount() {
this.loadStateFromURL()
const { source, path, label, next, prev, tutorials } = item

const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isSearchAvaible, setIsSearchAvaible] = useState(false)

const toggleMenu = useCallback(() => setIsMenuOpen(!isMenuOpen), [isMenuOpen])

useEffect(() => {
try {
docsearch
this.setState(
{
search: true
},
() => {
this.initDocsearch()
}
)

setIsSearchAvaible(true)

if (isSearchAvaible) {
docsearch({
apiKey: '755929839e113a981f481601c4f52082',
indexName: 'dvc',
inputSelector: '#doc-search',
debug: false // Set debug to true if you want to inspect the dropdown
})
}
} catch (ReferenceError) {
this.setState({
search: false
})
// nothing there
}
}, [isSearchAvaible])

window.addEventListener('popstate', this.loadStateFromURL)
}
useEffect(() => {
const handleRouteChange = () => {
const rootElement = document.getElementById(ROOT_ELEMENT)
if (rootElement) {
rootElement.scrollTop = 0
}
}

componentWillUnmount() {
window.removeEventListener('popstate', this.loadStateFromURL)
}
Router.events.on('routeChangeComplete', handleRouteChange)

initDocsearch = () => {
docsearch({
apiKey: '755929839e113a981f481601c4f52082',
indexName: 'dvc',
inputSelector: '#doc-search',
debug: false // Set debug to true if you want to inspect the dropdown
})
}
return () => Router.events.off('routeChangeComplete', handleRouteChange)
}, [])

onNavigate = (path, e) => {
if (e && (e.ctrlKey || e.metaKey)) return
const githubLink = `https://github.com/iterative/dvc.org/blob/master${source}`

if (e) e.preventDefault()
return (
<Page stickHeader={true}>
<HeadInjector sectionName={label} />
<Container>
<Backdrop onClick={toggleMenu} visible={isMenuOpen} />

window.history.pushState(null, null, path)
this.loadPath(path)
}
<SideToggle onClick={toggleMenu} isMenuOpen={isMenuOpen}>
<Hamburger />
</SideToggle>

loadStateFromURL = () => this.loadPath(window.location.pathname)

loadPath = path => {
const { currentItem } = this.state
const item = getItemByPath(path)
const isPageChanged = currentItem !== item
const isFirstPage = !currentItem.path

if (!item) {
this.setState({ pageNotFound: true, currentItem: {} })
} else if (!isFirstPage && !isPageChanged) {
this.updateScroll(isPageChanged)
} else {
this.setState({ isLoading: true, headings: [] })
fetch(item.source)
.then(res => {
res.text().then(text => {
this.setState(
{
currentItem: item,
headings: this.parseHeadings(text),
isLoading: false,
isMenuOpen: false,
markdown: text,
pageNotFound: false
},
() => this.updateScroll(!isFirstPage && isPageChanged)
)
})
})
.catch(() => {
window.location.reload()
})
}
}
<Side isOpen={isMenuOpen}>
{isSearchAvaible && (
<SearchArea>
<SearchForm />
</SearchArea>
)}

<SidebarMenu
sidebar={sidebar}
currentPath={path}
id={SIDEBAR_MENU}
onClick={toggleMenu}
/>
</Side>

<Markdown
markdown={markdown}
githubLink={githubLink}
prev={prev}
next={next}
tutorials={tutorials}
/>

<RightPanel
headings={headings}
githubLink={githubLink}
tutorials={tutorials}
/>
</Container>
</Page>
)
}

updateScroll(isPageChanged) {
const { hash } = window.location
Documentation.getInitialProps = async ({ asPath, req }) => {
const item = getItemByPath(asPath)

if (isPageChanged) {
this.setState({ isSmoothScrollEnabled: false }, () => {
this.scrollTop()
this.setState({ isSmoothScrollEnabled: true }, () => {
if (hash) this.scrollToLink(hash)
})
})
} else if (hash) {
this.scrollToLink(hash)
if (!item) {
return {
errorCode: 404
}
}

parseHeadings = text => {
const headingRegex = /\n(## \s*)(.*)/g
const matches = []
let match
do {
match = headingRegex.exec(text)
if (match)
matches.push({
text: match[2],
slug: kebabCase(match[2])
})
} while (match)

return matches
}
const host = req ? req.headers['host'] : window.location.host
const protocol = host.indexOf('localhost') > -1 ? 'http:' : 'https:'

scrollToLink = hash => {
const element = document.getElementById(hash.replace(/^#/, ''))
try {
const res = await fetch(`${protocol}//${host}${item.source}`)

if (element) {
const headerHeight = document.getElementById(HEADER).offsetHeight
const elementBoundary = element.getBoundingClientRect()
const rootElement = document.getElementById(ROOT_ELEMENT)
rootElement.scroll({ top: elementBoundary.top - headerHeight })
if (res.status !== 200) {
return {
errorCode: res.status
}
}
}

scrollTop = () => {
const rootElement = document.getElementById(ROOT_ELEMENT)
if (rootElement) {
rootElement.scrollTop = 0
}
}
const text = await res.text()

toggleMenu = () => {
this.setState(prevState => ({
isMenuOpen: !prevState.isMenuOpen
}))
return {
item: item,
headings: parseHeadings(text),
markdown: text
}
} catch {
window.location.reload()
}
}

render() {
const {
currentItem: { source, path, label, tutorials, next, prev },
headings,
markdown,
pageNotFound,
isLoading,
isMenuOpen,
isSmoothScrollEnabled
} = this.state

const githubLink = `https://github.com/iterative/dvc.org/blob/master${source}`

return (
<Page stickHeader={true} enableSmoothScroll={isSmoothScrollEnabled}>
<HeadInjector sectionName={label} />
<Container>
<Backdrop onClick={this.toggleMenu} visible={isMenuOpen} />

<SideToggle onClick={this.toggleMenu} isMenuOpen={isMenuOpen}>
<Hamburger />
</SideToggle>

<Side isOpen={isMenuOpen}>
{this.state.search && (
<SearchArea>
<SearchForm />
</SearchArea>
)}

<SidebarMenu
sidebar={sidebar}
currentPath={path}
onNavigate={this.onNavigate}
id={SIDEBAR_MENU}
/>
</Side>

{isLoading ? (
<Loader />
) : pageNotFound ? (
<Page404 />
) : (
<Markdown
markdown={markdown}
githubLink={githubLink}
tutorials={tutorials}
prev={prev}
next={next}
onNavigate={this.onNavigate}
/>
)}
<RightPanel
headings={headings}
tutorials={tutorials}
githubLink={githubLink}
/>
</Container>
</Page>
)
}
Documentation.propTypes = {
item: PropTypes.object,
headings: PropTypes.array,
markdown: PropTypes.string,
errorCode: PropTypes.bool
}

const Container = styled.div`
Expand Down
Loading

0 comments on commit 43170d3

Please sign in to comment.