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
+}