import Link from 'next/link' ; import fetch from 'node-fetch' ; import { useRouter } from 'next/router' ; import Header from '../../components/header' ; import Heading from '../../components/heading' ; import components from '../../components/dynamic' ; import ReactJSXParser from '@zeit/react-jsx-parser' ; import blogStyles from '../../styles/blog.module.css' ; import { textBlock } from '../../lib/notion/renderers' ; import getPageData from '../../lib/notion/getPageData' ; import React , { CSSProperties , useEffect } from 'react' ; import getBlogIndex from '../../lib/notion/getBlogIndex' ; import getNotionUsers from '../../lib/notion/getNotionUsers' ; import { getBlogLink , getDateStr } from '../../lib/blog-helpers' ; export async function getStaticProps ( { params : { slug } , preview } ) { const postsTable = await getBlogIndex ( ) ; const post = postsTable [ slug ] ; if ( ! post || ( post . Published !== 'Yes' && ! preview ) ) { console . log ( `Failed to find post for slug: ${ slug }` ) ; return { props : { redirect : '/blog' , preview : false , } , unstable_revalidate : 5 , } ; } const postData = await getPageData ( post . id ) ; post . content = postData . blocks ; for ( let i = 0 ; i < postData . blocks . length ; i ++ ) { const { value } = postData . blocks [ i ] ; const { type , properties } = value ; if ( type == 'tweet' ) { const src = properties . source [ 0 ] [ 0 ] ; const tweetId = src . split ( '/' ) [ 5 ] . split ( '?' ) [ 0 ] ; if ( ! tweetId ) continue ; try { const res = await fetch ( `https://api.twitter.com/1/statuses/oembed.json?id=${ tweetId }` ) ; const json = await res . json ( ) ; properties . html = json . html . split ( '<script' ) [ 0 ] ; post . hasTweet = true ; } catch ( _ ) { console . log ( `Failed to get tweet embed for ${ src }` ) ; } } } const { users } = await getNotionUsers ( post . Authors || [ ] ) ; post . Authors = Object . keys ( users ) . map ( ( id ) => users [ id ] . full_name ) ; return { props : { post , preview : preview || false , } , revalidate : 10 , } ; } export async function getStaticPaths ( ) { const postsTable = await getBlogIndex ( ) ; return { paths : Object . keys ( postsTable ) . filter ( ( post ) => postsTable [ post ] . Published === 'Yes' ) . map ( ( slug ) => getBlogLink ( slug ) ) , fallback : true , } ; } const listTypes = new Set ( [ 'bulleted_list' , 'numbered_list' ] ) ; const RenderPost = ( { post , redirect , preview } ) => { const router = useRouter ( ) ; let listTagName = null ; let listLastId = null ; let listMap = { } ; useEffect ( ( ) => { const twitterSrc = 'https://platform.twitter.com/widgets.js' ; if ( post && post . hasTweet ) { if ( ( window as any ) ?. twttr ?. widgets ) { ; ( window as any ) . twttr . widgets . load ( ) ; } else if ( ! document . querySelector ( `script[src="${ twitterSrc }"]` ) ) { const script = document . createElement ( 'script' ) ; script . async = true ; script . src = twitterSrc ; document . querySelector ( 'body' ) . appendChild ( script ) ; } } } , [ ] ) ; useEffect ( ( ) => { if ( redirect && ! post ) { router . replace ( redirect ) ; } } , [ redirect , post ] ) ; if ( router . isFallback ) { return < div > Loading ... < /div>; } if ( ! post ) { return ( < div className = { blogStyles . post } > < p > Woops ! didn 't find that post, redirecting you back to the blog index < /p> < /div>); } return ( < > < Header titlePre = { post . Page } / > { preview && ( < div className = { blogStyles . previewAlertContainer } > < div className = { blogStyles . previewAlert } > < b > Note : < /b> { ` ` } Viewing in preview mode { ' ' } < Link href = { `/api/clear-preview?slug=${ post . Slug }` } > < button className = { blogStyles . escapePreview } > Exit Preview < /button> < /Link> < /div> < /div>)} < div className = { blogStyles . post } > < h1 > { post . Page || '' } < /h1> { post . Authors . length > 0 && ( < div className = "authors" > By : { post . Authors . join ( ' ' ) } < /div>)} { post . Date && ( < div className = "posted" > Posted : { getDateStr ( post . Date ) } < /div>)} < hr / > { ( ! post . content || post . content . length === 0 ) && ( < p > This post has no content < /p>)} { ( post . content || [ ] ) . map ( ( block , blockIdx ) => { const { value } = block ; const { type , properties , id , parent_id } = value ; const isLast = blockIdx === post . content . length - 1 ; const isList = listTypes . has ( type ) ; let toRender = [ ] ; if ( isList ) { listTagName = components [ type === 'bulleted_list' ? 'ul' : 'ol' ] ; listLastId = `list${ id }` ; listMap [ id ] = { key : id , nested : [ ] , children : textBlock ( properties . title , true , id ) , } ; if ( listMap [ parent_id ] ) { listMap [ id ] . isNested = true ; listMap [ parent_id ] . nested . push ( id ) ; } } if ( listTagName && ( isLast || ! isList ) ) { toRender . push ( React . createElement ( listTagName , { key : listLastId ! } , Object . keys ( listMap ) . map ( ( itemId ) => { if ( listMap [ itemId ] . isNested ) return null ; const createEl = ( item ) => React . createElement ( components . li || 'ul' , { key : item . key } , item . children , item . nested . length > 0 ? React . createElement ( components . ul || 'ul' , { key : item + 'sub-list' } , item . nested . map ( ( nestedId ) => createEl ( listMap [ nestedId ] ) ) ) : null ) ; return createEl ( listMap [ itemId ] ) ; } ) ) ) ; listMap = { } ; listLastId = null ; listTagName = null ; } const renderHeading = ( Type ) => { toRender . push ( < Heading key = { id } > < Type key = { id } > { textBlock ( properties . title , true , id ) } < /Type> < /Heading>); } ; const renderBookmark = ( { link , title , description , format } ) => { const { bookmark_icon : icon , bookmark_cover : cover } = format ; toRender . push ( < div className = { blogStyles . bookmark } > < div > < div style = { { display : 'flex' } } > < a target = "_blank" rel = "noopener noreferrer" className = { blogStyles . bookmarkContentsWrapper } href = { link } > < div role = "button" className = { blogStyles . bookmarkContents } > < div className = { blogStyles . bookmarkInfo } > < div className = { blogStyles . bookmarkTitle } > { title } < /div> < div className = { blogStyles . bookmarkDescription } > { description } < /div> < div className = { blogStyles . bookmarkLinkWrapper } > < img src = { icon } className = { blogStyles . bookmarkLinkIcon } / > < div className = { blogStyles . bookmarkLink } > { link } < /div> < /div> < /div> < div className = { blogStyles . bookmarkCoverWrapper1 } > < div className = { blogStyles . bookmarkCoverWrapper2 } > < div className = { blogStyles . bookmarkCoverWrapper3 } > < img src = { cover } className = { blogStyles . bookmarkCover } / > < /div> < /div> < /div> < /div> < /a> < /div> < /div> < /div>); } ; switch ( type ) { case 'page' : case 'divider' : break ; case 'text' : if ( properties ) { toRender . push ( textBlock ( properties . title , false , id ) ) ; } break ; case 'image' : case 'video' : case 'embed' : { const { format = { } } = value ; const { block_width , block_height , display_source , block_aspect_ratio , } = format ; const baseBlockWidth = 768 ; const roundFactor = Math . pow ( 10 , 2 ) ; const width = block_width ? `${ Math . round ( ( block_width / baseBlockWidth ) * 100 * roundFactor ) / roundFactor }%` : block_height || '100%' ; const isImage = type === 'image' ; const Comp = isImage ? 'img' : 'video' ; const useWrapper = block_aspect_ratio && ! block_height ; const childStyle = useWrapper ? { width : '100%' , height : '100%' , border : 'none' , position : 'absolute' , top : 0 , } : { width , border : 'none' , height : block_height , display : 'block' , maxWidth : '100%' , } ; let child = null ; if ( ! isImage && ! value . file_ids ) { child = ( < iframe style = { childStyle } src = { display_source } key = { ! useWrapper ? id : undefined } className = { ! useWrapper ? 'asset-wrapper' : undefined } />); } else { child = ( < Comp key = { ! useWrapper ? id : undefined } src = { `/api/asset?assetUrl=${ encodeURIComponent ( display_source as any ) }&blockId=${ id }` } controls = { ! isImage } alt = { `An ${ isImage ? 'image' : 'video' } from Notion` } loop = { ! isImage } muted = { ! isImage } autoPlay = { ! isImage } style = { childStyle } / > ) ; } toRender . push ( useWrapper ? ( < div style = { { paddingTop : `${ Math . round ( block_aspect_ratio * 100 ) }%` , position : 'relative' , } } className = "asset-wrapper" key = { id } > { child } < /div>) : (child)); break ; } case 'header' : renderHeading ( 'h1' ) ; break ; case 'sub_header' : renderHeading ( 'h2' ) ; break ; case 'sub_sub_header' : renderHeading ( 'h3' ) ; break ; case 'bookmark' : const { link , title , description } = properties ; const { format = { } } = value ; renderBookmark ( { link , title , description , format } ) ; break ; case 'code' : { if ( properties . title ) { const content = properties . title [ 0 ] [ 0 ] ; const language = properties . language [ 0 ] [ 0 ] ; if ( language === 'LiveScript' ) { toRender . push ( < ReactJSXParser key = { id } jsx = { content } components = { components } componentsOnly = { false } renderInpost = { false } allowUnknownElements = { true } blacklistedTags = { [ 'script' , 'style' ] } / > ) ; } else { toRender . push ( < components . Code key = { id } language = { language || '' } > { content } < /components.Code>); } } break ; } case 'quote' : { if ( properties . title ) { toRender . push ( React . createElement ( components . blockquote , { key : id } , properties . title ) ) ; } break ; } case 'callout' : { toRender . push ( < div className = "callout" key = { id } > { value . format ?. page_icon && ( < div > { value . format ?. page_icon } < /div>)} < div className = "text" > { textBlock ( properties . title , true , id ) } < /div> < /div>); break ; } case 'tweet' : { if ( properties . html ) { toRender . push ( < div dangerouslySetInnerHTML = { { __html : properties . html } } key = { id } / > ) ; } break ; } case 'equation' : { if ( properties && properties . title ) { const content = properties . title [ 0 ] [ 0 ] ; toRender . push ( < components . Equation key = { id } displayMode = { true } > { content } < /components.Equation>); } break ; } default : if ( process . env . NODE_ENV !== 'production' && ! listTypes . has ( type ) ) { console . log ( 'unknown type' , type ) ; } break ; } return toRender ; } ) } < /div> < />); } ; export default RenderPost ;