feat: implement article page
This commit is contained in:
parent
bebde816ef
commit
052379afce
|
@ -35,3 +35,5 @@ yarn-error.log*
|
|||
next-env.d.ts
|
||||
|
||||
.idea
|
||||
|
||||
.env
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"@types/react": "18.0.35",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"axios": "^1.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-config-next": "13.3.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
|
||||
author?: string
|
||||
tags?: string[]
|
||||
coverImage?: UnbodyImageBlock
|
||||
coverImage?: UnbodyImageBlock | null
|
||||
summary?: string
|
||||
text?: string
|
||||
toc?: string[]
|
||||
}
|
||||
|
||||
export const PostImageRatioOptions = {
|
||||
|
@ -71,7 +74,14 @@ export default function Post({
|
|||
aspectRatio = PostImageRatio.LANDSCAPE,
|
||||
showImage = true,
|
||||
} = {},
|
||||
data: { coverImage, date: dateStr, title, description, author, tags = [] },
|
||||
data: {
|
||||
coverImage = null,
|
||||
date: dateStr = '',
|
||||
title,
|
||||
description,
|
||||
author,
|
||||
tags = [],
|
||||
},
|
||||
...props
|
||||
}: PostProps) {
|
||||
const date = new Date(dateStr)
|
||||
|
|
|
@ -16,7 +16,11 @@ export default function PostContainer({
|
|||
}: PostContainerProps) {
|
||||
return (
|
||||
<div {...props}>
|
||||
{title && (<Title variant="body1" genericFontFamily="sans-serif">{title}</Title>)}
|
||||
{title && (
|
||||
<Title variant="body1" genericFontFamily="sans-serif">
|
||||
{title}
|
||||
</Title>
|
||||
)}
|
||||
<Container>
|
||||
{postsData.map((post, index) => (
|
||||
<PostWrapper key={index}>
|
||||
|
@ -34,7 +38,7 @@ const Container = styled.div`
|
|||
padding: 16px;
|
||||
gap: 24px;
|
||||
|
||||
// temporariy breakpoint
|
||||
// temporary breakpoint
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
TextField,
|
||||
Autocomplete,
|
||||
IconButton,
|
||||
SearchIcon,
|
||||
CloseIcon,
|
||||
|
@ -15,6 +14,7 @@ import styled from '@emotion/styled'
|
|||
|
||||
export type SearchbarProps = {
|
||||
searchScope?: ESearchScope
|
||||
className?: string
|
||||
}
|
||||
|
||||
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 = {
|
||||
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 { Searchbar } from '@/components/Searchbar'
|
||||
import { ESearchScope } from '@/types/ui.types'
|
||||
import styles from './Article.layout.module.css'
|
||||
|
||||
export default function ArticleLayout(props: PropsWithChildren<any>) {
|
||||
const isDarkState = useIsDarkState()
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
<header className={styles.header}>
|
||||
<Navbar isDark={isDarkState.get()} toggle={isDarkState.toggle} />
|
||||
<NavbarFiller />
|
||||
<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,
|
||||
UnbodyGraphQlResponseGoogleDoc,
|
||||
} from '@/lib/unbody/unbody.types'
|
||||
import { getArticlePostQuery } from '@/queries/getPost'
|
||||
import { getHomePagePostsQuery } from '@/queries/getPosts'
|
||||
|
||||
const { UNBODY_API_KEY, UNBODY_LPE_PROJECT_ID } = process.env
|
||||
|
@ -23,4 +24,10 @@ export const getHomepagePosts = (): Promise<HomepagePost[]> => {
|
|||
.then(({ data }) => data.Get.GoogleDoc)
|
||||
}
|
||||
|
||||
export const getArticlePost = (): Promise<HomepagePost[]> => {
|
||||
return unbody
|
||||
.request<UnbodyGraphQlResponseGoogleDoc>(getArticlePostQuery())
|
||||
.then(({ data }) => data.Get.GoogleDoc)
|
||||
}
|
||||
|
||||
export default unbody
|
||||
|
|
Loading…
Reference in New Issue