mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-23 22:58:08 +00:00
fix hydration error and cleanup article render method
This commit is contained in:
parent
44afe38e25
commit
361cb59dcf
69
src/components/Article/Article.Block.tsx
Normal file
69
src/components/Article/Article.Block.tsx
Normal 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;
|
||||||
|
`
|
35
src/components/Article/Article.ImageBlockWrapper.tsx
Normal file
35
src/components/Article/Article.ImageBlockWrapper.tsx
Normal 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;
|
||||||
|
`
|
@ -8,13 +8,22 @@ import { Collapse } from '@/components/Collapse'
|
|||||||
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
import { moreFromAuthor, references, relatedArticles } from './tempData'
|
import { moreFromAuthor, references, relatedArticles } from './tempData'
|
||||||
import { ArticleReference } from '../ArticleReference'
|
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 {
|
interface Props {
|
||||||
data: UnbodyGoogleDoc
|
data: UnbodyGoogleDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Article({ data }: Props) {
|
export default function ArticleBody({ data }: Props) {
|
||||||
const { title, summary, blocks, toc, createdAt, tags } = data
|
const { title, summary, blocks, toc, createdAt, tags } = data
|
||||||
const articleContainer = useArticleContainerContext()
|
const articleContainer = useArticleContainerContext()
|
||||||
const { tocIndex, setTocIndex } = articleContainer
|
const { tocIndex, setTocIndex } = articleContainer
|
||||||
@ -26,53 +35,20 @@ export default function Article({ data }: Props) {
|
|||||||
const date = new Date(createdAt)
|
const date = new Date(createdAt)
|
||||||
|
|
||||||
const _thumbnail = useMemo(() => {
|
const _thumbnail = useMemo(() => {
|
||||||
const imageBlocks: UnbodyImageBlock[] = blocks.filter(
|
const coverImage = getArticleCover(blocks)
|
||||||
(block): block is UnbodyImageBlock => block !== null && 'url' in block,
|
|
||||||
)
|
|
||||||
|
|
||||||
const coverImage = imageBlocks.reduce((prev, curr) =>
|
|
||||||
prev.order < curr.order ? prev : curr,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!coverImage) return null
|
if (!coverImage) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThumbnailContainer aspectRatio={PostImageRatio.LANDSCAPE}>
|
<ArticleImageBlockWrapper
|
||||||
<Thumbnail fill src={coverImage.url} alt={coverImage.alt} />
|
ratio={PostImageRatio.LANDSCAPE}
|
||||||
</ThumbnailContainer>
|
image={coverImage}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}, [blocks])
|
}, [blocks])
|
||||||
|
|
||||||
const _blocks = useMemo(() => {
|
const _blocks = useMemo(() => {
|
||||||
// Exclude title, subtitle, coverImage
|
return getContentBlocks(blocks).map((block, idx) => (
|
||||||
const articleBlocks = blocks.sort((a, b) => a.order - b.order).slice(3)
|
<RenderArticleBlock key={'block-' + idx} block={block} />
|
||||||
|
))
|
||||||
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 }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [blocks])
|
}, [blocks])
|
||||||
|
|
||||||
const _mobileToc = useMemo(
|
const _mobileToc = useMemo(
|
||||||
@ -161,8 +137,6 @@ export default function Article({ data }: Props) {
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* assign id for toc scroll */}
|
|
||||||
|
|
||||||
<Title
|
<Title
|
||||||
/*
|
/*
|
||||||
// @ts-ignore */
|
// @ts-ignore */
|
||||||
@ -259,32 +233,6 @@ const TextContainer = styled.div`
|
|||||||
margin-bottom: 80px;
|
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`
|
const Row = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
@ -1,32 +1,29 @@
|
|||||||
import { Article } from '@/components/Article'
|
|
||||||
import { TableOfContents } from '@/components/TableOfContents'
|
import { TableOfContents } from '@/components/TableOfContents'
|
||||||
import styled from '@emotion/styled'
|
import styled from '@emotion/styled'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { uiConfigs } from '@/configs/ui.configs'
|
import { uiConfigs } from '@/configs/ui.configs'
|
||||||
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
||||||
|
import ArticleBody from '@/components/Article/ArticleBody'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: UnbodyGoogleDoc
|
data: UnbodyGoogleDoc
|
||||||
error: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArticleContainer = (props: Props) => {
|
const ArticleContainer = (props: Props) => {
|
||||||
const { data, error } = props
|
const { data } = props
|
||||||
const [tocIndex, setTocIndex] = useState(0)
|
const [tocIndex, setTocIndex] = useState(0)
|
||||||
|
|
||||||
return !error?.length ? (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<ArticleContainerContext.Provider
|
<ArticleContainerContext.Provider
|
||||||
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
||||||
>
|
>
|
||||||
<TableOfContents contents={data.toc ?? []} />
|
<TableOfContents contents={data.toc ?? []} />
|
||||||
<Article data={data} />
|
<ArticleBody data={data} />
|
||||||
<Right />
|
<Right />
|
||||||
</ArticleContainerContext.Provider>
|
</ArticleContainerContext.Provider>
|
||||||
</Container>
|
</Container>
|
||||||
) : (
|
|
||||||
<div>{error}</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +7,28 @@ import { ArticlePostData } from '@/types/data.types'
|
|||||||
import { SEO } from '@/components/SEO'
|
import { SEO } from '@/components/SEO'
|
||||||
|
|
||||||
type ArticleProps = {
|
type ArticleProps = {
|
||||||
data: ArticlePostData | null
|
data: ArticlePostData
|
||||||
error: string | null
|
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) => {
|
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
@ -30,33 +50,11 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data: article,
|
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) {
|
ArticlePage.getLayout = function getLayout(page: ReactNode) {
|
||||||
return <ArticleLayout>{page}</ArticleLayout>
|
return <ArticleLayout>{page}</ArticleLayout>
|
||||||
}
|
}
|
||||||
|
@ -15,17 +15,20 @@ export const getArticlePostQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
|||||||
createdAt
|
createdAt
|
||||||
modifiedAt
|
modifiedAt
|
||||||
toc
|
toc
|
||||||
|
slug
|
||||||
blocks{
|
blocks{
|
||||||
...on ImageBlock{
|
...on ImageBlock{
|
||||||
url
|
url
|
||||||
alt
|
alt
|
||||||
order
|
order
|
||||||
|
__typename
|
||||||
}
|
}
|
||||||
... on TextBlock {
|
... on TextBlock {
|
||||||
footnotes
|
footnotes
|
||||||
html
|
html
|
||||||
order
|
order
|
||||||
tagName
|
tagName
|
||||||
|
__typename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
@ -126,7 +126,11 @@ class UnbodyService extends UnbodyClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllArticlePostSlugs = (): Promise<ApiResponse<{ remoteId: string }[]>> => {
|
getAllArticlePostSlugs = (): Promise<ApiResponse<{ remoteId: string }[]>> => {
|
||||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(getAllPostsSlugQuery())
|
return this.request<UnbodyGraphQlResponseGoogleDoc>(
|
||||||
|
getAllPostsSlugQuery({
|
||||||
|
where: Operands.WHERE_PUBLISHED(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
if (!data) return this.handleResponse([], 'No data')
|
if (!data) return this.handleResponse([], 'No data')
|
||||||
return this.handleResponse(data.Get.GoogleDoc)
|
return this.handleResponse(data.Get.GoogleDoc)
|
||||||
@ -153,6 +157,7 @@ class UnbodyService extends UnbodyClient {
|
|||||||
const article = data.Get.GoogleDoc[0]
|
const article = data.Get.GoogleDoc[0]
|
||||||
return this.handleResponse({
|
return this.handleResponse({
|
||||||
...article,
|
...article,
|
||||||
|
blocks: article.blocks.sort((a, b) => a.order - b.order),
|
||||||
toc: JSON.parse(
|
toc: JSON.parse(
|
||||||
article.toc as string,
|
article.toc as string,
|
||||||
) as Array<UnbodyGraphQl.Fragments.TocItem>,
|
) as Array<UnbodyGraphQl.Fragments.TocItem>,
|
||||||
|
32
src/utils/data.utils.ts
Normal file
32
src/utils/data.utils.ts
Normal 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
18
src/utils/html.utils.ts
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user