refactor: separate article components

This commit is contained in:
jinhojang6 2023-05-12 09:52:05 +09:00 committed by Jinho Jang
parent 39fea87d83
commit a9db723aca
20 changed files with 426 additions and 356 deletions

View File

@ -0,0 +1,19 @@
import { getBodyBlocks } from '@/utils/data.utils'
import { RenderArticleBlock } from './Article.Block'
import { ArticlePostData } from '@/types/data.types'
type Props = {
data: ArticlePostData
}
const ArticleBlocks = ({ data }: Props) => {
return data?.article.blocks.length ? (
<>
{getBodyBlocks(data.article).map((block, idx) => (
<RenderArticleBlock key={'block-' + idx} block={block} />
))}
</>
) : null
}
export default ArticleBlocks

View File

@ -0,0 +1,47 @@
import styled from '@emotion/styled'
import { ArticlePostData } from '@/types/data.types'
import ArticleHeader from './Header/Article.Header'
import ArticleFooter from './Footer/Article.Footer'
import { MobileToc } from './Article.MobileToc'
import ArticleBlocks from './Article.Blocks'
interface Props {
data: ArticlePostData
}
export default function ArticleBody({ data }: Props) {
return (
<ArticleContainer>
<ArticleHeader {...data.article} />
<MobileToc toc={data.article.toc} />
<TextContainer>
<ArticleBlocks data={data} />
</TextContainer>
<ArticleFooter data={data} />
</ArticleContainer>
)
}
const ArticleContainer = styled.article`
display: flex;
position: relative;
flex-direction: column;
gap: 16px;
max-width: 700px;
margin-inline: 5%;
padding-bottom: 80px;
// temporary breakpoint
@media (max-width: 1024px) {
margin-inline: 16px;
}
`
const TextContainer = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 24px;
margin-bottom: 80px;
`

View File

@ -28,6 +28,7 @@ const ThumbnailContainer = styled.div<{
width: 100%;
height: 100%;
max-height: 458px; // temporary max-height based on the Figma design's max height
margin-bottom: 32px;
`
const Thumbnail = styled(Image)`

View File

@ -0,0 +1,48 @@
import styled from '@emotion/styled'
import React from 'react'
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
import { Typography } from '@acid-info/lsd-react'
import styles from './Article.module.css'
import { GoogleDocEnhanced } from '@/lib/unbody/unbody.types'
import { Collapse } from '../Collapse'
type Props = {
toc: Array<Partial<GoogleDocEnhanced>>
}
export const MobileToc = ({ toc }: Props) => {
const { tocIndex, setTocIndex } = useArticleContainerContext()
return toc?.length > 0 ? (
<Collapse className={styles.mobileToc} label="Contents">
{toc.map((toc, idx) => (
<Content
onClick={() => setTocIndex(idx)}
active={idx === tocIndex}
variant="body3"
key={idx}
>
{toc.title}
</Content>
))}
</Collapse>
) : null
}
const CustomTypography = styled(Typography)`
text-overflow: ellipsis;
word-break: break-word;
white-space: pre-wrap;
`
const Content = styled(CustomTypography)<{ active: boolean }>`
padding: 8px 14px;
background-color: ${(p) =>
p.active
? 'rgb(var(--lsd-theme-primary))'
: 'rgb(var(--lsd-theme-secondary))'};
color: ${(p) =>
p.active
? 'rgb(var(--lsd-theme-secondary))'
: 'rgb(var(--lsd-theme-primary))'};
`

View File

@ -0,0 +1,36 @@
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
const ArticleStats = ({
dateStr,
readingLength,
}: {
dateStr: string
readingLength: number
}) => (
<div>
<Row>
<Typography variant="body3" genericFontFamily="sans-serif">
{readingLength} minutes read
</Typography>
<Typography variant="body3"></Typography>
<Typography variant="body3" genericFontFamily="sans-serif">
{new Date(dateStr).toLocaleString('en-GB', {
day: 'numeric',
month: 'long', // TODO: Should be uppercase
year: 'numeric',
})}
</Typography>
</Row>
</div>
)
const Row = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin-bottom: 8px;
`
export default ArticleStats

View File

@ -1,7 +1,3 @@
.relatedArticles > div > button {
border-top: none !important;
}
/* temporary breakpoint */
@media (min-width: 1024px) {
.mobileToc {

View File

@ -1,338 +0,0 @@
import { Quote, Tag, Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import Image from 'next/image'
import { useMemo } from 'react'
import { PostImageRatio, PostImageRatioOptions } from '../Post/Post'
import styles from './Article.module.css'
import { Collapse } from '@/components/Collapse'
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
import { ArticleReference } from '../ArticleReference'
import {
GoogleDocEnhanced,
UnbodyGoogleDoc,
UnbodyImageBlock,
UnbodyTextBlock,
UnbodyTocItem,
} 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, getBodyBlocks } from '@/utils/data.utils'
import { ArticlePostData } from '@/types/data.types'
interface Props {
data: ArticlePostData
}
//@jinho
//TODO please move everything to a separate file
const ArticleTags = ({ tags }: { tags: string[] }) =>
tags.length > 0 ? (
<TagContainer>
{tags.map((tag) => (
<Tag size="small" disabled={false} key={tag}>
{tag}
</Tag>
))}
</TagContainer>
) : null
const ArticleAuthor = ({
mention,
}: {
mention: UnbodyGraphQl.Fragments.MentionItem
}) => (
<AuthorInfo key={mention.name}>
<Typography variant="body3" component="p" genericFontFamily="sans-serif">
{mention.name}
</Typography>
<Typography
href={`mailto:${mention.emailAddress}`}
variant="body3"
component="a"
genericFontFamily="sans-serif"
>
{mention.emailAddress}
</Typography>
</AuthorInfo>
)
const ArticleAuthors = ({
mentions,
}: {
mentions: UnbodyGraphQl.Fragments.MentionItem[]
}) =>
mentions.length > 0 ? (
<div>
{mentions.map((mention) => (
<ArticleAuthor key={mention.name} mention={mention} />
))}
</div>
) : null
const ArticleStats = ({
dateStr,
readingLength,
}: {
dateStr: string
readingLength: number
}) => (
<div>
<Row>
<Typography variant="body3" genericFontFamily="sans-serif">
{readingLength} minutes read
</Typography>
<Typography variant="body3"></Typography>
<Typography variant="body3" genericFontFamily="sans-serif">
{new Date(dateStr).toLocaleString('en-GB', {
day: 'numeric',
month: 'long', // TODO: Should be uppercase
year: 'numeric',
})}
</Typography>
</Row>
</div>
)
const ArticleSummary = ({ summary }: { summary: string }) => (
//TODO for ihor to work out the design for this
<ArticleSummaryContainer>
<Quote>{summary}</Quote>
</ArticleSummaryContainer>
)
const ArticleHeader = ({
title,
toc,
summary,
subtitle,
mentions,
tags,
modifiedAt,
blocks,
}: GoogleDocEnhanced) => {
const date = new Date(modifiedAt)
const _thumbnail = useMemo(() => {
const coverImage = getArticleCover(blocks)
if (!coverImage) return null
return (
<ArticleImageBlockWrapper
ratio={PostImageRatio.LANDSCAPE}
image={coverImage}
/>
)
}, [blocks])
return (
<header>
<ArticleStats dateStr={modifiedAt} readingLength={3} />
<ArticleTitle
id={toc[0].href.substring(1)}
variant={'h1'}
genericFontFamily="serif"
>
{title}
</ArticleTitle>
{subtitle && <ArticleSubtitle>{subtitle}</ArticleSubtitle>}
<ArticleTags tags={tags} />
<ArticleAuthors mentions={mentions} />
{_thumbnail}
<ArticleSummary summary={summary} />
</header>
)
}
const ArticleFootenotes = ({
footnotes,
}: {
footnotes: UnbodyGraphQl.Fragments.FootnoteItem[]
}) =>
footnotes.length > 0 ? (
<Collapse label="Footenotes">
{footnotes.map((footnote, idx) => (
<Reference key={idx}>
<Typography
component="a"
variant="body3"
href={`#${footnote.refId}`}
target="_blank"
id={footnote.id.replace('#', '')}
>
{footnote.refValue}
</Typography>
<p dangerouslySetInnerHTML={{ __html: footnote.valueHTML }} />
</Reference>
))}
</Collapse>
) : null
const RelatedArticles = ({ data }: { data: GoogleDocEnhanced[] }) =>
data.length > 0 ? (
<Collapse label="Related Articles">
{data.map((article, idx) => (
<ArticleReference key={idx} data={article} />
))}
</Collapse>
) : null
const FromSameAuthorsArticles = ({ data }: { data: GoogleDocEnhanced[] }) =>
data.length > 0 ? (
<Collapse label="From same authors">
{data.map((article, idx) => (
<ArticleReference key={idx} data={article} />
))}
</Collapse>
) : null
const ArticleFooter = ({ data }: { data: ArticlePostData }) => {
const { article, relatedArticles, articlesFromSameAuthors } = data
const footnotes = useMemo(() => {
return (
article.blocks
// @ts-ignore
.flatMap((b) =>
b.__typename === UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock
? b.footnotes
: [],
)
)
}, [article])
return (
<ArticleFooterContainer>
<ArticleFootenotes
footnotes={footnotes as Array<UnbodyGraphQl.Fragments.FootnoteItem>}
/>
<RelatedArticles data={relatedArticles} />
<FromSameAuthorsArticles data={articlesFromSameAuthors} />
</ArticleFooterContainer>
)
}
const CustomTypography = styled(Typography)`
text-overflow: ellipsis;
word-break: break-word;
white-space: pre-wrap;
`
const ArticleTitle = styled(CustomTypography)`
//margin-bottom: 24px;
`
const ArticleSubtitle = styled(CustomTypography)``
const ArticleSummaryContainer = styled('div')`
padding-left: 40px;
`
export default function ArticleBody({ data }: Props) {
const { title, summary, subtitle, blocks, toc, createdAt, tags, mentions } =
data.article
const articleContainer = useArticleContainerContext()
const { tocIndex, setTocIndex } = articleContainer
const _blocks = useMemo(() => {
return getBodyBlocks(data.article).map((block, idx) => (
<RenderArticleBlock key={'block-' + idx} block={block} />
))
}, [data.article])
console.log(data)
//TODO
//@Jinho please move everything (starts with _ ) to a separate file and import it here
const _mobileToc = useMemo(
() =>
toc?.length > 0 && (
<Collapse className={styles.mobileToc} label="Contents">
{toc.map((toc, idx) => (
<Content
onClick={() => setTocIndex(idx)}
active={idx === tocIndex}
variant="body3"
key={idx}
>
{toc.title}
</Content>
))}
</Collapse>
),
[toc, tocIndex],
)
return (
<ArticleContainer>
<ArticleHeader {...data.article} />
{_mobileToc}
<TextContainer>{_blocks}</TextContainer>
<ArticleFooter data={data} />
</ArticleContainer>
)
}
const ArticleContainer = styled.article`
display: flex;
position: relative;
flex-direction: column;
gap: 16px;
max-width: 700px;
margin-inline: 5%;
padding-bottom: 80px;
// temporary breakpoint
@media (max-width: 1024px) {
margin-inline: 16px;
}
`
const TextContainer = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 24px;
margin-bottom: 80px;
`
const Row = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin-bottom: 8px;
`
const TagContainer = styled.div`
display: flex;
gap: 8px;
`
const Content = styled(CustomTypography)<{ active: boolean }>`
padding: 8px 14px;
background-color: ${(p) =>
p.active
? 'rgb(var(--lsd-theme-primary))'
: 'rgb(var(--lsd-theme-secondary))'};
color: ${(p) =>
p.active
? 'rgb(var(--lsd-theme-secondary))'
: 'rgb(var(--lsd-theme-primary))'};
`
const Reference = styled.div`
display: flex;
padding: 8px 14px;
gap: 8px;
`
const ArticleFooterContainer = styled.div`
margin-top: 16px;
`
const AuthorInfo = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 8px;
`

View File

@ -0,0 +1,43 @@
import { ArticlePostData } from '@/types/data.types'
import { useMemo } from 'react'
import ArticleFootnotes from './Article.Footnotes'
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
import styled from '@emotion/styled'
import FromSameAuthorsArticles from './Article.FromSameAuthorsArticles'
import ArticleRelatedArticles from './Article.RelatedArticles'
const ArticleFooter = ({ data }: { data: ArticlePostData }) => {
const { article, relatedArticles, articlesFromSameAuthors } = data
const footnotes = useMemo(() => {
return (
article.blocks
// @ts-ignore
.flatMap((b) =>
b.__typename === UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock
? b.footnotes
: [],
)
)
}, [article])
return (
<ArticleFooterContainer>
<ArticleFootnotes
footnotes={footnotes as Array<UnbodyGraphQl.Fragments.FootnoteItem>}
/>
<ArticleRelatedArticles data={relatedArticles} />
<FromSameAuthorsArticles data={articlesFromSameAuthors} />
</ArticleFooterContainer>
)
}
const ArticleFooterContainer = styled.div`
margin-top: 16px;
& > div:not(:first-child) > div > button {
border-top: none;
}
`
export default ArticleFooter

View File

@ -0,0 +1,36 @@
import { Collapse } from '@/components/Collapse'
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
const ArticleFootnotes = ({
footnotes,
}: {
footnotes: UnbodyGraphQl.Fragments.FootnoteItem[]
}) =>
footnotes.length > 0 ? (
<Collapse label="Footenotes">
{footnotes.map((footnote, idx) => (
<Reference key={idx}>
<Typography
component="a"
variant="body3"
href={`#${footnote.refId}`}
target="_blank"
id={footnote.id.replace('#', '')}
>
{footnote.refValue}
</Typography>
<p dangerouslySetInnerHTML={{ __html: footnote.valueHTML }} />
</Reference>
))}
</Collapse>
) : null
const Reference = styled.div`
display: flex;
padding: 8px 14px;
gap: 8px;
`
export default ArticleFootnotes

View File

@ -0,0 +1,15 @@
import { ArticleReference } from '@/components/ArticleReference'
import { GoogleDocEnhanced } from '@/lib/unbody/unbody.types'
import styles from '../Article.module.css'
import { Collapse } from '@/components/Collapse'
const FromSameAuthorsArticles = ({ data }: { data: GoogleDocEnhanced[] }) =>
data.length > 0 ? (
<Collapse className={styles.relatedArticles} label="From same authors">
{data.map((article, idx) => (
<ArticleReference key={idx} data={article} />
))}
</Collapse>
) : null
export default FromSameAuthorsArticles

View File

@ -0,0 +1,14 @@
import { GoogleDocEnhanced } from '@/lib/unbody/unbody.types'
import { ArticleReference } from '@/components/ArticleReference'
import { Collapse } from '@/components/Collapse'
const ArticleRelatedArticles = ({ data }: { data: GoogleDocEnhanced[] }) =>
data.length > 0 ? (
<Collapse label="Related Articles">
{data.map((article, idx) => (
<ArticleReference key={idx} data={article} />
))}
</Collapse>
) : null
export default ArticleRelatedArticles

View File

@ -0,0 +1,32 @@
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
const ArticleAuthor = ({
mention,
}: {
mention: UnbodyGraphQl.Fragments.MentionItem
}) => (
<AuthorInfo key={mention.name}>
<Typography variant="body3" component="p" genericFontFamily="sans-serif">
{mention.name}
</Typography>
<Typography
href={`mailto:${mention.emailAddress}`}
variant="body3"
component="a"
genericFontFamily="sans-serif"
>
{mention.emailAddress}
</Typography>
</AuthorInfo>
)
const AuthorInfo = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 8px;
`
export default ArticleAuthor

View File

@ -0,0 +1,25 @@
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
import ArticleAuthor from './Article.Author'
import styled from '@emotion/styled'
const ArticleAuthors = ({
mentions,
}: {
mentions: UnbodyGraphQl.Fragments.MentionItem[]
}) =>
mentions.length > 0 ? (
<ArticleAuthorsContainer>
{mentions.map((mention) => (
<ArticleAuthor key={mention.name} mention={mention} />
))}
</ArticleAuthorsContainer>
) : null
const ArticleAuthorsContainer = styled.div`
margin-block: 24px;
display: flex;
flex-direction: column;
gap: 8px;
`
export default ArticleAuthors

View File

@ -0,0 +1,67 @@
import { GoogleDocEnhanced } from '@/lib/unbody/unbody.types'
import { getArticleCover } from '@/utils/data.utils'
import { useMemo } from 'react'
import { ArticleImageBlockWrapper } from '../Article.ImageBlockWrapper'
import { PostImageRatio } from '../../Post/Post'
import ArticleStats from '../Article.Stats'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import ArticleTags from './Article.Tags'
import ArticleAuthors from './Article.Authors'
import ArticleSummary from './Article.Summary'
const ArticleHeader = ({
title,
toc,
summary,
subtitle,
mentions,
tags,
modifiedAt,
blocks,
}: GoogleDocEnhanced) => {
const date = new Date(modifiedAt)
const _thumbnail = useMemo(() => {
const coverImage = getArticleCover(blocks)
if (!coverImage) return null
return (
<ArticleImageBlockWrapper
ratio={PostImageRatio.LANDSCAPE}
image={coverImage}
/>
)
}, [blocks])
return (
<header>
<ArticleStats dateStr={modifiedAt} readingLength={3} />
<ArticleTitle
id={toc[0].href.substring(1)}
variant={'h1'}
genericFontFamily="serif"
>
{title}
</ArticleTitle>
{subtitle && <ArticleSubtitle>{subtitle}</ArticleSubtitle>}
<ArticleTags tags={tags} />
<ArticleAuthors mentions={mentions} />
{_thumbnail}
<ArticleSummary summary={summary} />
</header>
)
}
const CustomTypography = styled(Typography)`
text-overflow: ellipsis;
word-break: break-word;
white-space: pre-wrap;
`
const ArticleTitle = styled(CustomTypography)`
margin-bottom: 24px;
`
const ArticleSubtitle = styled(CustomTypography)``
export default ArticleHeader

View File

@ -0,0 +1,16 @@
import { Quote } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
const ArticleSummary = ({ summary }: { summary: string }) => (
//TODO for ihor to work out the design for this
<ArticleSummaryContainer>
<Quote>{summary}</Quote>
</ArticleSummaryContainer>
)
const ArticleSummaryContainer = styled('div')`
padding-left: 40px;
margin-bottom: 32px;
`
export default ArticleSummary

View File

@ -0,0 +1,21 @@
import { Tag } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
const ArticleTags = ({ tags }: { tags: string[] }) =>
tags.length > 0 ? (
<TagContainer>
{tags.map((tag) => (
<Tag size="small" disabled={false} key={tag}>
{tag}
</Tag>
))}
</TagContainer>
) : null
const TagContainer = styled.div`
display: flex;
gap: 8px;
margin-top: 16px;
`
export default ArticleTags

View File

@ -1 +1 @@
export { default as Article } from './Article'
export { default as Article } from './Article.Body'

View File

@ -1,5 +1,3 @@
import { ArticleReferenceType } from '@/components/ArticleReference/ArticleReference'
// temporary type
export type ReferenceType = {
text: string
@ -18,7 +16,7 @@ export const references: ReferenceType[] = [
},
]
export const moreFromAuthor: ArticleReferenceType[] = [
export const moreFromAuthor = [
{
title: 'How to Build a Practical Household Bike Generator',
author: 'Jason Freeman',
@ -31,7 +29,7 @@ export const moreFromAuthor: ArticleReferenceType[] = [
},
]
export const relatedArticles: ArticleReferenceType[] = [
export const relatedArticles = [
{
title: 'How to Build a Practical Household Bike Generator',
author: 'Jason Freeman',

View File

@ -1,4 +1,4 @@
import { GoogleDocEnhanced, UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
import { GoogleDocEnhanced } from '@/lib/unbody/unbody.types'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
@ -24,7 +24,7 @@ export default function ArticleReference({
<div>
<Typography variant="body3" genericFontFamily="sans-serif">
{/*TODO we need handle multiple authors for same article*/}
{mentions[0].name}
{mentions[0]?.name}
</Typography>
<Typography variant="body3"></Typography>
<Typography variant="body3" genericFontFamily="sans-serif">

View File

@ -1,14 +1,8 @@
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 {
GoogleDocEnhanced,
UnbodyGoogleDoc,
UnbodyTocItem,
} from '@/lib/unbody/unbody.types'
import ArticleBody from '@/components/Article/ArticleBody'
import ArticleBody from '@/components/Article/Article.Body'
import { ArticlePostData } from '@/types/data.types'
interface Props {