mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-23 06:38:27 +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 { 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;
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
@ -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
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