fix hydration error and cleanup article render method

This commit is contained in:
amirhouieh 2023-05-10 19:13:00 +02:00
parent 44afe38e25
commit 361cb59dcf
9 changed files with 210 additions and 105 deletions

View File

@ -0,0 +1,69 @@
import { UnbodyImageBlock, UnbodyTextBlock } from '@/lib/unbody/unbody.types'
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
import { ArticleImageBlockWrapper } from './Article.ImageBlockWrapper'
import { PostImageRatio } from '../Post/Post'
import styled from '@emotion/styled'
import { Typography } from '@acid-info/lsd-react'
import {
extractClassFromFirstTag,
extractIdFromFirstTag,
extractInnerHtml,
} from '@/utils/html.utils'
export const RenderArticleBlock = ({
block,
}: {
block: UnbodyImageBlock | UnbodyTextBlock
}) => {
console.log(block.__typename)
switch (block.__typename) {
case UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock:
return (
<ArticleImageBlockWrapper
ratio={PostImageRatio.LANDSCAPE}
image={block}
/>
)
case UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock:
switch (block.tagName) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
<Headline
variant={block.tagName as any}
component={block.tagName as any}
genericFontFamily="sans-serif"
className={extractClassFromFirstTag(block.html) || ''}
id={extractIdFromFirstTag(block.html) || ''}
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
/>
)
default:
return (
<Paragraph
variant="body1"
component="p"
genericFontFamily="sans-serif"
className={extractClassFromFirstTag(block.html) || ''}
id={extractIdFromFirstTag(block.html) || ''}
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
/>
)
}
default:
return null
}
}
const Headline = styled(Typography)`
white-space: pre-wrap;
margin-top: 24px;
`
const Paragraph = styled(Typography)`
white-space: pre-wrap;
`

View File

@ -0,0 +1,35 @@
import { UnbodyImageBlock } from '@/lib/unbody/unbody.types'
import styled from '@emotion/styled'
import React from 'react'
import { PostImageRatio, PostImageRatioOptions } from '../Post/Post'
import Image from 'next/image'
type Props = {
image: UnbodyImageBlock
ratio: PostImageRatio
}
export const ArticleImageBlockWrapper = ({ image, ratio }: Props) => {
return (
<ThumbnailContainer aspectRatio={ratio}>
<Thumbnail fill src={image.url} alt={image.alt} />
</ThumbnailContainer>
)
}
const ThumbnailContainer = styled.div<{
aspectRatio: PostImageRatio
}>`
aspect-ratio: ${(p) =>
p.aspectRatio
? PostImageRatioOptions[p.aspectRatio]
: PostImageRatioOptions[PostImageRatio.PORTRAIT]};
position: relative;
width: 100%;
height: 100%;
max-height: 458px; // temporary max-height based on the Figma design's max height
`
const Thumbnail = styled(Image)`
object-fit: cover;
`

View File

@ -8,13 +8,22 @@ import { Collapse } from '@/components/Collapse'
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
import { moreFromAuthor, references, relatedArticles } from './tempData'
import { ArticleReference } from '../ArticleReference'
import { UnbodyGoogleDoc, UnbodyImageBlock } from '@/lib/unbody/unbody.types'
import {
UnbodyGoogleDoc,
UnbodyImageBlock,
UnbodyTextBlock,
} from '@/lib/unbody/unbody.types'
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
import { RenderArticleBlock } from './Article.Block'
import { ArticleImageBlockWrapper } from './Article.ImageBlockWrapper'
import { getArticleCover, getContentBlocks } from '@/utils/data.utils'
interface Props {
data: UnbodyGoogleDoc
}
export default function Article({ data }: Props) {
export default function ArticleBody({ data }: Props) {
const { title, summary, blocks, toc, createdAt, tags } = data
const articleContainer = useArticleContainerContext()
const { tocIndex, setTocIndex } = articleContainer
@ -26,53 +35,20 @@ export default function Article({ data }: Props) {
const date = new Date(createdAt)
const _thumbnail = useMemo(() => {
const imageBlocks: UnbodyImageBlock[] = blocks.filter(
(block): block is UnbodyImageBlock => block !== null && 'url' in block,
)
const coverImage = imageBlocks.reduce((prev, curr) =>
prev.order < curr.order ? prev : curr,
)
const coverImage = getArticleCover(blocks)
if (!coverImage) return null
return (
<ThumbnailContainer aspectRatio={PostImageRatio.LANDSCAPE}>
<Thumbnail fill src={coverImage.url} alt={coverImage.alt} />
</ThumbnailContainer>
<ArticleImageBlockWrapper
ratio={PostImageRatio.LANDSCAPE}
image={coverImage}
/>
)
}, [blocks])
const _blocks = useMemo(() => {
// Exclude title, subtitle, coverImage
const articleBlocks = blocks.sort((a, b) => a.order - b.order).slice(3)
return articleBlocks.map((block, idx) => {
return 'url' in block ? (
<ThumbnailContainer
key={'block-' + idx}
aspectRatio={PostImageRatio.LANDSCAPE}
>
<Thumbnail fill src={block.url} alt={block.alt} />
</ThumbnailContainer>
) : block.tagName.startsWith('h') ? (
<Headline
variant="body2"
component={block.tagName as any}
genericFontFamily="sans-serif"
key={'block-' + idx}
dangerouslySetInnerHTML={{ __html: block.html }}
/>
) : (
<Paragraph
variant="body1"
component="p"
genericFontFamily="sans-serif"
key={'block-' + idx}
dangerouslySetInnerHTML={{ __html: block.html }}
/>
)
})
return getContentBlocks(blocks).map((block, idx) => (
<RenderArticleBlock key={'block-' + idx} block={block} />
))
}, [blocks])
const _mobileToc = useMemo(
@ -161,11 +137,9 @@ export default function Article({ data }: Props) {
</Row>
</div>
{/* assign id for toc scroll */}
<Title
/*
// @ts-ignore */
// @ts-ignore */
id={toc[0].href.substring(1)}
variant={'h1'}
genericFontFamily="serif"
@ -259,32 +233,6 @@ const TextContainer = styled.div`
margin-bottom: 80px;
`
const Headline = styled(Typography)`
white-space: pre-wrap;
margin-top: 24px;
`
const Paragraph = styled(Typography)`
white-space: pre-wrap;
`
const ThumbnailContainer = styled.div<{
aspectRatio: PostImageRatio
}>`
aspect-ratio: ${(p) =>
p.aspectRatio
? PostImageRatioOptions[p.aspectRatio]
: PostImageRatioOptions[PostImageRatio.PORTRAIT]};
position: relative;
width: 100%;
height: 100%;
max-height: 458px; // temporary max-height based on the Figma design's max height
`
const Thumbnail = styled(Image)`
object-fit: cover;
`
const Row = styled.div`
display: flex;
flex-direction: row;

View File

@ -1,32 +1,29 @@
import { Article } from '@/components/Article'
import { TableOfContents } from '@/components/TableOfContents'
import styled from '@emotion/styled'
import { useState } from 'react'
import { uiConfigs } from '@/configs/ui.configs'
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
import ArticleBody from '@/components/Article/ArticleBody'
interface Props {
data: UnbodyGoogleDoc
error: string | null
}
const ArticleContainer = (props: Props) => {
const { data, error } = props
const { data } = props
const [tocIndex, setTocIndex] = useState(0)
return !error?.length ? (
return (
<Container>
<ArticleContainerContext.Provider
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
>
<TableOfContents contents={data.toc ?? []} />
<Article data={data} />
<ArticleBody data={data} />
<Right />
</ArticleContainerContext.Provider>
</Container>
) : (
<div>{error}</div>
)
}

View File

@ -7,8 +7,28 @@ import { ArticlePostData } from '@/types/data.types'
import { SEO } from '@/components/SEO'
type ArticleProps = {
data: ArticlePostData | null
error: string | null
data: ArticlePostData
errors: string | null
}
const ArticlePage = ({ data, errors }: ArticleProps) => {
if (errors) return <div>{errors}</div>
return (
<>
<SEO title={data.title} description={data.summary} />
<ArticleContainer data={data} />
</>
)
}
export async function getStaticPaths() {
const { data: posts, errors } = await api.getAllArticlePostSlugs()
return {
paths: errors
? []
: posts.map((post) => ({ params: { remoteId: `${post.remoteId}` } })),
fallback: true,
}
}
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
@ -30,33 +50,11 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
return {
props: {
data: article,
error: errors,
error: JSON.stringify(errors),
},
}
}
// @jinho lets handle the error directly in thew page component
const ArticlePage = (props: ArticleProps) => {
if (!props.data) return <div style={{ height: '100vh' }} />
return (
<>
<SEO title={props.data.title} description={props.data.summary} />
<ArticleContainer data={props.data} error={props.error} />
</>
)
}
export async function getStaticPaths() {
const { data: posts, errors } = await api.getAllArticlePostSlugs()
return {
paths: errors
? []
: posts.map((post) => ({ params: { remoteId: `${post.remoteId}` } })),
fallback: true,
}
}
ArticlePage.getLayout = function getLayout(page: ReactNode) {
return <ArticleLayout>{page}</ArticleLayout>
}

View File

@ -15,17 +15,20 @@ export const getArticlePostQuery = (args: UnbodyGetFilters = defaultArgs) =>
createdAt
modifiedAt
toc
slug
blocks{
...on ImageBlock{
url
alt
order
__typename
}
... on TextBlock {
footnotes
html
order
tagName
__typename
}
}
`)

View File

@ -126,7 +126,11 @@ class UnbodyService extends UnbodyClient {
}
getAllArticlePostSlugs = (): Promise<ApiResponse<{ remoteId: string }[]>> => {
return this.request<UnbodyGraphQlResponseGoogleDoc>(getAllPostsSlugQuery())
return this.request<UnbodyGraphQlResponseGoogleDoc>(
getAllPostsSlugQuery({
where: Operands.WHERE_PUBLISHED(),
}),
)
.then(({ data }) => {
if (!data) return this.handleResponse([], 'No data')
return this.handleResponse(data.Get.GoogleDoc)
@ -153,6 +157,7 @@ class UnbodyService extends UnbodyClient {
const article = data.Get.GoogleDoc[0]
return this.handleResponse({
...article,
blocks: article.blocks.sort((a, b) => a.order - b.order),
toc: JSON.parse(
article.toc as string,
) as Array<UnbodyGraphQl.Fragments.TocItem>,

32
src/utils/data.utils.ts Normal file
View File

@ -0,0 +1,32 @@
import {
UnbodyGoogleDoc,
UnbodyImageBlock,
UnbodyTextBlock,
} from '@/lib/unbody/unbody.types'
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
export const getContentBlocks = (
blocks: (UnbodyImageBlock | UnbodyTextBlock)[],
) => {
return blocks.filter((b) => {
return (
(b.__typename === UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock &&
b.order !== 4) ||
(b.__typename === UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock &&
b.html.indexOf(`class="subtitle"`) === -1 &&
b.html.indexOf(`class="title"`) === -1)
)
})
}
export const getArticleCover = (
blocks: (UnbodyImageBlock | UnbodyTextBlock)[],
): UnbodyImageBlock | null => {
return (
(blocks.find(
(b) =>
b.order === 4 &&
b.__typename === UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock,
) as UnbodyImageBlock) || null
)
}

18
src/utils/html.utils.ts Normal file
View File

@ -0,0 +1,18 @@
const regexForInnerHtml = /<[^>]*>([^<]*)<\/[^>]*>/
const regexForId = /id="([^"]*)"/
const regexForClass = /class="([^"]*)"/
export const extractInnerHtml = (html: string) => {
const match = html.match(regexForInnerHtml)
return match ? match[1] : html
}
export const extractIdFromFirstTag = (html: string) => {
const match = html.match(regexForId)
return match ? match[1] : null
}
export const extractClassFromFirstTag = (html: string) => {
const match = html.match(regexForClass)
return match ? match[1] : null
}