Skip to content

Commit 87c63cb

Browse files
authored
Branch was updated using the 'autoupdate branch' Actions workflow.
2 parents df63c1d + 06d8f81 commit 87c63cb

37 files changed

+8707
-3061
lines changed

components/article/ArticlePage.tsx

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
1+
import { useRouter } from 'next/router'
12
import cx from 'classnames'
23

4+
import { ZapIcon, InfoIcon } from '@primer/octicons-react'
5+
import { Callout } from 'components/ui/Callout'
6+
7+
import { Link } from 'components/Link'
38
import { DefaultLayout } from 'components/DefaultLayout'
49
import { ArticleTopper } from 'components/article/ArticleTopper'
510
import { ArticleTitle } from 'components/article/ArticleTitle'
611
import { useArticleContext } from 'components/context/ArticleContext'
7-
import { InfoIcon } from '@primer/octicons-react'
812
import { useTranslation } from 'components/hooks/useTranslation'
913
import { LearningTrackNav } from './LearningTrackNav'
1014
import { ArticleContent } from './ArticleContent'
1115
import { ArticleGridLayout } from './ArticleGridLayout'
12-
import { Callout } from 'components/ui/Callout'
16+
17+
// Mapping of a "normal" article to it's interactive counterpart
18+
const interactiveAlternatives: Record<string, { href: string }> = {
19+
'/actions/guides/building-and-testing-nodejs': {
20+
href: '/actions/guides/building-and-testing-nodejs-or-python?langId=nodejs',
21+
},
22+
'/actions/guides/building-and-testing-python': {
23+
href: '/actions/guides/building-and-testing-nodejs-or-python?langId=python',
24+
},
25+
}
1326

1427
export const ArticlePage = () => {
28+
const router = useRouter()
1529
const {
1630
title,
1731
intro,
@@ -25,6 +39,8 @@ export const ArticlePage = () => {
2539
currentLearningTrack,
2640
} = useArticleContext()
2741
const { t } = useTranslation('pages')
42+
const currentPath = router.asPath.split('?')[0]
43+
2844
return (
2945
<DefaultLayout>
3046
<div className="container-xl px-3 px-md-6 my-4 my-lg-4">
@@ -101,30 +117,40 @@ export const ArticlePage = () => {
101117
</>
102118
}
103119
toc={
104-
miniTocItems.length > 1 && (
105-
<>
106-
<h2 id="in-this-article" className="f5 mb-2">
107-
<a className="Link--primary" href="#in-this-article">
108-
{t('miniToc')}
109-
</a>
110-
</h2>
111-
<ul className="list-style-none pl-0 f5 mb-0">
112-
{miniTocItems.map((item) => {
113-
return (
114-
<li
115-
key={item.contents}
116-
className={cx(
117-
`ml-${item.indentationLevel * 3}`,
118-
item.platform,
119-
'mb-2 lh-condensed'
120-
)}
121-
dangerouslySetInnerHTML={{ __html: item.contents }}
122-
/>
123-
)
124-
})}
125-
</ul>
126-
</>
127-
)
120+
<>
121+
{!!interactiveAlternatives[currentPath] && (
122+
<div className="flash mb-3">
123+
<ZapIcon className="mr-2" />
124+
<Link href={interactiveAlternatives[currentPath].href}>
125+
Try the new interactive article
126+
</Link>
127+
</div>
128+
)}
129+
{miniTocItems.length > 1 && (
130+
<>
131+
<h2 id="in-this-article" className="f5 mb-2">
132+
<a className="Link--primary" href="#in-this-article">
133+
{t('miniToc')}
134+
</a>
135+
</h2>
136+
<ul className="list-style-none pl-0 f5 mb-0">
137+
{miniTocItems.map((item) => {
138+
return (
139+
<li
140+
key={item.contents}
141+
className={cx(
142+
`ml-${item.indentationLevel * 3}`,
143+
item.platform,
144+
'mb-2 lh-condensed'
145+
)}
146+
dangerouslySetInnerHTML={{ __html: item.contents }}
147+
/>
148+
)
149+
})}
150+
</ul>
151+
</>
152+
)}
153+
</>
128154
}
129155
>
130156
<ArticleContent>{renderedPage}</ArticleContent>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, { createContext, useContext, useState } from 'react'
2+
import { CodeLanguage, PlaygroundArticleT } from 'components/playground/types'
3+
import { useRouter } from 'next/router'
4+
5+
import jsArticle from 'components/playground/content/building-and-testing/nodejs'
6+
import pyArticle from 'components/playground/content/building-and-testing/python'
7+
8+
const articles = [jsArticle, pyArticle]
9+
const articlesByLangId = articles.reduce((obj, item) => {
10+
obj[item.codeLanguageId] = item
11+
return obj
12+
}, {} as Record<string, PlaygroundArticleT | undefined>)
13+
14+
const codeLanguages: Array<CodeLanguage> = [
15+
{
16+
id: 'nodejs',
17+
label: 'Node.js',
18+
},
19+
{
20+
id: 'py',
21+
label: 'Python',
22+
},
23+
]
24+
25+
type PlaygroundContextT = {
26+
activeSectionIndex: number
27+
setActiveSectionIndex: (sectionIndex: number) => void
28+
scrollToSection: number | undefined
29+
setScrollToSection: (sectionIndex?: number) => void
30+
codeLanguages: Array<CodeLanguage>
31+
currentLanguage: CodeLanguage
32+
article: PlaygroundArticleT | undefined
33+
}
34+
35+
export const PlaygroundContext = createContext<PlaygroundContextT | null>(null)
36+
37+
export const usePlaygroundContext = (): PlaygroundContextT => {
38+
const context = useContext(PlaygroundContext)
39+
40+
if (!context) {
41+
throw new Error('"usePlaygroundContext" may only be used inside "PlaygroundContext.Provider"')
42+
}
43+
44+
return context
45+
}
46+
47+
export const PlaygroundContextProvider = (props: { children: React.ReactNode }) => {
48+
const router = useRouter()
49+
const [activeSectionIndex, setActiveSectionIndex] = useState(0)
50+
const [scrollToSection, setScrollToSection] = useState<number>()
51+
const { langId } = router.query
52+
const currentLanguage = codeLanguages.find(({ id }) => id === langId) || codeLanguages[0]
53+
const availableLanguages = codeLanguages.filter(({ id }) => !!articlesByLangId[id])
54+
55+
const article = articlesByLangId[currentLanguage.id]
56+
57+
const context = {
58+
activeSectionIndex,
59+
setActiveSectionIndex,
60+
scrollToSection,
61+
setScrollToSection,
62+
currentLanguage,
63+
codeLanguages: availableLanguages,
64+
article,
65+
}
66+
67+
return <PlaygroundContext.Provider value={context}>{props.children}</PlaygroundContext.Provider>
68+
}

components/hooks/useBreakpoint.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useTheme } from '@primer/components'
2+
3+
import { useMediaQuery } from './useMediaQuery'
4+
5+
type Size = 'small' | 'medium' | 'large' | 'xlarge'
6+
export function useBreakpoint(size: Size) {
7+
const { theme } = useTheme()
8+
return useMediaQuery(`(max-width: ${theme?.sizes[size]})`)
9+
}

components/hooks/useClipboard.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useState, useEffect } from 'react'
2+
3+
interface IOptions {
4+
/**
5+
* Reset the status after a certain number of milliseconds. This is useful
6+
* for showing a temporary success message.
7+
*/
8+
successDuration?: number
9+
}
10+
11+
export default function useCopyClipboard(
12+
text: string,
13+
options?: IOptions
14+
): [boolean, () => Promise<void>] {
15+
const [isCopied, setIsCopied] = useState(false)
16+
const successDuration = options && options.successDuration
17+
18+
useEffect(() => {
19+
if (isCopied && successDuration) {
20+
const id = setTimeout(() => {
21+
setIsCopied(false)
22+
}, successDuration)
23+
24+
return () => {
25+
clearTimeout(id)
26+
}
27+
}
28+
}, [isCopied, successDuration])
29+
30+
return [
31+
isCopied,
32+
async () => {
33+
try {
34+
await navigator.clipboard.writeText(text)
35+
setIsCopied(true)
36+
} catch {
37+
setIsCopied(false)
38+
}
39+
},
40+
]
41+
}

components/hooks/useMediaQuery.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useState, useEffect } from 'react'
2+
3+
export function useMediaQuery(query: string) {
4+
const [state, setState] = useState(
5+
typeof window !== 'undefined' ? window.matchMedia(query).matches : false
6+
)
7+
8+
useEffect(() => {
9+
let mounted = true
10+
const mql = window.matchMedia(query)
11+
const onChange = () => {
12+
if (!mounted) {
13+
return
14+
}
15+
setState(!!mql.matches)
16+
}
17+
18+
mql.addEventListener('change', onChange)
19+
setState(mql.matches)
20+
21+
return () => {
22+
mounted = false
23+
mql.removeEventListener('change', onChange)
24+
}
25+
}, [query])
26+
27+
return state
28+
}

components/hooks/useOnScreen.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,23 @@ import { useState, useEffect, MutableRefObject, RefObject } from 'react'
22

33
export function useOnScreen<T extends Element>(
44
ref: MutableRefObject<T | undefined> | RefObject<T>,
5-
rootMargin: string = '0px'
5+
options?: IntersectionObserverInit
66
): boolean {
77
const [isIntersecting, setIntersecting] = useState(false)
88
useEffect(() => {
9-
const observer = new IntersectionObserver(
10-
([entry]) => {
11-
setIntersecting(entry.isIntersecting)
12-
},
13-
{
14-
rootMargin,
15-
}
16-
)
9+
let isMounted = true
10+
const observer = new IntersectionObserver(([entry]) => {
11+
isMounted && setIntersecting(entry.isIntersecting)
12+
}, options)
13+
1714
if (ref.current) {
1815
observer.observe(ref.current)
1916
}
17+
2018
return () => {
19+
isMounted = false
2120
ref.current && observer.unobserve(ref.current)
2221
}
23-
}, [])
22+
}, [Object.values(options || {}).join(',')])
2423
return isIntersecting
2524
}

components/hooks/useWindowScroll.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useState, useEffect } from 'react'
2+
3+
// returns scroll position
4+
export function useWindowScroll(): number {
5+
const [scrollPosition, setScrollPosition] = useState(
6+
typeof window !== 'undefined' ? window.scrollY : 0
7+
)
8+
9+
useEffect(() => {
10+
const setScollPositionCallback = () => setScrollPosition(window.scrollY)
11+
12+
if (typeof window !== 'undefined') {
13+
window.addEventListener('scroll', setScollPositionCallback)
14+
}
15+
16+
return () => {
17+
if (typeof window !== 'undefined') {
18+
window.removeEventListener('scroll', setScollPositionCallback)
19+
}
20+
}
21+
}, [])
22+
23+
return scrollPosition
24+
}

components/lib/events.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ type SendEventProps = {
6666
}
6767

6868
export function sendEvent({ type, version = '1.0.0', ...props }: SendEventProps) {
69+
let site_language = location.pathname.split('/')[1]
70+
if (location.pathname.startsWith('/playground')) {
71+
site_language = 'en'
72+
}
73+
6974
const body = {
7075
_csrf: getCsrf(),
7176

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

9095
// Device information
9196
// os, os_version, browser, browser_version:

components/lib/getAnchorLink.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const getAnchorLink = (title: string) => title.toLowerCase().replace(/\s/g, '-')
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react'
2+
import { useTheme } from '@primer/components'
3+
import ReactMarkdown from 'react-markdown'
4+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
5+
import { vs, vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'
6+
import gfm from 'remark-gfm'
7+
8+
type Props = {
9+
className?: string
10+
children: string
11+
}
12+
export const ArticleMarkdown = ({ className, children }: Props) => {
13+
const theme = useTheme()
14+
15+
return (
16+
<ReactMarkdown
17+
className={className}
18+
remarkPlugins={[gfm as any]}
19+
components={{
20+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
21+
code: ({ node, inline, className, children, ...props }) => {
22+
const match = /language-(\w+)/.exec(className || '')
23+
return !inline && match ? (
24+
<SyntaxHighlighter
25+
style={theme.colorScheme === 'dark' ? vscDarkPlus : vs}
26+
language={match[1]}
27+
PreTag="div"
28+
children={String(children).replace(/\n$/, '')}
29+
{...(props as any)}
30+
/>
31+
) : (
32+
<code className={className} {...props}>
33+
{children}
34+
</code>
35+
)
36+
},
37+
}}
38+
>
39+
{children}
40+
</ReactMarkdown>
41+
)
42+
}

0 commit comments

Comments
 (0)