mirror of
https://github.com/d0zingcat/nextjs-notion-starter-kit.git
synced 2026-05-13 15:09:47 +00:00
feat: initial webapp structure from notion2site
This commit is contained in:
34
components/CustomFont.tsx
Normal file
34
components/CustomFont.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import Head from 'next/head'
|
||||
import * as React from 'react'
|
||||
import * as types from '../lib/types'
|
||||
|
||||
export const CustomFont: React.FC<{ site: types.Site }> = ({ site }) => {
|
||||
if (!site.fontFamily) {
|
||||
return null
|
||||
}
|
||||
|
||||
// https://developers.google.com/fonts/docs/css2
|
||||
const fontFamilies = [site.fontFamily]
|
||||
const googleFontFamilies = fontFamilies
|
||||
.map((font) => font.replace(/ /g, '+'))
|
||||
.map((font) => `family=${font}:ital,wght@0,200..700;1,200..700`)
|
||||
.join('&')
|
||||
const googleFontsLink = `https://fonts.googleapis.com/css?${googleFontFamilies}&display=swap`
|
||||
const cssFontFamilies = fontFamilies.map((font) => `"${font}"`).join(', ')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel='stylesheet' href={googleFontsLink} />
|
||||
|
||||
<style>{`
|
||||
.notion.notion-app {
|
||||
font-family: ${cssFontFamilies}, -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif,
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
`}</style>
|
||||
</Head>
|
||||
</>
|
||||
)
|
||||
}
|
||||
11
components/CustomHtml.tsx
Normal file
11
components/CustomHtml.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as React from 'react'
|
||||
import InnerHTML from 'dangerously-set-html-content'
|
||||
import * as types from '../lib/types'
|
||||
|
||||
export const CustomHtml: React.FC<{ site: types.Site }> = ({ site }) => {
|
||||
if (!site.html) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <InnerHTML html={site.html} />
|
||||
}
|
||||
35
components/ErrorPage.tsx
Normal file
35
components/ErrorPage.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import Head from 'next/head'
|
||||
import { defaultSiteFavicon } from 'lib/config'
|
||||
import { PageHead } from './PageHead'
|
||||
|
||||
import styles from './styles.module.css'
|
||||
|
||||
export const ErrorPage: React.FC<{ statusCode: number }> = ({ statusCode }) => {
|
||||
const title = 'Error'
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead />
|
||||
|
||||
<Head>
|
||||
<link rel='shortcut icon' href={defaultSiteFavicon} />
|
||||
|
||||
<meta property='og:site_name' content={title} />
|
||||
<meta property='og:title' content={title} />
|
||||
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<h1>Error Loading Page</h1>
|
||||
|
||||
{statusCode && <p>Error code: {statusCode}</p>}
|
||||
|
||||
<img src='/error.png' alt='Error' className={styles.errorImage} />
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
10
components/Loading.tsx
Normal file
10
components/Loading.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react'
|
||||
import { LoadingIcon } from './LoadingIcon'
|
||||
|
||||
import styles from './styles.module.css'
|
||||
|
||||
export const Loading: React.FC = () => (
|
||||
<div className={styles.container}>
|
||||
<LoadingIcon />
|
||||
</div>
|
||||
)
|
||||
61
components/LoadingIcon.tsx
Normal file
61
components/LoadingIcon.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as React from 'react'
|
||||
import cs from 'classnames'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
export const LoadingIcon = (props) => {
|
||||
const { className, ...rest } = props
|
||||
return (
|
||||
<svg
|
||||
className={cs(styles.loadingIcon, className)}
|
||||
{...rest}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
x1='28.1542969%'
|
||||
y1='63.7402344%'
|
||||
x2='74.6289062%'
|
||||
y2='17.7832031%'
|
||||
id='linearGradient-1'
|
||||
>
|
||||
<stop stopColor='rgba(164, 164, 164, 1)' offset='0%' />
|
||||
<stop
|
||||
stopColor='rgba(164, 164, 164, 0)'
|
||||
stopOpacity='0'
|
||||
offset='100%'
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g id='Page-1' stroke='none' strokeWidth='1' fill='none'>
|
||||
<g transform='translate(-236.000000, -286.000000)'>
|
||||
<g transform='translate(238.000000, 286.000000)'>
|
||||
<circle
|
||||
id='Oval-2'
|
||||
stroke='url(#linearGradient-1)'
|
||||
strokeWidth='4'
|
||||
cx='10'
|
||||
cy='12'
|
||||
r='10'
|
||||
/>
|
||||
<path
|
||||
d='M10,2 C4.4771525,2 0,6.4771525 0,12'
|
||||
id='Oval-2'
|
||||
stroke='rgba(164, 164, 164, 1)'
|
||||
strokeWidth='4'
|
||||
/>
|
||||
<rect
|
||||
id='Rectangle-1'
|
||||
fill='rgba(164, 164, 164, 1)'
|
||||
x='8'
|
||||
y='0'
|
||||
width='4'
|
||||
height='4'
|
||||
rx='8'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
126
components/NotionPage.tsx
Normal file
126
components/NotionPage.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import * as React from 'react'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useLocalStorage, useSearchParam } from 'react-use'
|
||||
import BodyClassName from 'react-body-classname'
|
||||
import isUrl from 'is-url-superb'
|
||||
|
||||
// core notion renderer
|
||||
import { NotionRenderer } from 'react-notion-x'
|
||||
|
||||
// utils
|
||||
import { getBlockTitle } from 'notion-utils'
|
||||
import * as types from 'lib/types'
|
||||
import { mapPageUrl, getCanonicalPageUrl } from 'lib/map-page-url'
|
||||
import { mapNotionImageUrl } from 'lib/map-image-url'
|
||||
// import { isDev } from 'lib/config'
|
||||
|
||||
// components
|
||||
import { CustomFont } from './CustomFont'
|
||||
import { CustomHtml } from './CustomHtml'
|
||||
import { Loading } from './Loading'
|
||||
import { Page404 } from './Page404'
|
||||
import { PageHead } from './PageHead'
|
||||
|
||||
import styles from './styles.module.css'
|
||||
|
||||
const isServer = typeof window === 'undefined'
|
||||
|
||||
export const NotionPage: React.FC<types.PageProps> = ({
|
||||
site,
|
||||
recordMap,
|
||||
error,
|
||||
pageId
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
|
||||
const dark = useSearchParam('dark')
|
||||
const lite = useSearchParam('lite')
|
||||
|
||||
const params: any = {}
|
||||
if (dark) params.dark = dark
|
||||
if (lite) params.lite = lite
|
||||
|
||||
const searchParams = new URLSearchParams(params)
|
||||
|
||||
// TODO: add ability to toggle dark mode
|
||||
const [darkMode, setDarkMode] = useLocalStorage(
|
||||
'notionx-dark-mode',
|
||||
!!site?.darkMode
|
||||
)
|
||||
|
||||
const isLiteMode = lite === 'true'
|
||||
const isDarkMode = dark !== null ? dark === 'true' : darkMode
|
||||
|
||||
if (router.isFallback) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
const keys = Object.keys(recordMap?.block || {})
|
||||
const block = recordMap?.block?.[keys[0]]?.value
|
||||
|
||||
if (error || !site || !keys.length || !block) {
|
||||
return <Page404 site={site} pageId={pageId} error={error} />
|
||||
}
|
||||
|
||||
const title = getBlockTitle(block, recordMap) || site.name
|
||||
let notionIcon = (block.format as any)?.page_icon
|
||||
|
||||
if (notionIcon && isUrl(notionIcon)) {
|
||||
notionIcon = mapNotionImageUrl(notionIcon, block)
|
||||
}
|
||||
|
||||
const icon = notionIcon
|
||||
const iconUrl = (icon && isUrl(icon)) ?? icon
|
||||
|
||||
console.log('notion page', {
|
||||
// isDev,
|
||||
rootNotionPageId: site.rootNotionPageId,
|
||||
pageId,
|
||||
recordMap
|
||||
})
|
||||
|
||||
if (!isServer) {
|
||||
;(window as any).recordMap = recordMap
|
||||
;(window as any).block = block
|
||||
}
|
||||
|
||||
const siteMapPageUrl = mapPageUrl(site, recordMap, searchParams)
|
||||
|
||||
// const canonicalPageUrl =
|
||||
// !isDev && getCanonicalPageUrl(site, recordMap)(pageId)
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead site={site} />
|
||||
|
||||
<Head>
|
||||
{/* {iconUrl && <link rel='shortcut icon' href={iconUrl} />} */}
|
||||
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:site_name' content={site.name} />
|
||||
|
||||
{/* {canonicalPageUrl && <link rel='canonical' href={canonicalPageUrl} />} */}
|
||||
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
|
||||
<CustomFont site={site} />
|
||||
|
||||
{isLiteMode && <BodyClassName className='notion-lite' />}
|
||||
|
||||
<NotionRenderer
|
||||
bodyClassName={styles.notion}
|
||||
recordMap={recordMap}
|
||||
fullPage={!isLiteMode}
|
||||
darkMode={isDarkMode}
|
||||
previewImages={site.previewImages !== false}
|
||||
mapPageUrl={siteMapPageUrl}
|
||||
mapImageUrl={mapNotionImageUrl}
|
||||
rootPageId={site.rootNotionPageId}
|
||||
/>
|
||||
|
||||
<CustomHtml site={site} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
48
components/Page404.tsx
Normal file
48
components/Page404.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import Head from 'next/head'
|
||||
import * as React from 'react'
|
||||
import * as types from 'lib/types'
|
||||
import { defaultSiteFavicon } from 'lib/config'
|
||||
import { PageHead } from './PageHead'
|
||||
|
||||
import styles from './styles.module.css'
|
||||
|
||||
export const Page404: React.FC<types.PageProps> = ({ site, pageId, error }) => {
|
||||
const title = site?.name || 'Notion Page Not Found'
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead site={site} />
|
||||
|
||||
<Head>
|
||||
<link rel='shortcut icon' href={defaultSiteFavicon} />
|
||||
|
||||
<meta property='og:site_name' content={title} />
|
||||
<meta property='og:title' content={title} />
|
||||
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<h1>Notion Page Not Found</h1>
|
||||
|
||||
{error ? (
|
||||
<p>{error.message}</p>
|
||||
) : (
|
||||
pageId && (
|
||||
<p>
|
||||
Make sure that Notion page "{pageId}" is publicly accessible.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
|
||||
<img
|
||||
src='/404.png'
|
||||
alt='404 Not Found'
|
||||
className={styles.errorImage}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
35
components/PageHead.tsx
Normal file
35
components/PageHead.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Head from 'next/head'
|
||||
import * as React from 'react'
|
||||
import * as types from 'lib/types'
|
||||
import { mapImageUrl } from 'lib/map-image-url'
|
||||
|
||||
export const PageHead: React.FC<types.PageProps> = ({ site }) => {
|
||||
return (
|
||||
<Head>
|
||||
<meta charSet='utf-8' />
|
||||
<meta httpEquiv='Content-Type' content='text/html; charset=utf-8' />
|
||||
<meta
|
||||
name='viewport'
|
||||
content='width=device-width, initial-scale=1, shrink-to-fit=no'
|
||||
/>
|
||||
|
||||
{site?.description && (
|
||||
<>
|
||||
<meta name='description' content={site.description} />
|
||||
<meta property='og:description' content={site.description} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{site?.image && (
|
||||
<meta property='og:image' content={mapImageUrl(site.image)} />
|
||||
)}
|
||||
|
||||
<meta name='theme-color' content='#EB625A' />
|
||||
<meta property='og:type' content='website' />
|
||||
|
||||
{site?.domain && (
|
||||
<meta property='og:url' content={`https://${site.domain}`} />
|
||||
)}
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
3
components/index.ts
Normal file
3
components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './NotionPage'
|
||||
export * from './Page404'
|
||||
export * from './ErrorPage'
|
||||
145
components/styles.module.css
Normal file
145
components/styles.module.css
Normal file
@@ -0,0 +1,145 @@
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: rgb(55, 53, 47);
|
||||
caret-color: rgb(55, 53, 47);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica,
|
||||
'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
|
||||
.loadingIcon {
|
||||
animation: spinner 0.6s linear infinite;
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: rgba(55, 53, 47, 0.4);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.errorImage {
|
||||
max-width: 100%;
|
||||
width: 640px;
|
||||
}
|
||||
|
||||
.notion {
|
||||
padding-bottom: calc(max(10vh, 120px));
|
||||
}
|
||||
|
||||
.demoFooter {
|
||||
position: fixed;
|
||||
z-index: 800;
|
||||
bottom: 1em;
|
||||
left: 10vw;
|
||||
right: 10vw;
|
||||
border-radius: 4px;
|
||||
background: #eb625a;
|
||||
padding: 16px 24px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.demoLhs a,
|
||||
.demoLhs a:visited,
|
||||
.demoLhs a:hover,
|
||||
.demoLhs a:active {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.demoLhs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demoTitle {
|
||||
font-size: 1.2em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
padding-right: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
border-right: 1px solid #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.demoTitle img {
|
||||
height: 1.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.demoDesc {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.demoCta {
|
||||
display: block;
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.9em;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
padding: 8px 12px;
|
||||
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
transition: all 300ms ease;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.demoCta:hover {
|
||||
background: #fff;
|
||||
color: #eb625a;
|
||||
|
||||
transition: all 200ms ease;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.demoCta:active {
|
||||
transition: all 100ms ease;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 740px) {
|
||||
.demoFooter {
|
||||
left: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.demoTitle {
|
||||
border: 0 none;
|
||||
}
|
||||
|
||||
.demoDesc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
34
components/temp
Normal file
34
components/temp
Normal file
@@ -0,0 +1,34 @@
|
||||
pageLink: ({ href = '', ...rest }) => {
|
||||
const parts = href
|
||||
.split('?')[0]
|
||||
.split('/')
|
||||
.filter((p: string) => !!p.trim())
|
||||
let pagesPath =
|
||||
parts.length <= 1 ? '/[domain]' : '/[domain]/[pageId]'
|
||||
let as = href
|
||||
|
||||
if (isDemo) {
|
||||
pagesPath = '/[domain]'
|
||||
} else if (isDev) {
|
||||
// localhost
|
||||
} else {
|
||||
// prod, non-demo
|
||||
as = `/${site.domain}${href}`
|
||||
}
|
||||
|
||||
console.log({ href, parts, domain: site.domain, as })
|
||||
// const MyButton = React.forwardRef(
|
||||
// ({ href: href2, ...rest }, ref) => {
|
||||
// return (
|
||||
// <a {...rest} href={href2} ref={ref}>
|
||||
// Click Me
|
||||
// </a>
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
return (
|
||||
<Link href={pagesPath} as={as}>
|
||||
<a href={as} {...rest} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user