Skip to content

Commit

Permalink
Two-pane Experiment (github#21092)
Browse files Browse the repository at this point in the history
* pull changes from docs-playground

* cleanup, add callout banner

* cleanup linting and test fixes

* add discussion link

Co-authored-by: James M. Greene <JamesMGreene@github.com>
  • Loading branch information
mikesurowiec and JamesMGreene authored Aug 26, 2021
1 parent 94200f3 commit 06d8f81
Show file tree
Hide file tree
Showing 37 changed files with 8,707 additions and 3,061 deletions.
78 changes: 52 additions & 26 deletions components/article/ArticlePage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { useRouter } from 'next/router'
import cx from 'classnames'

import { ZapIcon, InfoIcon } from '@primer/octicons-react'
import { Callout } from 'components/ui/Callout'

import { Link } from 'components/Link'
import { DefaultLayout } from 'components/DefaultLayout'
import { ArticleTopper } from 'components/article/ArticleTopper'
import { ArticleTitle } from 'components/article/ArticleTitle'
import { useArticleContext } from 'components/context/ArticleContext'
import { InfoIcon } from '@primer/octicons-react'
import { useTranslation } from 'components/hooks/useTranslation'
import { LearningTrackNav } from './LearningTrackNav'
import { ArticleContent } from './ArticleContent'
import { ArticleGridLayout } from './ArticleGridLayout'
import { Callout } from 'components/ui/Callout'

// Mapping of a "normal" article to it's interactive counterpart
const interactiveAlternatives: Record<string, { href: string }> = {
'/actions/guides/building-and-testing-nodejs': {
href: '/actions/guides/building-and-testing-nodejs-or-python?langId=nodejs',
},
'/actions/guides/building-and-testing-python': {
href: '/actions/guides/building-and-testing-nodejs-or-python?langId=python',
},
}

export const ArticlePage = () => {
const router = useRouter()
const {
title,
intro,
Expand All @@ -25,6 +39,8 @@ export const ArticlePage = () => {
currentLearningTrack,
} = useArticleContext()
const { t } = useTranslation('pages')
const currentPath = router.asPath.split('?')[0]

return (
<DefaultLayout>
<div className="container-xl px-3 px-md-6 my-4 my-lg-4">
Expand Down Expand Up @@ -101,30 +117,40 @@ export const ArticlePage = () => {
</>
}
toc={
miniTocItems.length > 1 && (
<>
<h2 id="in-this-article" className="f5 mb-2">
<a className="Link--primary" href="#in-this-article">
{t('miniToc')}
</a>
</h2>
<ul className="list-style-none pl-0 f5 mb-0">
{miniTocItems.map((item) => {
return (
<li
key={item.contents}
className={cx(
`ml-${item.indentationLevel * 3}`,
item.platform,
'mb-2 lh-condensed'
)}
dangerouslySetInnerHTML={{ __html: item.contents }}
/>
)
})}
</ul>
</>
)
<>
{!!interactiveAlternatives[currentPath] && (
<div className="flash mb-3">
<ZapIcon className="mr-2" />
<Link href={interactiveAlternatives[currentPath].href}>
Try the new interactive article
</Link>
</div>
)}
{miniTocItems.length > 1 && (
<>
<h2 id="in-this-article" className="f5 mb-2">
<a className="Link--primary" href="#in-this-article">
{t('miniToc')}
</a>
</h2>
<ul className="list-style-none pl-0 f5 mb-0">
{miniTocItems.map((item) => {
return (
<li
key={item.contents}
className={cx(
`ml-${item.indentationLevel * 3}`,
item.platform,
'mb-2 lh-condensed'
)}
dangerouslySetInnerHTML={{ __html: item.contents }}
/>
)
})}
</ul>
</>
)}
</>
}
>
<ArticleContent>{renderedPage}</ArticleContent>
Expand Down
68 changes: 68 additions & 0 deletions components/context/PlaygroundContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { createContext, useContext, useState } from 'react'
import { CodeLanguage, PlaygroundArticleT } from 'components/playground/types'
import { useRouter } from 'next/router'

import jsArticle from 'components/playground/content/building-and-testing/nodejs'
import pyArticle from 'components/playground/content/building-and-testing/python'

const articles = [jsArticle, pyArticle]
const articlesByLangId = articles.reduce((obj, item) => {
obj[item.codeLanguageId] = item
return obj
}, {} as Record<string, PlaygroundArticleT | undefined>)

const codeLanguages: Array<CodeLanguage> = [
{
id: 'nodejs',
label: 'Node.js',
},
{
id: 'py',
label: 'Python',
},
]

type PlaygroundContextT = {
activeSectionIndex: number
setActiveSectionIndex: (sectionIndex: number) => void
scrollToSection: number | undefined
setScrollToSection: (sectionIndex?: number) => void
codeLanguages: Array<CodeLanguage>
currentLanguage: CodeLanguage
article: PlaygroundArticleT | undefined
}

export const PlaygroundContext = createContext<PlaygroundContextT | null>(null)

export const usePlaygroundContext = (): PlaygroundContextT => {
const context = useContext(PlaygroundContext)

if (!context) {
throw new Error('"usePlaygroundContext" may only be used inside "PlaygroundContext.Provider"')
}

return context
}

export const PlaygroundContextProvider = (props: { children: React.ReactNode }) => {
const router = useRouter()
const [activeSectionIndex, setActiveSectionIndex] = useState(0)
const [scrollToSection, setScrollToSection] = useState<number>()
const { langId } = router.query
const currentLanguage = codeLanguages.find(({ id }) => id === langId) || codeLanguages[0]
const availableLanguages = codeLanguages.filter(({ id }) => !!articlesByLangId[id])

const article = articlesByLangId[currentLanguage.id]

const context = {
activeSectionIndex,
setActiveSectionIndex,
scrollToSection,
setScrollToSection,
currentLanguage,
codeLanguages: availableLanguages,
article,
}

return <PlaygroundContext.Provider value={context}>{props.children}</PlaygroundContext.Provider>
}
9 changes: 9 additions & 0 deletions components/hooks/useBreakpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useTheme } from '@primer/components'

import { useMediaQuery } from './useMediaQuery'

type Size = 'small' | 'medium' | 'large' | 'xlarge'
export function useBreakpoint(size: Size) {
const { theme } = useTheme()
return useMediaQuery(`(max-width: ${theme?.sizes[size]})`)
}
41 changes: 41 additions & 0 deletions components/hooks/useClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState, useEffect } from 'react'

interface IOptions {
/**
* Reset the status after a certain number of milliseconds. This is useful
* for showing a temporary success message.
*/
successDuration?: number
}

export default function useCopyClipboard(
text: string,
options?: IOptions
): [boolean, () => Promise<void>] {
const [isCopied, setIsCopied] = useState(false)
const successDuration = options && options.successDuration

useEffect(() => {
if (isCopied && successDuration) {
const id = setTimeout(() => {
setIsCopied(false)
}, successDuration)

return () => {
clearTimeout(id)
}
}
}, [isCopied, successDuration])

return [
isCopied,
async () => {
try {
await navigator.clipboard.writeText(text)
setIsCopied(true)
} catch {
setIsCopied(false)
}
},
]
}
28 changes: 28 additions & 0 deletions components/hooks/useMediaQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useState, useEffect } from 'react'

export function useMediaQuery(query: string) {
const [state, setState] = useState(
typeof window !== 'undefined' ? window.matchMedia(query).matches : false
)

useEffect(() => {
let mounted = true
const mql = window.matchMedia(query)
const onChange = () => {
if (!mounted) {
return
}
setState(!!mql.matches)
}

mql.addEventListener('change', onChange)
setState(mql.matches)

return () => {
mounted = false
mql.removeEventListener('change', onChange)
}
}, [query])

return state
}
19 changes: 9 additions & 10 deletions components/hooks/useOnScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@ import { useState, useEffect, MutableRefObject, RefObject } from 'react'

export function useOnScreen<T extends Element>(
ref: MutableRefObject<T | undefined> | RefObject<T>,
rootMargin: string = '0px'
options?: IntersectionObserverInit
): boolean {
const [isIntersecting, setIntersecting] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting)
},
{
rootMargin,
}
)
let isMounted = true
const observer = new IntersectionObserver(([entry]) => {
isMounted && setIntersecting(entry.isIntersecting)
}, options)

if (ref.current) {
observer.observe(ref.current)
}

return () => {
isMounted = false
ref.current && observer.unobserve(ref.current)
}
}, [])
}, [Object.values(options || {}).join(',')])
return isIntersecting
}
24 changes: 24 additions & 0 deletions components/hooks/useWindowScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useState, useEffect } from 'react'

// returns scroll position
export function useWindowScroll(): number {
const [scrollPosition, setScrollPosition] = useState(
typeof window !== 'undefined' ? window.scrollY : 0
)

useEffect(() => {
const setScollPositionCallback = () => setScrollPosition(window.scrollY)

if (typeof window !== 'undefined') {
window.addEventListener('scroll', setScollPositionCallback)
}

return () => {
if (typeof window !== 'undefined') {
window.removeEventListener('scroll', setScollPositionCallback)
}
}
}, [])

return scrollPosition
}
7 changes: 6 additions & 1 deletion components/lib/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ type SendEventProps = {
}

export function sendEvent({ type, version = '1.0.0', ...props }: SendEventProps) {
let site_language = location.pathname.split('/')[1]
if (location.pathname.startsWith('/playground')) {
site_language = 'en'
}

const body = {
_csrf: getCsrf(),

Expand All @@ -85,7 +90,7 @@ export function sendEvent({ type, version = '1.0.0', ...props }: SendEventProps)
referrer: document.referrer,
search: location.search,
href: location.href,
site_language: location.pathname.split('/')[1],
site_language,

// Device information
// os, os_version, browser, browser_version:
Expand Down
1 change: 1 addition & 0 deletions components/lib/getAnchorLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const getAnchorLink = (title: string) => title.toLowerCase().replace(/\s/g, '-')
42 changes: 42 additions & 0 deletions components/playground/ArticleMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import { useTheme } from '@primer/components'
import ReactMarkdown from 'react-markdown'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { vs, vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'
import gfm from 'remark-gfm'

type Props = {
className?: string
children: string
}
export const ArticleMarkdown = ({ className, children }: Props) => {
const theme = useTheme()

return (
<ReactMarkdown
className={className}
remarkPlugins={[gfm as any]}
components={{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
code: ({ node, inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<SyntaxHighlighter
style={theme.colorScheme === 'dark' ? vscDarkPlus : vs}
language={match[1]}
PreTag="div"
children={String(children).replace(/\n$/, '')}
{...(props as any)}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
},
}}
>
{children}
</ReactMarkdown>
)
}
Loading

0 comments on commit 06d8f81

Please sign in to comment.