refactor: separate article components
This commit is contained in:
parent
39fea87d83
commit
a9db723aca
|
@ -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
|
|
@ -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;
|
||||||
|
`
|
|
@ -28,6 +28,7 @@ const ThumbnailContainer = styled.div<{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 458px; // temporary max-height based on the Figma design's max height
|
max-height: 458px; // temporary max-height based on the Figma design's max height
|
||||||
|
margin-bottom: 32px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Thumbnail = styled(Image)`
|
const Thumbnail = styled(Image)`
|
||||||
|
|
|
@ -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))'};
|
||||||
|
`
|
|
@ -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
|
|
@ -1,7 +1,3 @@
|
||||||
.relatedArticles > div > button {
|
|
||||||
border-top: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* temporary breakpoint */
|
/* temporary breakpoint */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.mobileToc {
|
.mobileToc {
|
||||||
|
|
|
@ -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;
|
|
||||||
`
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1 +1 @@
|
||||||
export { default as Article } from './Article'
|
export { default as Article } from './Article.Body'
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { ArticleReferenceType } from '@/components/ArticleReference/ArticleReference'
|
|
||||||
|
|
||||||
// temporary type
|
// temporary type
|
||||||
export type ReferenceType = {
|
export type ReferenceType = {
|
||||||
text: string
|
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',
|
title: 'How to Build a Practical Household Bike Generator',
|
||||||
author: 'Jason Freeman',
|
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',
|
title: 'How to Build a Practical Household Bike Generator',
|
||||||
author: 'Jason Freeman',
|
author: 'Jason Freeman',
|
||||||
|
|
|
@ -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 { Typography } from '@acid-info/lsd-react'
|
||||||
import styled from '@emotion/styled'
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export default function ArticleReference({
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body3" genericFontFamily="sans-serif">
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
{/*TODO we need handle multiple authors for same article*/}
|
{/*TODO we need handle multiple authors for same article*/}
|
||||||
{mentions[0].name}
|
{mentions[0]?.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body3">•</Typography>
|
<Typography variant="body3">•</Typography>
|
||||||
<Typography variant="body3" genericFontFamily="sans-serif">
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
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 { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
import {
|
import ArticleBody from '@/components/Article/Article.Body'
|
||||||
GoogleDocEnhanced,
|
|
||||||
UnbodyGoogleDoc,
|
|
||||||
UnbodyTocItem,
|
|
||||||
} from '@/lib/unbody/unbody.types'
|
|
||||||
import ArticleBody from '@/components/Article/ArticleBody'
|
|
||||||
import { ArticlePostData } from '@/types/data.types'
|
import { ArticlePostData } from '@/types/data.types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
Loading…
Reference in New Issue