diff --git a/src/components/Article/Article.Blocks.tsx b/src/components/Article/Article.Blocks.tsx new file mode 100644 index 0000000..82255fa --- /dev/null +++ b/src/components/Article/Article.Blocks.tsx @@ -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) => ( + + ))} + + ) : null +} + +export default ArticleBlocks diff --git a/src/components/Article/Article.Body.tsx b/src/components/Article/Article.Body.tsx new file mode 100644 index 0000000..24c4b02 --- /dev/null +++ b/src/components/Article/Article.Body.tsx @@ -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 ( + + + + + + + + + ) +} + +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; +` diff --git a/src/components/Article/Article.ImageBlockWrapper.tsx b/src/components/Article/Article.ImageBlockWrapper.tsx index f68d28a..d339bac 100644 --- a/src/components/Article/Article.ImageBlockWrapper.tsx +++ b/src/components/Article/Article.ImageBlockWrapper.tsx @@ -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)` diff --git a/src/components/Article/Article.MobileToc.tsx b/src/components/Article/Article.MobileToc.tsx new file mode 100644 index 0000000..c7403c3 --- /dev/null +++ b/src/components/Article/Article.MobileToc.tsx @@ -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> +} + +export const MobileToc = ({ toc }: Props) => { + const { tocIndex, setTocIndex } = useArticleContainerContext() + + return toc?.length > 0 ? ( + + {toc.map((toc, idx) => ( + setTocIndex(idx)} + active={idx === tocIndex} + variant="body3" + key={idx} + > + {toc.title} + + ))} + + ) : 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))'}; +` diff --git a/src/components/Article/Article.Stats.tsx b/src/components/Article/Article.Stats.tsx new file mode 100644 index 0000000..830036d --- /dev/null +++ b/src/components/Article/Article.Stats.tsx @@ -0,0 +1,36 @@ +import { Typography } from '@acid-info/lsd-react' +import styled from '@emotion/styled' + +const ArticleStats = ({ + dateStr, + readingLength, +}: { + dateStr: string + readingLength: number +}) => ( +
+ + + {readingLength} minutes read + + + + {new Date(dateStr).toLocaleString('en-GB', { + day: 'numeric', + month: 'long', // TODO: Should be uppercase + year: 'numeric', + })} + + +
+) + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin-bottom: 8px; +` + +export default ArticleStats diff --git a/src/components/Article/Article.module.css b/src/components/Article/Article.module.css index d0bf48e..3ab7663 100644 --- a/src/components/Article/Article.module.css +++ b/src/components/Article/Article.module.css @@ -1,7 +1,3 @@ -.relatedArticles > div > button { - border-top: none !important; -} - /* temporary breakpoint */ @media (min-width: 1024px) { .mobileToc { diff --git a/src/components/Article/ArticleBody.tsx b/src/components/Article/ArticleBody.tsx deleted file mode 100644 index 88f3929..0000000 --- a/src/components/Article/ArticleBody.tsx +++ /dev/null @@ -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 ? ( - - {tags.map((tag) => ( - - {tag} - - ))} - - ) : null - -const ArticleAuthor = ({ - mention, -}: { - mention: UnbodyGraphQl.Fragments.MentionItem -}) => ( - - - {mention.name} - - - {mention.emailAddress} - - -) - -const ArticleAuthors = ({ - mentions, -}: { - mentions: UnbodyGraphQl.Fragments.MentionItem[] -}) => - mentions.length > 0 ? ( -
- {mentions.map((mention) => ( - - ))} -
- ) : null - -const ArticleStats = ({ - dateStr, - readingLength, -}: { - dateStr: string - readingLength: number -}) => ( -
- - - {readingLength} minutes read - - - - {new Date(dateStr).toLocaleString('en-GB', { - day: 'numeric', - month: 'long', // TODO: Should be uppercase - year: 'numeric', - })} - - -
-) - -const ArticleSummary = ({ summary }: { summary: string }) => ( - //TODO for ihor to work out the design for this - - {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 ( - - ) - }, [blocks]) - - return ( -
- - - {title} - - {subtitle && {subtitle}} - - - {_thumbnail} - -
- ) -} - -const ArticleFootenotes = ({ - footnotes, -}: { - footnotes: UnbodyGraphQl.Fragments.FootnoteItem[] -}) => - footnotes.length > 0 ? ( - - {footnotes.map((footnote, idx) => ( - - - {footnote.refValue} - -

- - ))} - - ) : null - -const RelatedArticles = ({ data }: { data: GoogleDocEnhanced[] }) => - data.length > 0 ? ( - - {data.map((article, idx) => ( - - ))} - - ) : null - -const FromSameAuthorsArticles = ({ data }: { data: GoogleDocEnhanced[] }) => - data.length > 0 ? ( - - {data.map((article, idx) => ( - - ))} - - ) : 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 ( - - } - /> - - - - ) -} - -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) => ( - - )) - }, [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 && ( - - {toc.map((toc, idx) => ( - setTocIndex(idx)} - active={idx === tocIndex} - variant="body3" - key={idx} - > - {toc.title} - - ))} - - ), - [toc, tocIndex], - ) - - return ( - - - {_mobileToc} - {_blocks} - - - ) -} - -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; -` diff --git a/src/components/Article/Footer/Article.Footer.tsx b/src/components/Article/Footer/Article.Footer.tsx new file mode 100644 index 0000000..cd7a7d5 --- /dev/null +++ b/src/components/Article/Footer/Article.Footer.tsx @@ -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 ( + + } + /> + + + + ) +} + +const ArticleFooterContainer = styled.div` + margin-top: 16px; + + & > div:not(:first-child) > div > button { + border-top: none; + } +` + +export default ArticleFooter diff --git a/src/components/Article/Footer/Article.Footnotes.tsx b/src/components/Article/Footer/Article.Footnotes.tsx new file mode 100644 index 0000000..d9aaa51 --- /dev/null +++ b/src/components/Article/Footer/Article.Footnotes.tsx @@ -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 ? ( + + {footnotes.map((footnote, idx) => ( + + + {footnote.refValue} + +

+ + ))} + + ) : null + +const Reference = styled.div` + display: flex; + padding: 8px 14px; + gap: 8px; +` + +export default ArticleFootnotes diff --git a/src/components/Article/Footer/Article.FromSameAuthorsArticles.tsx b/src/components/Article/Footer/Article.FromSameAuthorsArticles.tsx new file mode 100644 index 0000000..ae0f3c5 --- /dev/null +++ b/src/components/Article/Footer/Article.FromSameAuthorsArticles.tsx @@ -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 ? ( + + {data.map((article, idx) => ( + + ))} + + ) : null + +export default FromSameAuthorsArticles diff --git a/src/components/Article/Footer/Article.RelatedArticles.tsx b/src/components/Article/Footer/Article.RelatedArticles.tsx new file mode 100644 index 0000000..a38c290 --- /dev/null +++ b/src/components/Article/Footer/Article.RelatedArticles.tsx @@ -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 ? ( + + {data.map((article, idx) => ( + + ))} + + ) : null + +export default ArticleRelatedArticles diff --git a/src/components/Article/Header/Article.Author.tsx b/src/components/Article/Header/Article.Author.tsx new file mode 100644 index 0000000..d8008b1 --- /dev/null +++ b/src/components/Article/Header/Article.Author.tsx @@ -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 +}) => ( + + + {mention.name} + + + {mention.emailAddress} + + +) + +const AuthorInfo = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 8px; +` + +export default ArticleAuthor diff --git a/src/components/Article/Header/Article.Authors.tsx b/src/components/Article/Header/Article.Authors.tsx new file mode 100644 index 0000000..6e2a0b0 --- /dev/null +++ b/src/components/Article/Header/Article.Authors.tsx @@ -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 ? ( + + {mentions.map((mention) => ( + + ))} + + ) : null + +const ArticleAuthorsContainer = styled.div` + margin-block: 24px; + display: flex; + flex-direction: column; + gap: 8px; +` + +export default ArticleAuthors diff --git a/src/components/Article/Header/Article.Header.tsx b/src/components/Article/Header/Article.Header.tsx new file mode 100644 index 0000000..efb4fd6 --- /dev/null +++ b/src/components/Article/Header/Article.Header.tsx @@ -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 ( + + ) + }, [blocks]) + + return ( +

+ + + {title} + + {subtitle && {subtitle}} + + + {_thumbnail} + +
+ ) +} + +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 diff --git a/src/components/Article/Header/Article.Summary.tsx b/src/components/Article/Header/Article.Summary.tsx new file mode 100644 index 0000000..09709a7 --- /dev/null +++ b/src/components/Article/Header/Article.Summary.tsx @@ -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 + + {summary} + +) + +const ArticleSummaryContainer = styled('div')` + padding-left: 40px; + margin-bottom: 32px; +` + +export default ArticleSummary diff --git a/src/components/Article/Header/Article.Tags.tsx b/src/components/Article/Header/Article.Tags.tsx new file mode 100644 index 0000000..81bc22a --- /dev/null +++ b/src/components/Article/Header/Article.Tags.tsx @@ -0,0 +1,21 @@ +import { Tag } from '@acid-info/lsd-react' +import styled from '@emotion/styled' + +const ArticleTags = ({ tags }: { tags: string[] }) => + tags.length > 0 ? ( + + {tags.map((tag) => ( + + {tag} + + ))} + + ) : null + +const TagContainer = styled.div` + display: flex; + gap: 8px; + margin-top: 16px; +` + +export default ArticleTags diff --git a/src/components/Article/index.ts b/src/components/Article/index.ts index ad2cb33..11f02d7 100644 --- a/src/components/Article/index.ts +++ b/src/components/Article/index.ts @@ -1 +1 @@ -export { default as Article } from './Article' +export { default as Article } from './Article.Body' diff --git a/src/components/Article/tempData/index.tsx b/src/components/Article/tempData/index.tsx index 758c1aa..e7e7417 100644 --- a/src/components/Article/tempData/index.tsx +++ b/src/components/Article/tempData/index.tsx @@ -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', diff --git a/src/components/ArticleReference/ArticleReference.tsx b/src/components/ArticleReference/ArticleReference.tsx index dbd4129..5ab9d67 100644 --- a/src/components/ArticleReference/ArticleReference.tsx +++ b/src/components/ArticleReference/ArticleReference.tsx @@ -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({
{/*TODO we need handle multiple authors for same article*/} - {mentions[0].name} + {mentions[0]?.name} diff --git a/src/containers/ArticleContainer.tsx b/src/containers/ArticleContainer.tsx index 88532b2..626722d 100644 --- a/src/containers/ArticleContainer.tsx +++ b/src/containers/ArticleContainer.tsx @@ -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 {