feat: initial webapp structure from notion2site

This commit is contained in:
Travis Fischer
2021-01-15 11:31:09 -05:00
parent 253400fba9
commit 5765d3f5bc
47 changed files with 1894 additions and 68 deletions

34
components/CustomFont.tsx Normal file
View 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
View 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
View 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
View 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>
)

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
export * from './NotionPage'
export * from './Page404'
export * from './ErrorPage'

View 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
View 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>
)
}