307 lines
9.6 KiB
TypeScript
307 lines
9.6 KiB
TypeScript
import { type BlockObjectResponse } from '@notionhq/client/build/src/api-endpoints'
|
|
import clsx from 'clsx'
|
|
import Image from 'next/image'
|
|
import { type FC } from 'react'
|
|
|
|
import Callout, { CalloutIcon } from '@/components/server/Callout'
|
|
import CodeSnippet, { SyncCodeSnippet } from '@/components/server/CodeSnippet'
|
|
import List from '@/components/server/List'
|
|
import RichText from '@/components/server/RichText'
|
|
import TeX, { SyncTeX } from '@/components/server/TeX'
|
|
import { type Block, type BlockType } from '@/lib/notion/types'
|
|
import extractCurrentListItems from '@/lib/notion/utils/extractCurrentListItems'
|
|
import getProxiedAssetUrl from '@/lib/notion/utils/getProxiedAssetUrl'
|
|
import richTextAsPlainText from '@/lib/notion/utils/richTextToPlainText'
|
|
|
|
export const blockRenderMap: {
|
|
[T in BlockType]: FC<{ block: Block<T> }>
|
|
} = {
|
|
paragraph: ({ block }) => (
|
|
<p className="mb-6 text-justify">
|
|
<RichText richText={block.paragraph.rich_text} />
|
|
</p>
|
|
),
|
|
heading_1: ({ block }) => (
|
|
<>
|
|
<h2
|
|
id={block.id}
|
|
className="font-display mb-6 text-4xl before:-mt-40 before:block before:h-40"
|
|
>
|
|
<RichText richText={block.heading_1.rich_text} />
|
|
</h2>
|
|
{/* @ts-expect-error TODO find a way to fix this type */}
|
|
{block.has_children && <RenderBlockList blockList={block.children} />}
|
|
</>
|
|
),
|
|
heading_2: ({ block }) => (
|
|
<>
|
|
<h3
|
|
id={block.id}
|
|
className="font-display mb-5 text-2xl before:-mt-40 before:block before:h-40"
|
|
>
|
|
<RichText richText={block.heading_2.rich_text} />
|
|
</h3>
|
|
{/* @ts-expect-error TODO find a way to fix this type */}
|
|
{block.has_children && <RenderBlockList blockList={block.children} />}
|
|
</>
|
|
),
|
|
heading_3: ({ block }) => (
|
|
<>
|
|
<h4
|
|
id={block.id}
|
|
className="font-display mb-5 text-xl before:-mt-40 before:block before:h-40"
|
|
>
|
|
<RichText richText={block.heading_3.rich_text} />
|
|
</h4>
|
|
{/* @ts-expect-error TODO find a way to fix this type */}
|
|
{block.has_children && <RenderBlockList blockList={block.children} />}
|
|
</>
|
|
),
|
|
image: ({ block }) => {
|
|
const caption = block.image.caption
|
|
const altText = caption[0]?.plain_text.startsWith('alt: ')
|
|
? richTextAsPlainText(caption).substring(5)
|
|
: richTextAsPlainText(caption)
|
|
return (
|
|
<figure className="mb-6">
|
|
<Image
|
|
alt={altText}
|
|
className={clsx('h-auto w-full rounded object-cover', {
|
|
'mb-2': block.image.caption.length > 0,
|
|
})}
|
|
src={getProxiedAssetUrl({
|
|
blockId: block.id,
|
|
lastEditedTime: block.last_edited_time,
|
|
})}
|
|
width={9999} // TODO
|
|
height={9999} // TODO
|
|
/>
|
|
{block.image.caption.length > 0 && (
|
|
<figcaption className="text-sm font-light">
|
|
<RichText richText={block.image.caption} />
|
|
</figcaption>
|
|
)}
|
|
</figure>
|
|
)
|
|
},
|
|
bulleted_list_item: ({ block }) => (
|
|
<li>
|
|
<RichText richText={block.bulleted_list_item.rich_text} />
|
|
</li>
|
|
),
|
|
numbered_list_item: ({ block }) => (
|
|
<li>
|
|
<RichText richText={block.numbered_list_item.rich_text} />
|
|
</li>
|
|
),
|
|
quote: ({ block }) => (
|
|
<blockquote className="border-l-1 mb-6 bg-gradient-to-r from-[#171717] px-10 py-7 text-justify dark:[border-image:linear-gradient(to_bottom,rgba(255,255,255,1),rgba(255,255,255,0))_1]">
|
|
<RichText richText={block.quote.rich_text} />
|
|
</blockquote>
|
|
),
|
|
toggle: ({ block }) => (
|
|
<details>
|
|
<summary>
|
|
<RichText richText={block.toggle.rich_text} />
|
|
</summary>
|
|
{/* @ts-expect-error TODO find a way to fix this type */}
|
|
{block.children && <RenderBlockList blockList={block.children} />}
|
|
</details>
|
|
),
|
|
callout: ({ block }) => (
|
|
<Callout>
|
|
<CalloutIcon icon={block.callout.icon} />
|
|
<RichText richText={block.callout.rich_text} />
|
|
</Callout>
|
|
),
|
|
// @ts-expect-error Server Component
|
|
equation: ({ block }) => <TeX block math={block.equation.expression} />,
|
|
divider: () => (
|
|
<hr className="my-9 [border-image:linear-gradient(to_right,#424242,#FFFFFF00)_1]" />
|
|
),
|
|
code: ({ block }) => (
|
|
// @ts-expect-error Server Component
|
|
<CodeSnippet
|
|
language={block.code.language}
|
|
codeRichText={block.code.rich_text}
|
|
/>
|
|
),
|
|
|
|
// TODO?
|
|
table_of_contents: ({ block }) => <></>,
|
|
column_list: ({ block }) => <></>,
|
|
column: ({ block }) => <></>,
|
|
link_to_page: ({ block }) => <></>,
|
|
table: ({ block }) => <></>,
|
|
table_row: ({ block }) => <></>,
|
|
embed: ({ block }) => <></>,
|
|
bookmark: ({ block }) => <></>,
|
|
video: ({ block }) => <></>,
|
|
pdf: ({ block }) => <></>,
|
|
file: ({ block }) => <></>,
|
|
audio: ({ block }) => <></>,
|
|
link_preview: ({ block }) => <></>,
|
|
breadcrumb: ({ block }) => <></>,
|
|
synced_block: ({ block }) => <></>,
|
|
template: ({ block }) => <></>,
|
|
to_do: ({ block }) => <></>,
|
|
child_page: ({ block }) => <></>,
|
|
child_database: ({ block }) => <></>,
|
|
unsupported: ({ block }) => <></>,
|
|
}
|
|
|
|
export const rssBlockRenderMap: typeof blockRenderMap = {
|
|
...blockRenderMap,
|
|
paragraph: ({ block }) => (
|
|
<p className="mb-6 text-justify">
|
|
<RichText rss richText={block.paragraph.rich_text} />
|
|
</p>
|
|
),
|
|
heading_1: ({ block }) => (
|
|
<>
|
|
<h2
|
|
id={block.id}
|
|
className="font-display mb-6 text-4xl before:-mt-40 before:block before:h-40"
|
|
>
|
|
<RichText rss richText={block.heading_1.rich_text} />
|
|
</h2>
|
|
{/* @ts-expect-error TODO find a way to fix this type */}
|
|
{block.has_children && <RenderBlockList blockList={block.children} />}
|
|
</>
|
|
),
|
|
heading_2: ({ block }) => (
|
|
<>
|
|
<h3
|
|
id={block.id}
|
|
className="font-display mb-5 text-2xl before:-mt-40 before:block before:h-40"
|
|
>
|
|
<RichText rss richText={block.heading_2.rich_text} />
|
|
</h3>
|
|
{/* @ts-expect-error TODO find a way to fix this type */}
|
|
{block.has_children && <RenderBlockList blockList={block.children} />}
|
|
</>
|
|
),
|
|
heading_3: ({ block }) => (
|
|
<>
|
|
<h4
|
|
id={block.id}
|
|
className="font-display mb-5 text-xl before:-mt-40 before:block before:h-40"
|
|
>
|
|
<RichText rss richText={block.heading_3.rich_text} />
|
|
</h4>
|
|
{/* @ts-expect-error TODO find a way to fix this type */}
|
|
{block.has_children && <RenderBlockList blockList={block.children} />}
|
|
</>
|
|
),
|
|
image: ({ block }) => {
|
|
const caption = block.image.caption
|
|
const altText = caption[0]?.plain_text.startsWith('alt: ')
|
|
? richTextAsPlainText(caption).substring(5)
|
|
: richTextAsPlainText(caption)
|
|
return (
|
|
<figure className="mb-6">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
alt={altText}
|
|
className={clsx('h-auto w-full rounded object-cover', {
|
|
'mb-2': block.image.caption.length > 0,
|
|
})}
|
|
src={getProxiedAssetUrl({
|
|
blockId: block.id,
|
|
lastEditedTime: block.last_edited_time,
|
|
withBaseUrl: true,
|
|
})}
|
|
/>
|
|
{block.image.caption.length > 0 && (
|
|
<figcaption className="text-sm font-light">
|
|
<RichText rss richText={block.image.caption} />
|
|
</figcaption>
|
|
)}
|
|
</figure>
|
|
)
|
|
},
|
|
bulleted_list_item: ({ block }) => (
|
|
<li>
|
|
<RichText rss richText={block.bulleted_list_item.rich_text} />
|
|
</li>
|
|
),
|
|
numbered_list_item: ({ block }) => (
|
|
<li>
|
|
<RichText rss richText={block.numbered_list_item.rich_text} />
|
|
</li>
|
|
),
|
|
quote: ({ block }) => (
|
|
<blockquote className="border-l-1 mb-6 bg-gradient-to-r from-[#171717] px-10 py-7 text-justify dark:[border-image:linear-gradient(to_bottom,rgba(255,255,255,1),rgba(255,255,255,0))_1]">
|
|
<RichText rss richText={block.quote.rich_text} />
|
|
</blockquote>
|
|
),
|
|
toggle: ({ block }) => (
|
|
<details>
|
|
<summary>
|
|
<RichText rss richText={block.toggle.rich_text} />
|
|
</summary>
|
|
{/* @ts-expect-error TODO find a way to fix this type */}
|
|
{block.children && <RenderBlockList blockList={block.children} />}
|
|
</details>
|
|
),
|
|
callout: ({ block }) => (
|
|
<Callout>
|
|
<CalloutIcon icon={block.callout.icon} />
|
|
<RichText rss richText={block.callout.rich_text} />
|
|
</Callout>
|
|
),
|
|
equation: ({ block }) => <SyncTeX block math={block.equation.expression} />,
|
|
code: ({ block }) => (
|
|
<SyncCodeSnippet
|
|
language={block.code.language}
|
|
codeRichText={block.code.rich_text}
|
|
/>
|
|
),
|
|
}
|
|
|
|
export const RenderBlockList = ({
|
|
blockList,
|
|
rss = false,
|
|
}: {
|
|
blockList: BlockObjectResponse[]
|
|
rss?: boolean
|
|
}) => {
|
|
return (
|
|
<>
|
|
{blockList.map((block, index, list) => {
|
|
const isListItem =
|
|
block.type === 'bulleted_list_item' ||
|
|
block.type === 'numbered_list_item'
|
|
|
|
if (!isListItem)
|
|
return <RenderBlock rss={rss} key={block.id} block={block} />
|
|
else if (list[index - 1].type !== block.type)
|
|
return (
|
|
<List
|
|
key={block.id}
|
|
as={block.type === 'bulleted_list_item' ? 'ul' : 'ol'}
|
|
>
|
|
{extractCurrentListItems({ list, currentIndex: index }).map(
|
|
(listItem) => (
|
|
<RenderBlock rss={rss} key={listItem.id} block={listItem} />
|
|
),
|
|
)}
|
|
</List>
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export function RenderBlock({
|
|
block,
|
|
rss = false,
|
|
}: {
|
|
block: BlockObjectResponse
|
|
rss?: boolean
|
|
}) {
|
|
const Component = (rss ? rssBlockRenderMap : blockRenderMap)[block.type]
|
|
// @ts-expect-error we know this is correct because `block.type` is the name we want to use as a key for `blockRenderMap`
|
|
return <Component block={block} />
|
|
}
|