diff --git a/docs/src/gatsby-theme-apollo-docs/components/multi-code-block.js b/docs/src/gatsby-theme-apollo-docs/components/multi-code-block.js new file mode 100644 index 000000000..c9346ebb5 --- /dev/null +++ b/docs/src/gatsby-theme-apollo-docs/components/multi-code-block.js @@ -0,0 +1,127 @@ +import PropTypes from 'prop-types'; +import React, {createContext, useContext, useMemo} from 'react'; +import styled from '@emotion/styled'; +import {trackCustomEvent} from 'gatsby-plugin-google-analytics'; + +export const GA_EVENT_CATEGORY_CODE_BLOCK = 'Code Block'; +export const MultiCodeBlockContext = createContext({}); +export const SelectedLanguageContext = createContext(); + +const Container = styled.div({ + position: 'relative' +}); + +const langLabels = { + js: 'JavaScript', + ts: 'TypeScript', + 'hooks-js': 'Hooks (JS)', + 'hooks-ts': 'Hooks (TS)' +}; + +function getUnifiedLang(language) { + switch (language) { + case 'js': + case 'jsx': + case 'javascript': + return 'js'; + case 'ts': + case 'tsx': + case 'typescript': + return 'ts'; + default: + return language; + } +} + +function getLang(child) { + return getUnifiedLang(child.props['data-language']); +} + +export function MultiCodeBlock(props) { + const {codeBlocks, titles} = useMemo(() => { + const defaultState = { + codeBlocks: {}, + titles: {} + }; + + if (!Array.isArray(props.children)) { + return defaultState; + } + + return props.children.reduce((acc, child, index, array) => { + const lang = getLang(child); + if (lang) { + return { + ...acc, + codeBlocks: { + ...acc.codeBlocks, + [lang]: child + } + }; + } + + if (child.props.className === 'gatsby-code-title') { + const nextNode = array[index + 1]; + const title = child.props.children; + const lang = getLang(nextNode); + if (nextNode && title && lang) { + return { + ...acc, + titles: { + ...acc.titles, + [lang]: title + } + }; + } + } + + return acc; + }, defaultState); + }, [props.children]); + + const languages = useMemo(() => Object.keys(codeBlocks), [codeBlocks]); + const [selectedLanguage, setSelectedLanguage] = useContext( + SelectedLanguageContext + ); + + if (!languages.length) { + return props.children; + } + + function handleLanguageChange(language) { + setSelectedLanguage(language); + trackCustomEvent({ + category: GA_EVENT_CATEGORY_CODE_BLOCK, + action: 'Change language', + label: language + }); + } + + const defaultLanguage = languages[0]; + const renderedLanguage = + selectedLanguage in codeBlocks ? selectedLanguage : defaultLanguage; + + return ( + + ({ + lang, + label: + // try to find a label or capitalize the provided lang + langLabels[lang] || lang.charAt(0).toUpperCase() + lang.slice(1) + })), + onLanguageChange: handleLanguageChange + }} + > +
{titles[renderedLanguage]}
+ {codeBlocks[renderedLanguage]} +
+
+ ); +} + +MultiCodeBlock.propTypes = { + children: PropTypes.node.isRequired +}; diff --git a/docs/src/gatsby-theme-apollo-docs/components/page-layout.js b/docs/src/gatsby-theme-apollo-docs/components/page-layout.js new file mode 100644 index 000000000..7c97481ab --- /dev/null +++ b/docs/src/gatsby-theme-apollo-docs/components/page-layout.js @@ -0,0 +1,312 @@ +import '../prism.less'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; +import DocsetSwitcher from './docset-switcher'; +import Header from './header'; +import HeaderButton from './header-button'; +import PropTypes from 'prop-types'; +import React, {createContext, useMemo, useRef, useState} from 'react'; +import Search from './search'; +import styled from '@emotion/styled'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import {Button} from '@apollo/space-kit/Button'; +import { + FlexWrapper, + Layout, + MenuButton, + Sidebar, + SidebarNav, + breakpoints, + colors, + useResponsiveSidebar +} from 'gatsby-theme-apollo-core'; +import {Helmet} from 'react-helmet'; +import {IconLayoutModule} from '@apollo/space-kit/icons/IconLayoutModule'; +import {Link, graphql, navigate, useStaticQuery} from 'gatsby'; +import {MobileLogo} from './mobile-logo'; +import {Select} from './select'; +import {SelectedLanguageContext} from './multi-code-block'; +import {getSpectrumUrl, getVersionBasePath} from '../utils'; +import {groupBy} from 'lodash'; +import {size} from 'polished'; +import {trackCustomEvent} from 'gatsby-plugin-google-analytics'; + +const Main = styled.main({ + flexGrow: 1 +}); + +const ButtonWrapper = styled.div({ + flexGrow: 1 +}); + +const StyledButton = styled(Button)({ + width: '100%', + ':not(:hover)': { + backgroundColor: colors.background + } +}); + +const StyledIcon = styled(IconLayoutModule)(size(16), { + marginLeft: 'auto' +}); + +const MobileNav = styled.div({ + display: 'none', + [breakpoints.md]: { + display: 'flex', + alignItems: 'center', + marginRight: 32, + color: colors.text1 + } +}); + +const HeaderInner = styled.span({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 32 +}); + +const Eyebrow = styled.div({ + flexShrink: 0, + padding: '8px 56px', + backgroundColor: colors.background, + color: colors.primary, + fontSize: 14, + position: 'sticky', + top: 0, + a: { + color: 'inherit', + fontWeight: 600 + }, + [breakpoints.md]: { + padding: '8px 24px' + } +}); + +function getVersionLabel(version) { + return `v${version}`; +} + +const GA_EVENT_CATEGORY_SIDEBAR = 'Sidebar'; + +function handleToggleAll(expanded) { + trackCustomEvent({ + category: GA_EVENT_CATEGORY_SIDEBAR, + action: 'Toggle all', + label: expanded ? 'expand' : 'collapse' + }); +} + +function handleToggleCategory(label, expanded) { + trackCustomEvent({ + category: GA_EVENT_CATEGORY_SIDEBAR, + action: 'Toggle category', + label, + value: Number(expanded) + }); +} + +export const NavItemsContext = createContext(); + +export default function PageLayout(props) { + const data = useStaticQuery( + graphql` + { + site { + siteMetadata { + title + siteName + } + } + } + ` + ); + + const { + sidebarRef, + openSidebar, + sidebarOpen, + handleWrapperClick, + handleSidebarNavLinkClick + } = useResponsiveSidebar(); + + const buttonRef = useRef(null); + const [menuOpen, setMenuOpen] = useState(false); + const selectedLanguageState = useLocalStorage('docs-lang'); + + function openMenu() { + setMenuOpen(true); + } + + function closeMenu() { + setMenuOpen(false); + } + + const {pathname} = props.location; + const {siteName, title} = data.site.siteMetadata; + const { + subtitle, + sidebarContents, + versions, + versionDifference, + versionBasePath, + defaultVersion + } = props.pageContext; + const { + spectrumHandle, + twitterHandle, + youtubeUrl, + navConfig = {}, + footerNavConfig, + logoLink, + algoliaApiKey, + algoliaIndexName, + menuTitle + } = props.pluginOptions; + + const {navItems, navCategories} = useMemo(() => { + const navItems = Object.entries(navConfig).map(([title, navItem]) => ({ + ...navItem, + title + })); + return { + navItems, + navCategories: Object.entries(groupBy(navItems, 'category')) + }; + }, [navConfig]); + + const hasNavItems = navItems.length > 0; + const sidebarTitle = ( + {subtitle || siteName} + ); + + return ( + + + + + + + + + {hasNavItems ? ( + + + {sidebarTitle} + + + + ) : ( + sidebarTitle + )} + {versions && versions.length > 0 && ( +