feat: implement article page
This commit is contained in:
parent
bebde816ef
commit
052379afce
|
@ -35,3 +35,5 @@ yarn-error.log*
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"@types/react": "18.0.35",
|
"@types/react": "18.0.35",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
"eslint": "8.38.0",
|
"eslint": "8.38.0",
|
||||||
"eslint-config-next": "13.3.0",
|
"eslint-config-next": "13.3.0",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
.relatedArticles > div > button {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* temporary breakpoint */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.mobileToc {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,250 @@
|
||||||
|
import { Tag, Typography } from '@acid-info/lsd-react'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { PostProps } from '../Post'
|
||||||
|
import { PostImageRatio, PostImageRatioOptions, PostSize } from '../Post/Post'
|
||||||
|
import styles from './Article.module.css'
|
||||||
|
import { Collapse } from '@/components/Collapse'
|
||||||
|
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
|
import { moreFromAuthor, references, relatedArticles } from './tempData'
|
||||||
|
import { ArticleReference } from '../ArticleReference'
|
||||||
|
|
||||||
|
export default function Article({
|
||||||
|
appearance: {
|
||||||
|
size = PostSize.SMALL,
|
||||||
|
aspectRatio = PostImageRatio.LANDSCAPE,
|
||||||
|
} = {},
|
||||||
|
data: {
|
||||||
|
coverImage = null,
|
||||||
|
date: dateStr = '',
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
summary,
|
||||||
|
author,
|
||||||
|
tags = [],
|
||||||
|
toc = [],
|
||||||
|
},
|
||||||
|
...props
|
||||||
|
}: PostProps) {
|
||||||
|
const articleContainer = useArticleContainerContext()
|
||||||
|
const { tocIndex, setTocIndex } = articleContainer
|
||||||
|
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
|
||||||
|
const _thumbnail = useMemo(() => {
|
||||||
|
if (!coverImage) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThumbnailContainer aspectRatio={aspectRatio}>
|
||||||
|
<Thumbnail fill src={coverImage.url} alt={coverImage.alt} />
|
||||||
|
</ThumbnailContainer>
|
||||||
|
)
|
||||||
|
}, [coverImage])
|
||||||
|
|
||||||
|
const _text = useMemo(
|
||||||
|
() => (
|
||||||
|
<CustomTypography variant="body1" genericFontFamily="sans-serif">
|
||||||
|
{text}
|
||||||
|
</CustomTypography>
|
||||||
|
),
|
||||||
|
[text],
|
||||||
|
)
|
||||||
|
|
||||||
|
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}
|
||||||
|
</Content>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
),
|
||||||
|
[toc, tocIndex],
|
||||||
|
)
|
||||||
|
|
||||||
|
const _references = useMemo(
|
||||||
|
() =>
|
||||||
|
references?.length > 0 && (
|
||||||
|
<Collapse label="References">
|
||||||
|
{references.map((reference, idx) => (
|
||||||
|
<Reference key={idx}>
|
||||||
|
<Typography component="span" variant="body3">
|
||||||
|
{idx + 1}.
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
component="a"
|
||||||
|
variant="body3"
|
||||||
|
href={reference.link}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{reference.text}
|
||||||
|
</Typography>
|
||||||
|
</Reference>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
),
|
||||||
|
[references],
|
||||||
|
)
|
||||||
|
|
||||||
|
const _moreFromAuthor = useMemo(
|
||||||
|
() =>
|
||||||
|
moreFromAuthor?.length > 0 && (
|
||||||
|
<Collapse label="More From The Author">
|
||||||
|
{moreFromAuthor.map((article, idx) => (
|
||||||
|
<ArticleReference key={idx} data={article} />
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
),
|
||||||
|
[moreFromAuthor],
|
||||||
|
)
|
||||||
|
|
||||||
|
const _relatedArticles = useMemo(
|
||||||
|
() =>
|
||||||
|
relatedArticles?.length > 0 && (
|
||||||
|
<Collapse className={styles.relatedArticles} label="Related Articles">
|
||||||
|
{relatedArticles.map((article, idx) => (
|
||||||
|
<ArticleReference key={idx} data={article} />
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
),
|
||||||
|
[relatedArticles],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArticleContainer {...props}>
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
10 minutes read
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body3">•</Typography>
|
||||||
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
{date.toLocaleString('en-GB', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long', // TODO: Should be uppercase
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CustomTypography
|
||||||
|
variant={size === PostSize.SMALL ? 'h4' : 'h2'}
|
||||||
|
genericFontFamily="serif"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</CustomTypography>
|
||||||
|
|
||||||
|
{_thumbnail}
|
||||||
|
|
||||||
|
<CustomTypography
|
||||||
|
variant={size === PostSize.SMALL ? 'h4' : 'h2'}
|
||||||
|
genericFontFamily="serif"
|
||||||
|
>
|
||||||
|
{summary}
|
||||||
|
</CustomTypography>
|
||||||
|
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<TagContainer>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Tag size="small" disabled={false} key={tag}>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</TagContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
{author}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{_mobileToc}
|
||||||
|
|
||||||
|
{_text}
|
||||||
|
|
||||||
|
{_references}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{_moreFromAuthor}
|
||||||
|
{_relatedArticles}
|
||||||
|
</div>
|
||||||
|
</ArticleContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArticleContainer = styled.article`
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-width: 700px;
|
||||||
|
margin-inline: 5%;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
|
||||||
|
// temporary breakpoint
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
margin-inline: 16px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
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;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CustomTypography = styled(Typography)`
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
`
|
||||||
|
const TagContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
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;
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Article } from './Article'
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { ArticleReferenceType } from '@/components/ArticleReference/ArticleReference'
|
||||||
|
|
||||||
|
// temporary type
|
||||||
|
export type ReferenceType = {
|
||||||
|
text: string
|
||||||
|
link: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// temporary data
|
||||||
|
export const references: ReferenceType[] = [
|
||||||
|
{
|
||||||
|
text: 'Szto, Courtney, and Brian Wilson. "Reduce, re-use, re-ride: Bike waste and moving towards a circular economy for sporting goods." International Review for the Sociology of Sport (2022): 10126902221138033',
|
||||||
|
link: 'https://acid.info/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'ohnson, Rebecca, Alice Kodama, and Regina Willensky. "The complete impact of bicycle use: analyzing the environmental impact and initiative of the bicycle industry." (2014).',
|
||||||
|
link: 'https://acid.info/',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const moreFromAuthor: ArticleReferenceType[] = [
|
||||||
|
{
|
||||||
|
title: 'How to Build a Practical Household Bike Generator',
|
||||||
|
author: 'Jason Freeman',
|
||||||
|
date: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Preventing an Orwellian Future with Privacy-Enhancing Technology',
|
||||||
|
author: 'Jason Freeman',
|
||||||
|
date: new Date(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const relatedArticles: ArticleReferenceType[] = [
|
||||||
|
{
|
||||||
|
title: 'How to Build a Practical Household Bike Generator',
|
||||||
|
author: 'Jason Freeman',
|
||||||
|
date: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Preventing an Orwellian Future with Privacy-Enhancing Technology',
|
||||||
|
author: 'Jason Freeman',
|
||||||
|
date: new Date(),
|
||||||
|
},
|
||||||
|
]
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
export type ArticleReferenceType = {
|
||||||
|
title: string
|
||||||
|
author: string
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: ArticleReferenceType
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArticleReference({
|
||||||
|
data: { title, author, date },
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const localDate = date.toLocaleString('en-GB', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reference {...props}>
|
||||||
|
<Typography component="span" variant="body1">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
{author}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body3">•</Typography>
|
||||||
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
{localDate}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Reference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Reference = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-bottom: 1px solid rgb(var(--lsd-border-primary));
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as ArticleReference } from './ArticleReference'
|
|
@ -0,0 +1,8 @@
|
||||||
|
.collapse > div > button {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { CollapseProps, Collapse as LsdCollapse } from '@acid-info/lsd-react'
|
||||||
|
import styles from './Collapse.module.css'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export default function Collapse({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CollapseProps) {
|
||||||
|
return (
|
||||||
|
<LsdCollapse
|
||||||
|
label={label}
|
||||||
|
{...props}
|
||||||
|
className={clsx(styles.collapse, className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LsdCollapse>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Collapse } from './Collapse'
|
|
@ -47,7 +47,10 @@ export type PostDataProps = {
|
||||||
description?: string
|
description?: string
|
||||||
author?: string
|
author?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
coverImage?: UnbodyImageBlock
|
coverImage?: UnbodyImageBlock | null
|
||||||
|
summary?: string
|
||||||
|
text?: string
|
||||||
|
toc?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PostImageRatioOptions = {
|
export const PostImageRatioOptions = {
|
||||||
|
@ -71,7 +74,14 @@ export default function Post({
|
||||||
aspectRatio = PostImageRatio.LANDSCAPE,
|
aspectRatio = PostImageRatio.LANDSCAPE,
|
||||||
showImage = true,
|
showImage = true,
|
||||||
} = {},
|
} = {},
|
||||||
data: { coverImage, date: dateStr, title, description, author, tags = [] },
|
data: {
|
||||||
|
coverImage = null,
|
||||||
|
date: dateStr = '',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
author,
|
||||||
|
tags = [],
|
||||||
|
},
|
||||||
...props
|
...props
|
||||||
}: PostProps) {
|
}: PostProps) {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
|
|
|
@ -16,7 +16,11 @@ export default function PostContainer({
|
||||||
}: PostContainerProps) {
|
}: PostContainerProps) {
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{title && (<Title variant="body1" genericFontFamily="sans-serif">{title}</Title>)}
|
{title && (
|
||||||
|
<Title variant="body1" genericFontFamily="sans-serif">
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
)}
|
||||||
<Container>
|
<Container>
|
||||||
{postsData.map((post, index) => (
|
{postsData.map((post, index) => (
|
||||||
<PostWrapper key={index}>
|
<PostWrapper key={index}>
|
||||||
|
@ -34,7 +38,7 @@ const Container = styled.div`
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
|
||||||
// temporariy breakpoint
|
// temporary breakpoint
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
TextField,
|
TextField,
|
||||||
Autocomplete,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
|
@ -15,6 +14,7 @@ import styled from '@emotion/styled'
|
||||||
|
|
||||||
export type SearchbarProps = {
|
export type SearchbarProps = {
|
||||||
searchScope?: ESearchScope
|
searchScope?: ESearchScope
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Searchbar(props: SearchbarProps) {
|
export default function Searchbar(props: SearchbarProps) {
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { uiConfigs } from '@/configs/ui.configs'
|
||||||
|
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
|
import { useSticky } from '@/utils/ui.utils'
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contents: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TableOfContents({ contents, ...props }: Props) {
|
||||||
|
const articleContainer = useArticleContainerContext()
|
||||||
|
const { tocIndex, setTocIndex } = articleContainer
|
||||||
|
const dy = uiConfigs.navbarRenderedHeight + uiConfigs.postMarginTop
|
||||||
|
|
||||||
|
const { sticky, stickyRef, height } = useSticky<HTMLDivElement>(dy)
|
||||||
|
|
||||||
|
const handleSectionClick = (index: number) => {
|
||||||
|
setTocIndex(index)
|
||||||
|
// TODO: scrollIntoView
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
dy={dy}
|
||||||
|
height={height}
|
||||||
|
ref={stickyRef}
|
||||||
|
{...props}
|
||||||
|
className={sticky ? 'sticky' : ''}
|
||||||
|
>
|
||||||
|
<Title variant="body3">Contents</Title>
|
||||||
|
{contents.map((content, index) => (
|
||||||
|
<Section
|
||||||
|
active={index === tocIndex}
|
||||||
|
onClick={() => handleSectionClick(index)}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
{content}
|
||||||
|
</Typography>
|
||||||
|
</Section>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.aside<{ dy: number; height: number }>`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 162px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: fit-content;
|
||||||
|
position: sticky;
|
||||||
|
top: ${(p) => `${p.dy}px`};
|
||||||
|
margin-left: 16px;
|
||||||
|
|
||||||
|
&.sticky {
|
||||||
|
top: ${uiConfigs.navbarRenderedHeight + 78 + 1}px;
|
||||||
|
z-index: 100;
|
||||||
|
height: ${(p) => `${p.height}px`};
|
||||||
|
}
|
||||||
|
|
||||||
|
// temporary breakpoint
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled(Typography)`
|
||||||
|
margin-bottom: 24px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Section = styled.section<{ active: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 0 8px 12px;
|
||||||
|
border-left: ${(p) =>
|
||||||
|
p.active
|
||||||
|
? '1px solid rgb(var(--lsd-border-primary))'
|
||||||
|
: '1px solid transparent'};
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as TableOfContents } from './TableOfContents'
|
|
@ -1,3 +1,4 @@
|
||||||
export const uiConfigs = {
|
export const uiConfigs = {
|
||||||
navbarRenderedHeight: 45,
|
navbarRenderedHeight: 45,
|
||||||
|
postMarginTop: 78,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export type ArticleContainerContextType = {
|
||||||
|
tocIndex: number
|
||||||
|
setTocIndex: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArticleContainerContext =
|
||||||
|
React.createContext<ArticleContainerContextType>(null as any)
|
||||||
|
|
||||||
|
export const useArticleContainerContext = () =>
|
||||||
|
React.useContext(ArticleContainerContext)
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Article } from '@/components/Article'
|
||||||
|
import { TableOfContents } from '@/components/TableOfContents'
|
||||||
|
import { ArticleProps } from '@/pages/article/[slug]'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { uiConfigs } from '@/configs/ui.configs'
|
||||||
|
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
|
|
||||||
|
const ArticleContainer = (props: ArticleProps) => {
|
||||||
|
const { post } = props
|
||||||
|
const [tocIndex, setTocIndex] = useState(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{typeof post !== 'undefined' ? (
|
||||||
|
<ArticleContainerContext.Provider
|
||||||
|
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
||||||
|
>
|
||||||
|
<TableOfContents contents={post.toc ?? []} />
|
||||||
|
<Article data={post} />
|
||||||
|
<Right />
|
||||||
|
</ArticleContainerContext.Provider>
|
||||||
|
) : (
|
||||||
|
<div style={{ marginTop: '108px', textAlign: 'center' }}>
|
||||||
|
<h3>Loading</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: ${uiConfigs.postMarginTop}px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Right = styled.aside`
|
||||||
|
width: 162px;
|
||||||
|
// temporary breakpoint
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default ArticleContainer
|
|
@ -0,0 +1,3 @@
|
||||||
|
.header > nav {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
|
@ -4,12 +4,13 @@ import { PropsWithChildren } from 'react'
|
||||||
import { NavbarFiller } from '@/components/Navbar/NavbarFiller'
|
import { NavbarFiller } from '@/components/Navbar/NavbarFiller'
|
||||||
import { Searchbar } from '@/components/Searchbar'
|
import { Searchbar } from '@/components/Searchbar'
|
||||||
import { ESearchScope } from '@/types/ui.types'
|
import { ESearchScope } from '@/types/ui.types'
|
||||||
|
import styles from './Article.layout.module.css'
|
||||||
|
|
||||||
export default function ArticleLayout(props: PropsWithChildren<any>) {
|
export default function ArticleLayout(props: PropsWithChildren<any>) {
|
||||||
const isDarkState = useIsDarkState()
|
const isDarkState = useIsDarkState()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header className={styles.header}>
|
||||||
<Navbar isDark={isDarkState.get()} toggle={isDarkState.toggle} />
|
<Navbar isDark={isDarkState.get()} toggle={isDarkState.toggle} />
|
||||||
<NavbarFiller />
|
<NavbarFiller />
|
||||||
<Searchbar searchScope={ESearchScope.ARTICLE} />
|
<Searchbar searchScope={ESearchScope.ARTICLE} />
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { NextPage } from 'next'
|
|
||||||
import { ArticleLayout } from '@/layouts/ArticleLayout'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
type Props = NextPage<{}>
|
|
||||||
|
|
||||||
const ArticlePage = (props: Props) => {
|
|
||||||
return <article>article</article>
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticlePage.getLayout = function getLayout(page: ReactNode) {
|
|
||||||
return <ArticleLayout>{page}</ArticleLayout>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ArticlePage
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { GetStaticPropsContext } from 'next'
|
||||||
|
import { ArticleLayout } from '@/layouts/ArticleLayout'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import ArticleContainer from '@/containers/ArticleContainer'
|
||||||
|
import { UnbodyGoogleDoc, UnbodyImageBlock } from '@/lib/unbody/unbody.types'
|
||||||
|
import { getArticlePost } from '@/services/unbody.service'
|
||||||
|
import { PostDataProps } from '@/components/Post/Post'
|
||||||
|
|
||||||
|
export type ArticleProps = {
|
||||||
|
post: PostDataProps
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
|
const slug = params?.slug
|
||||||
|
console.log('slug', slug) // TODO : fetch data based on slug
|
||||||
|
let post: Partial<UnbodyGoogleDoc> = {}
|
||||||
|
let error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const posts = await getArticlePost()
|
||||||
|
post = posts[0]
|
||||||
|
} catch (e) {
|
||||||
|
error = JSON.stringify(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
post: {
|
||||||
|
date: post.modifiedAt,
|
||||||
|
title: post.title,
|
||||||
|
summary: post.summary,
|
||||||
|
text: post.text,
|
||||||
|
author: 'Jinho',
|
||||||
|
tags: post.tags,
|
||||||
|
toc: [
|
||||||
|
'The dangers of totalitarian surveillance',
|
||||||
|
'Orwellian Future',
|
||||||
|
'Privacy-enhancing technology and its benefits',
|
||||||
|
'Ethical considerations of privacy-enhancing technology',
|
||||||
|
],
|
||||||
|
...(post.blocks && post.blocks!.length > 0
|
||||||
|
? { coverImage: post.blocks![0] as UnbodyImageBlock }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArticlePage = (props: ArticleProps) => {
|
||||||
|
return <ArticleContainer post={props.post} error={props.error} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
return {
|
||||||
|
paths: [{ params: { slug: 'sth' } }],
|
||||||
|
fallback: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArticlePage.getLayout = function getLayout(page: ReactNode) {
|
||||||
|
return <ArticleLayout>{page}</ArticleLayout>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArticlePage
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { GetGoogleDocQuery } from '.'
|
||||||
|
import { UnbodyExploreArgs } from '@/lib/unbody/unbody.types'
|
||||||
|
|
||||||
|
const defaultArgs: UnbodyExploreArgs = {
|
||||||
|
limit: 1,
|
||||||
|
nearText: { concepts: ['home'] },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getArticlePostQuery = (args: UnbodyExploreArgs = defaultArgs) =>
|
||||||
|
GetGoogleDocQuery(args)(`
|
||||||
|
sourceId
|
||||||
|
remoteId
|
||||||
|
title
|
||||||
|
summary
|
||||||
|
tags
|
||||||
|
createdAt
|
||||||
|
modifiedAt
|
||||||
|
text
|
||||||
|
blocks{
|
||||||
|
...on ImageBlock{
|
||||||
|
url
|
||||||
|
alt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
|
@ -3,6 +3,7 @@ import {
|
||||||
UnbodyGoogleDoc,
|
UnbodyGoogleDoc,
|
||||||
UnbodyGraphQlResponseGoogleDoc,
|
UnbodyGraphQlResponseGoogleDoc,
|
||||||
} from '@/lib/unbody/unbody.types'
|
} from '@/lib/unbody/unbody.types'
|
||||||
|
import { getArticlePostQuery } from '@/queries/getPost'
|
||||||
import { getHomePagePostsQuery } from '@/queries/getPosts'
|
import { getHomePagePostsQuery } from '@/queries/getPosts'
|
||||||
|
|
||||||
const { UNBODY_API_KEY, UNBODY_LPE_PROJECT_ID } = process.env
|
const { UNBODY_API_KEY, UNBODY_LPE_PROJECT_ID } = process.env
|
||||||
|
@ -23,4 +24,10 @@ export const getHomepagePosts = (): Promise<HomepagePost[]> => {
|
||||||
.then(({ data }) => data.Get.GoogleDoc)
|
.then(({ data }) => data.Get.GoogleDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getArticlePost = (): Promise<HomepagePost[]> => {
|
||||||
|
return unbody
|
||||||
|
.request<UnbodyGraphQlResponseGoogleDoc>(getArticlePostQuery())
|
||||||
|
.then(({ data }) => data.Get.GoogleDoc)
|
||||||
|
}
|
||||||
|
|
||||||
export default unbody
|
export default unbody
|
||||||
|
|
Loading…
Reference in New Issue