Consolidate some related components to single files

Luke Karrys committed Oct 22, 2023 at 00:14 UTC 67e2dfb3cec781d94a59abff09b69ef192db8812
7 files changed +227 -268
src/components/__tests__/contributors.test.js deleted
-28
@@ -1,28 +0,0 @@
1 -import {render} from '@testing-library/react'
2 -import React from 'react'
3 -import Contributors from '../contributors'
4 -
5 -test('renders contributors', () => {
6 - const {queryByText} = render(
7 - <Contributors
8 - contributors={['colebemis', 'emplums']}
9 - latestCommit={{
10 - login: 'colebemis',
11 - url: '#',
12 - date: '2019-08-15T23:40:19Z',
13 - }}
14 - />,
15 - )
16 -
17 - expect(queryByText(/2 contributors/)).toBeInTheDocument()
18 - expect(queryByText(/Last edited by/)).toBeInTheDocument()
19 - expect(queryByText(/colebemis/)).toBeInTheDocument()
20 - expect(queryByText(/August 15, 2019/)).toBeInTheDocument()
21 -})
22 -
23 -test('does not render "last edited by" if latest contributor does not have a latest commit', () => {
24 - const {queryByText} = render(<Contributors contributors={['ashygee']} />)
25 -
26 - expect(queryByText(/1 contributor/)).toBeInTheDocument()
27 - expect(queryByText(/Last edited by/)).toBeNull()
28 -})
src/components/contributors.js deleted
-49
@@ -1,49 +0,0 @@
1 -import React from 'react'
2 -import {Box, Avatar, Text, Tooltip} from '@primer/react'
3 -import Link from './link'
4 -
5 -const months = [
6 - 'January',
7 - 'February',
8 - 'March',
9 - 'April',
10 - 'May',
11 - 'June',
12 - 'July',
13 - 'August',
14 - 'September',
15 - 'October',
16 - 'November',
17 - 'December',
18 -]
19 -
20 -const format = d => `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`
21 -
22 -const pluralize = (word, count) => `${word}${count === 1 ? '' : 's'}`
23 -
24 -function Contributors({contributors = [], latestCommit}) {
25 - return (
26 - <div>
27 - <Box sx={{display: 'flex', alignItems: 'center'}}>
28 - <Text sx={{mr: 2}}>
29 - {contributors.length} {pluralize('contributor', contributors.length)}
30 - </Text>
31 - {contributors.map(login => (
32 - <Tooltip key={login} aria-label={login}>
33 - <Link href={`https://github.com/${login}`} sx={{lineHeight: 'condensedUltra', mr: 2}}>
34 - <Avatar src={`https://github.com/${login}.png?size=40`} alt={login} />
35 - </Link>
36 - </Tooltip>
37 - ))}
38 - </Box>
39 - {latestCommit ? (
40 - <Text sx={{fontSize: 1, color: 'fg.muted', mt: 1}}>
41 - Last edited by <Link href={`https://github.com/${latestCommit.login}`}>{latestCommit.login}</Link> on{' '}
42 - <Link href={latestCommit.url}>{format(new Date(latestCommit.date))}</Link>
43 - </Text>
44 - ) : null}
45 - </div>
46 - )
47 -}
48 -
49 -export default Contributors
src/components/header.js
+3 -4
@@ -1,9 +1,8 @@
1 import React from 'react'
2 import {Box} from '@primer/react'
3 import styled from 'styled-components'
4 -import MobileSearch from './mobile-search'
4 +import * as Search from './search'
5 import NavDrawer from './nav-drawer'
6 -import Search from './search'
6 import Link from './link'
7 import useSearch from '../hooks/use-search'
8 import {HEADER_HEIGHT, HEADER_BAR} from '../constants'
@@ -63,7 +62,7 @@ function Header() {
62 {siteMetadata.title}
63 </Link>
64 <Box sx={{display: ['none', null, null, 'block'], ml: 4}}>
66 - <Search {...search} />
65 + <Search.Desktop {...search} />
66 </Box>
67 </Box>
68 <Box sx={{display: 'flex'}}>
@@ -75,7 +74,7 @@ function Header() {
74 ))}
75 </Box>
76 <Box sx={{display: ['flex', null, null, 'none']}}>
78 - <MobileSearch {...search} />
77 + <Search.Mobile {...search} />
78 <NavDrawer />
79 </Box>
80 </Box>
src/components/mobile-search.js deleted
-166
@@ -1,166 +0,0 @@
1 -import React from 'react'
2 -import {Button, Box} from '@primer/react'
3 -import {XIcon, SearchIcon} from '@primer/octicons-react'
4 -import {AnimatePresence, motion} from 'framer-motion'
5 -import {FocusOn} from 'react-focus-on'
6 -import TextInput from './text-input'
7 -import SearchResults from './search-results'
8 -import useSiteMetadata from '../hooks/use-site-metadata'
9 -import {HEADER_BAR, HEADER_HEIGHT} from '../constants'
10 -import {LightTheme} from '../theme'
11 -
12 -function MobileSearch({open, setOpen, ...props}) {
13 - const siteMetadata = useSiteMetadata()
14 - const {reset, results, isOpen: resultsOpen, getInputProps, getItemProps, getMenuProps, highlightedIndex} = props
15 -
16 - const handleDismiss = () => {
17 - reset()
18 - setOpen(false)
19 - }
20 -
21 - return (
22 - <AnimatePresence>
23 - {open ? (
24 - <FocusOn returnFocus={true} onEscapeKey={handleDismiss}>
25 - <Box
26 - sx={{
27 - position: 'fixed',
28 - top: `${HEADER_BAR}px`,
29 - left: 0,
30 - right: 0,
31 - bottom: 0,
32 - zIndex: 1,
33 - }}
34 - >
35 - <Box
36 - sx={{
37 - position: 'absolute',
38 - top: 0,
39 - left: 0,
40 - right: 0,
41 - bottom: 0,
42 - bg: 'overlay.backdrop',
43 - zIndex: -1,
44 - }}
45 - key="search-backdrop"
46 - as={motion.div}
47 - initial={{opacity: 0}}
48 - animate={{opacity: 1}}
49 - exit={{opacity: 0}}
50 - transition={{type: 'tween'}}
51 - onClick={handleDismiss}
52 - />
53 - <Box sx={{display: 'flex', flexDirection: 'column', height: resultsOpen ? '100%' : 'auto'}}>
54 - <Box
55 - sx={{
56 - display: 'flex',
57 - color: 'fg.default',
58 - height: `${HEADER_HEIGHT}px`,
59 - flex: '0 0 auto',
60 - px: 3,
61 - alignItems: 'center',
62 - border: '1px solid',
63 - borderTopWidth: 0,
64 - borderLeftWidth: 0,
65 - borderRightWidth: 0,
66 - borderColor: 'border.muted',
67 - position: 'relative',
68 - }}
69 - >
70 - <motion.div
71 - key="search-box"
72 - initial={{scaleX: 0}}
73 - animate={{scaleX: 1}}
74 - exit={{scaleX: 0}}
75 - transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
76 - style={{width: '100%', originX: '100%'}}
77 - >
78 - <Box
79 - sx={{
80 - position: 'absolute',
81 - top: 0,
82 - bottom: 0,
83 - left: 0,
84 - width: '70px',
85 - bg: 'canvas.default',
86 - zIndex: '-1',
87 - }}
88 - />
89 - <TextInput
90 - leadingVisual={SearchIcon}
91 - placeholder={`Search ${siteMetadata.title}`}
92 - aria-label={`Search ${siteMetadata.title}`}
93 - sx={{width: '100%'}}
94 - {...getInputProps()}
95 - />
96 - </motion.div>
97 - <Box
98 - key="button-blocker"
99 - as={motion.div}
100 - initial={{opacity: 0}}
101 - animate={{opacity: 1}}
102 - exit={{opacity: 0}}
103 - transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
104 - sx={{
105 - position: 'absolute',
106 - top: 0,
107 - bottom: 0,
108 - right: 0,
109 - width: '70px',
110 - bg: 'canvas.default',
111 - zIndex: '-1',
112 - }}
113 - />
114 - <Box
115 - key="button"
116 - as={motion.div}
117 - initial={{opacity: 0}}
118 - animate={{opacity: 1}}
119 - exit={{opacity: 0}}
120 - transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
121 - >
122 - <Button sx={{ml: 3}} aria-label="Cancel" onClick={handleDismiss}>
123 - <XIcon />
124 - </Button>
125 - </Box>
126 - </Box>
127 - <LightTheme>
128 - <Box
129 - sx={{
130 - display: 'flex',
131 - bg: 'canvas.default',
132 - py: resultsOpen ? 1 : 0,
133 - flexDirection: 'column',
134 - flex: '1 1 auto',
135 - overflow: 'auto',
136 - }}
137 - style={{
138 - WebkitOverflowScrolling: 'touch',
139 - }}
140 - {...getMenuProps()}
141 - >
142 - {resultsOpen ? <SearchResults {...{results, getItemProps, highlightedIndex}} /> : null}
143 - </Box>
144 - </LightTheme>
145 - </Box>
146 - </Box>
147 - </FocusOn>
148 - ) : null}
149 - </AnimatePresence>
150 - )
151 -}
152 -
153 -function MobileSearchWrapper(props) {
154 - const [open, setOpen] = React.useState(false)
155 -
156 - return (
157 - <>
158 - <Button aria-label="Search" aria-expanded={open} onClick={() => setOpen(true)}>
159 - <SearchIcon />
160 - </Button>
161 - <MobileSearch open={open} setOpen={setOpen} {...props} />
162 - </>
163 - )
164 -}
165 -
166 -export default MobileSearchWrapper
src/components/page-footer.js
+58 -6
@@ -1,13 +1,65 @@
1 import React from 'react'
2 -import {Box, Octicon} from '@primer/react'
2 +import {Box, Octicon, Avatar, Text, Tooltip} from '@primer/react'
3 import {PencilIcon} from '@primer/octicons-react'
4 import Link from './link'
5 -import Contributors from './contributors'
5 import usePage from '../hooks/use-page'
6
8 -function PageFooter() {
7 +const months = [
8 + 'January',
9 + 'February',
10 + 'March',
11 + 'April',
12 + 'May',
13 + 'June',
14 + 'July',
15 + 'August',
16 + 'September',
17 + 'October',
18 + 'November',
19 + 'December',
20 +]
21 +
22 +const format = d => `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`
23 +
24 +const pluralize = (word, count) => `${word}${count === 1 ? '' : 's'}`
25 +
26 +const Contributors = ({contributors = [], latestCommit}) => {
27 + if (!contributors.length) {
28 + return null
29 + }
30 +
31 + return (
32 + <div>
33 + <Box sx={{display: 'flex', alignItems: 'center'}}>
34 + <Text sx={{mr: 2}}>
35 + {contributors.length} {pluralize('contributor', contributors.length)}
36 + </Text>
37 + {contributors.map(login => (
38 + <Tooltip key={login} aria-label={login}>
39 + <Link href={`https://github.com/${login}`} sx={{lineHeight: 'condensedUltra', mr: 2}}>
40 + <Avatar src={`https://github.com/${login}.png?size=40`} alt={login} />
41 + </Link>
42 + </Tooltip>
43 + ))}
44 + </Box>
45 + {latestCommit ? (
46 + <Text sx={{fontSize: 1, color: 'fg.muted', mt: 1}}>
47 + Last edited by <Link href={`https://github.com/${latestCommit.login}`}>{latestCommit.login}</Link> on{' '}
48 + <Link href={latestCommit.url}>{format(new Date(latestCommit.date))}</Link>
49 + </Text>
50 + ) : null}
51 + </div>
52 + )
53 +}
54 +
55 +const PageFooter = () => {
56 const {editUrl, latestCommit, contributors = []} = usePage().pageContext
10 - return editUrl || contributors.length ? (
57 +
58 + if (!editUrl && !contributors.length) {
59 + return null
60 + }
61 +
62 + return (
63 <Box
64 sx={{
65 borderWidth: 0,
@@ -26,10 +78,10 @@ function PageFooter() {
78 Edit this page on GitHub
79 </Link>
80 ) : null}
29 - {contributors.length ? <Contributors contributors={contributors} latestCommit={latestCommit} /> : null}
81 + <Contributors contributors={contributors} latestCommit={latestCommit} />
82 </Box>
83 </Box>
32 - ) : null
84 + )
85 }
86
87 export default PageFooter
src/components/search-results.js
+11 -8
@@ -4,7 +4,7 @@ import {LinkNoUnderline} from './link'
4 import useSiteMetadata from '../hooks/use-site-metadata'
5 import * as getNav from '../util/get-nav'
6
7 -const Breadcrumbs = ({item}) => {
7 +const SearchItem = ({item}) => {
8 const siteMetadata = useSiteMetadata()
9 const variant = getNav.getVariant(getNav.getVariantRoot(item.path), item.path)
10 const hierarchy = getNav.getItemBreadcrumbs(item.path)
@@ -16,9 +16,14 @@ const Breadcrumbs = ({item}) => {
16 hierarchy.pop()
17 }
18
19 - const text = hierarchy.length ? hierarchy.map(s => s.shortName || s.title).join(' / ') : siteMetadata.shortName
20 -
21 - return <Text sx={{fontSize: 0}}>{text}</Text>
19 + return (
20 + <>
21 + <Text sx={{fontSize: 0}}>
22 + {hierarchy.length ? hierarchy.map(s => s.shortName || s.title).join(' / ') : siteMetadata.shortName}
23 + </Text>
24 + {item.title}
25 + </>
26 + )
27 }
28
29 function SearchResults({results, getItemProps, highlightedIndex}) {
@@ -32,10 +37,10 @@ function SearchResults({results, getItemProps, highlightedIndex}) {
37
38 return results.map((item, index) => (
39 <Box
40 + {...getItemProps({item, index})}
41 as={LinkNoUnderline}
42 to={item.path}
43 key={item.path}
38 - style={{cursor: 'pointer'}}
44 sx={{
45 display: 'flex',
46 flexDirection: 'column',
@@ -46,10 +51,8 @@ function SearchResults({results, getItemProps, highlightedIndex}) {
51 fontSize: 1,
52 bg: highlightedIndex === index ? 'neutral.muted' : 'transparent',
53 }}
49 - {...getItemProps({item, index})}
54 >
51 - <Breadcrumbs item={item} />
52 - {item.title}
55 + <SearchItem item={item} />
56 </Box>
57 ))
58 }
src/components/search.js
+155 -7
@@ -1,11 +1,15 @@
1 import React from 'react'
2 -import {Box} from '@primer/react'
2 +import {Button, Box} from '@primer/react'
3 +import {XIcon, SearchIcon} from '@primer/octicons-react'
4 +import {AnimatePresence, motion} from 'framer-motion'
5 +import {FocusOn} from 'react-focus-on'
6 import TextInput from './text-input'
7 import SearchResults from './search-results'
8 import useSiteMetadata from '../hooks/use-site-metadata'
9 +import {HEADER_BAR, HEADER_HEIGHT} from '../constants'
10 import {LightTheme} from '../theme'
11
8 -function Search(props) {
12 +export const Desktop = props => {
13 const siteMetadata = useSiteMetadata()
14 const {getInputProps, getMenuProps, isOpen, results, getItemProps, highlightedIndex} = props
15
@@ -17,7 +21,7 @@ function Search(props) {
21 aria-label={`Search ${siteMetadata.title}`}
22 {...getInputProps()}
23 />
20 - <Box sx={{position: 'absolute', left: 0, right: 0, pt: 2}} {...getMenuProps()}>
24 + <Box sx={{position: 'absolute', left: 0, right: 0, pt: 1}} {...getMenuProps()}>
25 {isOpen ? (
26 <LightTheme>
27 <Box
@@ -25,12 +29,11 @@ function Search(props) {
29 overflow: 'auto',
30 minWidth: 300,
31 maxHeight: '70vh',
28 - p: 2,
32 boxShadow: 'shadow.large',
33 borderColor: 'border.muted',
34 bg: 'canvas.overlay',
32 - borderRadius: '12px',
33 - borderWidth: '1px',
35 + borderRadius: 2,
36 + borderWidth: 1,
37 borderStyle: 'solid',
38 }}
39 >
@@ -43,4 +46,149 @@ function Search(props) {
46 )
47 }
48
46 -export default Search
49 +export const Mobile = props => {
50 + const [open, setOpen] = React.useState(false)
51 + const siteMetadata = useSiteMetadata()
52 + const {reset, results, isOpen: resultsOpen, getInputProps, getItemProps, getMenuProps, highlightedIndex} = props
53 +
54 + const handleDismiss = () => {
55 + reset()
56 + setOpen(false)
57 + }
58 +
59 + return (
60 + <>
61 + <Button aria-label="Search" aria-expanded={open} onClick={() => setOpen(true)}>
62 + <SearchIcon />
63 + </Button>
64 + <AnimatePresence>
65 + {open ? (
66 + <FocusOn returnFocus={true} onEscapeKey={handleDismiss}>
67 + <Box
68 + sx={{
69 + position: 'fixed',
70 + top: `${HEADER_BAR}px`,
71 + left: 0,
72 + right: 0,
73 + bottom: 0,
74 + zIndex: 1,
75 + }}
76 + >
77 + <Box
78 + sx={{
79 + position: 'absolute',
80 + top: 0,
81 + left: 0,
82 + right: 0,
83 + bottom: 0,
84 + bg: 'overlay.backdrop',
85 + zIndex: -1,
86 + }}
87 + key="search-backdrop"
88 + as={motion.div}
89 + initial={{opacity: 0}}
90 + animate={{opacity: 1}}
91 + exit={{opacity: 0}}
92 + transition={{type: 'tween'}}
93 + onClick={handleDismiss}
94 + />
95 + <Box sx={{display: 'flex', flexDirection: 'column', height: resultsOpen ? '100%' : 'auto'}}>
96 + <Box
97 + sx={{
98 + display: 'flex',
99 + color: 'fg.default',
100 + height: `${HEADER_HEIGHT}px`,
101 + flex: '0 0 auto',
102 + px: 3,
103 + alignItems: 'center',
104 + border: '1px solid',
105 + borderTopWidth: 0,
106 + borderLeftWidth: 0,
107 + borderRightWidth: 0,
108 + borderColor: 'border.muted',
109 + position: 'relative',
110 + }}
111 + >
112 + <motion.div
113 + key="search-box"
114 + initial={{scaleX: 0}}
115 + animate={{scaleX: 1}}
116 + exit={{scaleX: 0}}
117 + transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
118 + style={{width: '100%', originX: '100%'}}
119 + >
120 + <Box
121 + sx={{
122 + position: 'absolute',
123 + top: 0,
124 + bottom: 0,
125 + left: 0,
126 + width: '70px',
127 + bg: 'canvas.default',
128 + zIndex: '-1',
129 + }}
130 + />
131 + <TextInput
132 + leadingVisual={SearchIcon}
133 + placeholder={`Search ${siteMetadata.title}`}
134 + aria-label={`Search ${siteMetadata.title}`}
135 + sx={{width: '100%'}}
136 + {...getInputProps()}
137 + />
138 + </motion.div>
139 + <Box
140 + key="button-blocker"
141 + as={motion.div}
142 + initial={{opacity: 0}}
143 + animate={{opacity: 1}}
144 + exit={{opacity: 0}}
145 + transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
146 + sx={{
147 + position: 'absolute',
148 + top: 0,
149 + bottom: 0,
150 + right: 0,
151 + width: '70px',
152 + bg: 'canvas.default',
153 + zIndex: '-1',
154 + }}
155 + />
156 + <Box
157 + key="button"
158 + as={motion.div}
159 + initial={{opacity: 0}}
160 + animate={{opacity: 1}}
161 + exit={{opacity: 0}}
162 + transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
163 + >
164 + <Button sx={{ml: 3}} aria-label="Cancel" onClick={handleDismiss}>
165 + <XIcon />
166 + </Button>
167 + </Box>
168 + </Box>
169 + <LightTheme>
170 + <Box
171 + sx={{
172 + display: 'flex',
173 + bg: 'canvas.default',
174 + py: resultsOpen ? 1 : 0,
175 + flexDirection: 'column',
176 + flex: '1 1 auto',
177 + overflow: 'auto',
178 + }}
179 + style={{
180 + WebkitOverflowScrolling: 'touch',
181 + }}
182 + {...getMenuProps()}
183 + >
184 + {resultsOpen ? <SearchResults {...{results, getItemProps, highlightedIndex}} /> : null}
185 + </Box>
186 + </LightTheme>
187 + </Box>
188 + </Box>
189 + </FocusOn>
190 + ) : null}
191 + </AnimatePresence>
192 + </>
193 + )
194 +}