Skip to content

Commit 386c666

Browse files
Merge pull request #259 from transitive-bullshit/feature/custom-navigation
2 parents 031c41c + 28b29c3 commit 386c666

14 files changed

+298
-86
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
# Optional (for persisting preview images to redis)
2020
# NOTE: if you want to enable redis, only REDIS_HOST and REDIS_PASSWORD are required
21-
# NOTE: don't forget to set isRedisEnabled to true in the site.config.js file
21+
# NOTE: don't forget to set isRedisEnabled to true in the site.config.ts file
2222
#REDIS_HOST=
2323
#REDIS_PASSWORD=
2424
#REDIS_USER='default'

components/Footer.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react'
2+
import useDarkMode from '@fisch0920/use-dark-mode'
23
import { FaTwitter } from '@react-icons/all-files/fa/FaTwitter'
34
import { FaZhihu } from '@react-icons/all-files/fa/FaZhihu'
45
import { FaGithub } from '@react-icons/all-files/fa/FaGithub'
@@ -11,17 +12,16 @@ import styles from './styles.module.css'
1112

1213
// TODO: merge the data and icons from PageSocial with the social links in Footer
1314

14-
export const Footer: React.FC<{
15-
isDarkMode: boolean
16-
toggleDarkMode: () => void
17-
}> = ({ isDarkMode, toggleDarkMode }) => {
15+
export const FooterImpl: React.FC = () => {
1816
const [hasMounted, setHasMounted] = React.useState(false)
19-
const toggleDarkModeCb = React.useCallback(
17+
const darkMode = useDarkMode(false, { classNameDark: 'dark-mode' })
18+
19+
const onToggleDarkMode = React.useCallback(
2020
(e) => {
2121
e.preventDefault()
22-
toggleDarkMode()
22+
darkMode.toggle()
2323
},
24-
[toggleDarkMode]
24+
[darkMode]
2525
)
2626

2727
React.useEffect(() => {
@@ -32,18 +32,18 @@ export const Footer: React.FC<{
3232
<footer className={styles.footer}>
3333
<div className={styles.copyright}>Copyright 2022 {config.author}</div>
3434

35-
{hasMounted ? (
36-
<div className={styles.settings}>
35+
<div className={styles.settings}>
36+
{hasMounted && (
3737
<a
3838
className={styles.toggleDarkMode}
3939
href='#'
40-
onClick={toggleDarkModeCb}
41-
title='Toggle dark mode'
40+
role='button'
41+
onClick={onToggleDarkMode}
4242
>
43-
{isDarkMode ? <IoMoonSharp /> : <IoSunnyOutline />}
43+
{darkMode.value ? <IoMoonSharp /> : <IoSunnyOutline />}
4444
</a>
45-
</div>
46-
) : null}
45+
)}
46+
</div>
4747

4848
<div className={styles.social}>
4949
{config.twitter && (
@@ -97,3 +97,5 @@ export const Footer: React.FC<{
9797
</footer>
9898
)
9999
}
100+
101+
export const Footer = React.memo(FooterImpl)

components/NotionPage.tsx

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { PageHead } from './PageHead'
3131
import { PageActions } from './PageActions'
3232
import { Footer } from './Footer'
3333
import { PageSocial } from './PageSocial'
34+
import { NotionPageHeader } from './NotionPageHeader'
3435
import { GitHubShareButton } from './GitHubShareButton'
3536

3637
import styles from './styles.module.css'
@@ -57,7 +58,11 @@ const Pdf = dynamic(
5758
}
5859
)
5960
const Modal = dynamic(
60-
() => import('react-notion-x/build/third-party/modal').then((m) => m.Modal),
61+
() =>
62+
import('react-notion-x/build/third-party/modal').then((m) => {
63+
m.Modal.setAppElement('.notion-viewport')
64+
return m.Modal
65+
}),
6166
{
6267
ssr: false
6368
}
@@ -72,15 +77,48 @@ export const NotionPage: React.FC<types.PageProps> = ({
7277
const router = useRouter()
7378
const lite = useSearchParam('lite')
7479

75-
const params: any = {}
76-
if (lite) params.lite = lite
80+
const components = React.useMemo(
81+
() => ({
82+
nextImage: Image,
83+
nextLink: Link,
84+
Code,
85+
Collection,
86+
Equation,
87+
Pdf,
88+
Modal,
89+
Tweet,
90+
Header: NotionPageHeader
91+
}),
92+
[]
93+
)
94+
95+
const twitterContextValue = React.useMemo(() => {
96+
if (!recordMap) {
97+
return null
98+
}
99+
100+
return {
101+
tweetAstMap: (recordMap as any).tweetAstMap || {},
102+
swrOptions: {
103+
fetcher: (id: string) =>
104+
fetch(`/api/get-tweet-ast/${id}`).then((r) => r.json())
105+
}
106+
}
107+
}, [recordMap])
77108

78109
// lite mode is for oembed
79110
const isLiteMode = lite === 'true'
80-
const searchParams = new URLSearchParams(params)
81111

82112
const darkMode = useDarkMode(false, { classNameDark: 'dark-mode' })
83113

114+
const siteMapPageUrl = React.useMemo(() => {
115+
const params: any = {}
116+
if (lite) params.lite = lite
117+
118+
const searchParams = new URLSearchParams(params)
119+
return mapPageUrl(site, recordMap, searchParams)
120+
}, [site, recordMap, lite])
121+
84122
if (router.isFallback) {
85123
return <Loading />
86124
}
@@ -110,8 +148,6 @@ export const NotionPage: React.FC<types.PageProps> = ({
110148
g.block = block
111149
}
112150

113-
const siteMapPageUrl = mapPageUrl(site, recordMap, searchParams)
114-
115151
const canonicalPageUrl =
116152
!config.isDev && getCanonicalPageUrl(site, recordMap)(pageId)
117153

@@ -146,15 +182,7 @@ export const NotionPage: React.FC<types.PageProps> = ({
146182
}
147183

148184
return (
149-
<TwitterContextProvider
150-
value={{
151-
tweetAstMap: (recordMap as any).tweetAstMap || {},
152-
swrOptions: {
153-
fetcher: (id) =>
154-
fetch(`/api/get-tweet-ast/${id}`).then((r) => r.json())
155-
}
156-
}}
157-
>
185+
<TwitterContextProvider value={twitterContextValue}>
158186
<PageHead
159187
pageId={pageId}
160188
site={site}
@@ -173,16 +201,7 @@ export const NotionPage: React.FC<types.PageProps> = ({
173201
styles.notion,
174202
pageId === site.rootNotionPageId && 'index-page'
175203
)}
176-
components={{
177-
nextImage: Image,
178-
nextLink: Link,
179-
Code,
180-
Collection,
181-
Equation,
182-
Pdf,
183-
Modal,
184-
Tweet
185-
}}
204+
components={components}
186205
recordMap={recordMap}
187206
rootPageId={site.rootNotionPageId}
188207
rootDomain={site.domain}
@@ -197,14 +216,9 @@ export const NotionPage: React.FC<types.PageProps> = ({
197216
defaultPageCoverPosition={config.defaultPageCoverPosition}
198217
mapPageUrl={siteMapPageUrl}
199218
mapImageUrl={mapImageUrl}
200-
searchNotion={searchNotion}
219+
searchNotion={config.isSearchEnabled ? searchNotion : null}
201220
pageAside={pageAside}
202-
footer={
203-
<Footer
204-
isDarkMode={darkMode.value}
205-
toggleDarkMode={darkMode.toggle}
206-
/>
207-
}
221+
footer={<Footer />}
208222
/>
209223

210224
<GitHubShareButton />

components/NotionPageHeader.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react'
2+
import cs from 'classnames'
3+
import useDarkMode from '@fisch0920/use-dark-mode'
4+
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline'
5+
import { IoMoonSharp } from '@react-icons/all-files/io5/IoMoonSharp'
6+
7+
import { Header, Breadcrumbs, Search, useNotionContext } from 'react-notion-x'
8+
9+
import * as types from 'lib/types'
10+
import { navigationStyle, navigationLinks, isSearchEnabled } from 'lib/config'
11+
12+
import styles from './styles.module.css'
13+
14+
export const NotionPageHeader: React.FC<{
15+
block: types.CollectionViewPageBlock | types.PageBlock
16+
}> = ({ block }) => {
17+
const darkMode = useDarkMode(false, { classNameDark: 'dark-mode' })
18+
const [hasMounted, setHasMounted] = React.useState(false)
19+
const { components, mapPageUrl } = useNotionContext()
20+
21+
React.useEffect(() => {
22+
setHasMounted(true)
23+
}, [])
24+
25+
if (navigationStyle === 'default') {
26+
return <Header block={block} />
27+
}
28+
29+
return (
30+
<header className='notion-header'>
31+
<div className='notion-nav-header'>
32+
<Breadcrumbs block={block} rootOnly={true} />
33+
34+
<div className='notion-nav-header-rhs breadcrumbs'>
35+
{navigationLinks
36+
?.map((link, index) => {
37+
if (!link.pageId && !link.url) {
38+
return null
39+
}
40+
41+
if (link.pageId) {
42+
return (
43+
<components.PageLink
44+
href={mapPageUrl(link.pageId)}
45+
key={index}
46+
className={cs(styles.navLink, 'breadcrumb', 'button')}
47+
>
48+
{link.title}
49+
</components.PageLink>
50+
)
51+
} else {
52+
return (
53+
<components.Link
54+
href={link.url}
55+
key={index}
56+
className={cs(styles.navLink, 'breadcrumb', 'button')}
57+
>
58+
{link.title}
59+
</components.Link>
60+
)
61+
}
62+
})
63+
.filter(Boolean)}
64+
65+
<div
66+
className={cs('breadcrumb', 'button')}
67+
role='button'
68+
onClick={darkMode.toggle}
69+
>
70+
{hasMounted && darkMode.value ? (
71+
<IoMoonSharp />
72+
) : (
73+
<IoSunnyOutline />
74+
)}
75+
</div>
76+
77+
{isSearchEnabled && <Search block={block} title={null} />}
78+
</div>
79+
</div>
80+
</header>
81+
)
82+
}

lib/config.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
/**
22
* Site-wide app configuration.
33
*
4-
* This file pulls from the root "site.config.js" as well as environment variables
4+
* This file pulls from the root "site.config.ts" as well as environment variables
55
* for optional depenencies.
66
*/
77

88
import { parsePageId } from 'notion-utils'
99
import posthog from 'posthog-js'
1010
import { getEnv, getSiteConfig } from './get-config-value'
11-
import { PageUrlOverridesInverseMap, PageUrlOverridesMap } from './types'
11+
import { NavigationLink } from './site-config'
12+
import {
13+
PageUrlOverridesInverseMap,
14+
PageUrlOverridesMap,
15+
NavigationStyle
16+
} from './types'
1217

1318
export const rootNotionPageId: string = parsePageId(
1419
getSiteConfig('rootNotionPageId'),
@@ -48,9 +53,9 @@ export const description: string = getSiteConfig('description', 'Notion Blog')
4853

4954
// social accounts
5055
export const twitter: string | null = getSiteConfig('twitter', null)
51-
export const zhihu: string | null = getSiteConfig('zhihu', null)
5256
export const github: string | null = getSiteConfig('github', null)
5357
export const linkedin: string | null = getSiteConfig('linkedin', null)
58+
export const zhihu: string | null = getSiteConfig('zhihu', null)
5459

5560
// default notion values for site-wide consistency (optional; may be overridden on a per-page basis)
5661
export const defaultPageIcon: string | null = getSiteConfig(
@@ -78,12 +83,25 @@ export const isTweetEmbedSupportEnabled: boolean = getSiteConfig(
7883
true
7984
)
8085

81-
// where it all starts -- the site's root Notion page
86+
// Optional whether or not to include the Notion ID in page URLs or just use slugs
8287
export const includeNotionIdInUrls: boolean = getSiteConfig(
8388
'includeNotionIdInUrls',
8489
!!isDev
8590
)
8691

92+
export const navigationStyle: NavigationStyle = getSiteConfig(
93+
'navigationStyle',
94+
'default'
95+
)
96+
97+
export const navigationLinks: Array<NavigationLink | null> = getSiteConfig(
98+
'navigationLinks',
99+
null
100+
)
101+
102+
// Optional site search
103+
export const isSearchEnabled: boolean = getSiteConfig('isSearchEnabled', true)
104+
87105
// ----------------------------------------------------------------------------
88106

89107
// Optional redis instance for persisting preview images

lib/get-config-value.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import rawSiteConfig from '../site.config'
2+
import { SiteConfig } from './site-config'
23

34
if (!rawSiteConfig) {
4-
throw new Error(`Config error: invalid site.config.js`)
5+
throw new Error(`Config error: invalid site.config.ts`)
56
}
67

7-
// TODO: allow environment variables to override site.config.js
8-
let siteConfigOverrides
8+
// allow environment variables to override site.config.ts
9+
let siteConfigOverrides: SiteConfig
910

1011
try {
1112
if (process.env.NEXT_PUBLIC_SITE_CONFIG) {
@@ -16,7 +17,7 @@ try {
1617
throw err
1718
}
1819

19-
const siteConfig = {
20+
const siteConfig: SiteConfig = {
2021
...rawSiteConfig,
2122
...siteConfigOverrides
2223
}

lib/map-page-url.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ const uuid = !!includeNotionIdInUrls
1212
export const mapPageUrl =
1313
(site: Site, recordMap: ExtendedRecordMap, searchParams: URLSearchParams) =>
1414
(pageId = '') => {
15-
if (uuidToId(pageId) === site.rootNotionPageId) {
15+
const pageUuid = parsePageId(pageId, { uuid: true })
16+
17+
if (uuidToId(pageUuid) === site.rootNotionPageId) {
1618
return createUrl('/', searchParams)
1719
} else {
1820
return createUrl(
19-
`/${getCanonicalPageId(pageId, recordMap, { uuid })}`,
21+
`/${getCanonicalPageId(pageUuid, recordMap, { uuid })}`,
2022
searchParams
2123
)
2224
}

0 commit comments

Comments
 (0)