mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-23 14:48:08 +00:00
Merge pull request #11 from acid-info/search-view
Implement Search view (WIP)
This commit is contained in:
commit
4e7518eb1e
68
src/components/Article/Article.Block.tsx
Normal file
68
src/components/Article/Article.Block.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { UnbodyImageBlock, UnbodyTextBlock } from '@/lib/unbody/unbody.types'
|
||||||
|
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||||
|
import { ArticleImageBlockWrapper } from './Article.ImageBlockWrapper'
|
||||||
|
import { PostImageRatio } from '../Post/Post'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import {
|
||||||
|
extractClassFromFirstTag,
|
||||||
|
extractIdFromFirstTag,
|
||||||
|
extractInnerHtml,
|
||||||
|
} from '@/utils/html.utils'
|
||||||
|
|
||||||
|
export const RenderArticleBlock = ({
|
||||||
|
block,
|
||||||
|
}: {
|
||||||
|
block: UnbodyImageBlock | UnbodyTextBlock
|
||||||
|
}) => {
|
||||||
|
switch (block.__typename) {
|
||||||
|
case UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock:
|
||||||
|
return (
|
||||||
|
<ArticleImageBlockWrapper
|
||||||
|
ratio={PostImageRatio.LANDSCAPE}
|
||||||
|
image={block}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock:
|
||||||
|
switch (block.tagName) {
|
||||||
|
case 'h1':
|
||||||
|
case 'h2':
|
||||||
|
case 'h3':
|
||||||
|
case 'h4':
|
||||||
|
case 'h5':
|
||||||
|
case 'h6':
|
||||||
|
return (
|
||||||
|
<Headline
|
||||||
|
variant={block.tagName as any}
|
||||||
|
component={block.tagName as any}
|
||||||
|
genericFontFamily="sans-serif"
|
||||||
|
className={extractClassFromFirstTag(block.html) || ''}
|
||||||
|
id={extractIdFromFirstTag(block.html) || ''}
|
||||||
|
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Paragraph
|
||||||
|
variant="body1"
|
||||||
|
component="p"
|
||||||
|
genericFontFamily="sans-serif"
|
||||||
|
className={extractClassFromFirstTag(block.html) || ''}
|
||||||
|
id={extractIdFromFirstTag(block.html) || ''}
|
||||||
|
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Headline = styled(Typography)`
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin-top: 24px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Paragraph = styled(Typography)`
|
||||||
|
white-space: pre-wrap;
|
||||||
|
`
|
35
src/components/Article/Article.ImageBlockWrapper.tsx
Normal file
35
src/components/Article/Article.ImageBlockWrapper.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { UnbodyImageBlock } from '@/lib/unbody/unbody.types'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import React from 'react'
|
||||||
|
import { PostImageRatio, PostImageRatioOptions } from '../Post/Post'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
image: UnbodyImageBlock
|
||||||
|
ratio: PostImageRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArticleImageBlockWrapper = ({ image, ratio }: Props) => {
|
||||||
|
return (
|
||||||
|
<ThumbnailContainer aspectRatio={ratio}>
|
||||||
|
<Thumbnail fill src={image.url} alt={image.alt} />
|
||||||
|
</ThumbnailContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
`
|
@ -8,13 +8,25 @@ import { Collapse } from '@/components/Collapse'
|
|||||||
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
import { moreFromAuthor, references, relatedArticles } from './tempData'
|
import { moreFromAuthor, references, relatedArticles } from './tempData'
|
||||||
import { ArticleReference } from '../ArticleReference'
|
import { ArticleReference } from '../ArticleReference'
|
||||||
import { UnbodyGoogleDoc, UnbodyImageBlock } from '@/lib/unbody/unbody.types'
|
import {
|
||||||
|
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, getContentBlocks } from '@/utils/data.utils'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: UnbodyGoogleDoc
|
data: UnbodyGoogleDoc & {
|
||||||
|
toc: UnbodyTocItem[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Article({ data }: Props) {
|
export default function ArticleBody({ data }: Props) {
|
||||||
const { title, summary, blocks, toc, createdAt, tags } = data
|
const { title, summary, blocks, toc, createdAt, tags } = data
|
||||||
const articleContainer = useArticleContainerContext()
|
const articleContainer = useArticleContainerContext()
|
||||||
const { tocIndex, setTocIndex } = articleContainer
|
const { tocIndex, setTocIndex } = articleContainer
|
||||||
@ -26,60 +38,26 @@ export default function Article({ data }: Props) {
|
|||||||
const date = new Date(createdAt)
|
const date = new Date(createdAt)
|
||||||
|
|
||||||
const _thumbnail = useMemo(() => {
|
const _thumbnail = useMemo(() => {
|
||||||
const imageBlocks: UnbodyImageBlock[] = blocks.filter(
|
const coverImage = getArticleCover(blocks)
|
||||||
(block): block is UnbodyImageBlock => block !== null && 'url' in block,
|
|
||||||
)
|
|
||||||
|
|
||||||
const coverImage = imageBlocks.reduce((prev, curr) =>
|
|
||||||
prev.order < curr.order ? prev : curr,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!coverImage) return null
|
if (!coverImage) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThumbnailContainer aspectRatio={PostImageRatio.LANDSCAPE}>
|
<ArticleImageBlockWrapper
|
||||||
<Thumbnail fill src={coverImage.url} alt={coverImage.alt} />
|
ratio={PostImageRatio.LANDSCAPE}
|
||||||
</ThumbnailContainer>
|
image={coverImage}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}, [blocks])
|
}, [blocks])
|
||||||
|
|
||||||
const _blocks = useMemo(() => {
|
const _blocks = useMemo(() => {
|
||||||
// Exclude title, subtitle, coverImage
|
return getContentBlocks(blocks).map((block, idx) => (
|
||||||
const articleBlocks = blocks.sort((a, b) => a.order - b.order).slice(3)
|
<RenderArticleBlock key={'block-' + idx} block={block} />
|
||||||
|
))
|
||||||
return articleBlocks.map((block, idx) => {
|
|
||||||
return 'url' in block ? (
|
|
||||||
<ThumbnailContainer
|
|
||||||
key={'block-' + idx}
|
|
||||||
aspectRatio={PostImageRatio.LANDSCAPE}
|
|
||||||
>
|
|
||||||
<Thumbnail fill src={block.url} alt={block.alt} />
|
|
||||||
</ThumbnailContainer>
|
|
||||||
) : block.tagName.startsWith('h') ? (
|
|
||||||
<Headline
|
|
||||||
variant="body2"
|
|
||||||
component={block.tagName as any}
|
|
||||||
genericFontFamily="sans-serif"
|
|
||||||
key={'block-' + idx}
|
|
||||||
dangerouslySetInnerHTML={{ __html: block.html }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Paragraph
|
|
||||||
variant="body1"
|
|
||||||
component="p"
|
|
||||||
genericFontFamily="sans-serif"
|
|
||||||
key={'block-' + idx}
|
|
||||||
dangerouslySetInnerHTML={{ __html: block.html }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [blocks])
|
}, [blocks])
|
||||||
|
|
||||||
const _mobileToc = useMemo(
|
const _mobileToc = useMemo(
|
||||||
() =>
|
() =>
|
||||||
toc?.length > 0 && (
|
toc?.length > 0 && (
|
||||||
<Collapse className={styles.mobileToc} label="Contents">
|
<Collapse className={styles.mobileToc} label="Contents">
|
||||||
{/* @ts-ignore */}
|
|
||||||
{toc.map((toc, idx) => (
|
{toc.map((toc, idx) => (
|
||||||
<Content
|
<Content
|
||||||
onClick={() => setTocIndex(idx)}
|
onClick={() => setTocIndex(idx)}
|
||||||
@ -87,7 +65,7 @@ export default function Article({ data }: Props) {
|
|||||||
variant="body3"
|
variant="body3"
|
||||||
key={idx}
|
key={idx}
|
||||||
>
|
>
|
||||||
{toc}
|
{toc.title}
|
||||||
</Content>
|
</Content>
|
||||||
))}
|
))}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
@ -161,11 +139,9 @@ export default function Article({ data }: Props) {
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* assign id for toc scroll */}
|
|
||||||
|
|
||||||
<Title
|
<Title
|
||||||
/*
|
/*
|
||||||
// @ts-ignore */
|
// @ts-ignore */
|
||||||
id={toc[0].href.substring(1)}
|
id={toc[0].href.substring(1)}
|
||||||
variant={'h1'}
|
variant={'h1'}
|
||||||
genericFontFamily="serif"
|
genericFontFamily="serif"
|
||||||
@ -259,32 +235,6 @@ const TextContainer = styled.div`
|
|||||||
margin-bottom: 80px;
|
margin-bottom: 80px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Headline = styled(Typography)`
|
|
||||||
white-space: pre-wrap;
|
|
||||||
margin-top: 24px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Paragraph = styled(Typography)`
|
|
||||||
white-space: pre-wrap;
|
|
||||||
`
|
|
||||||
|
|
||||||
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`
|
const Row = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
34
src/components/FeaturedPost/FeaturedPost.tsx
Normal file
34
src/components/FeaturedPost/FeaturedPost.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { Grid, GridItem } from '../Grid/Grid'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import Post, { PostDataProps } from '../Post/Post'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
post: PostDataProps
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeaturedPost = ({ post }: Props) => {
|
||||||
|
return (
|
||||||
|
<Grid>
|
||||||
|
<GridItem className="w-16">
|
||||||
|
<PostLink href={`/article/${post.remoteId}`}>
|
||||||
|
<PostWrapper>
|
||||||
|
<Post data={post} />
|
||||||
|
</PostWrapper>
|
||||||
|
</PostLink>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostWrapper = styled.div`
|
||||||
|
padding: 16px 0;
|
||||||
|
border-top: 1px solid rgb(var(--lsd-theme-primary));
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PostLink = styled(Link)`
|
||||||
|
text-decoration: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default FeaturedPost
|
1
src/components/FeaturedPost/index.ts
Normal file
1
src/components/FeaturedPost/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as FeaturedPost } from './FeaturedPost'
|
44
src/components/Grid/Grid.tsx
Normal file
44
src/components/Grid/Grid.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
export const Grid = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(16, 1fr);
|
||||||
|
padding: 16px;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GridItem = styled.div`
|
||||||
|
grid-column: span 4;
|
||||||
|
|
||||||
|
&.w-1 {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w-2 {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w-3 {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w-4 {
|
||||||
|
grid-column: span 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w-8 {
|
||||||
|
grid-column: span 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w-16 {
|
||||||
|
grid-column: span 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-column: span 16 !important;
|
||||||
|
}
|
||||||
|
`
|
13
src/components/Main/Main.tsx
Normal file
13
src/components/Main/Main.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { uiConfigs } from '@/configs/ui.configs'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
const Main = ({ children }: PropsWithChildren) => {
|
||||||
|
return <Container>{children}</Container>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.main`
|
||||||
|
margin-block: ${uiConfigs.postSectionMargin}px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default Main
|
1
src/components/Main/index.ts
Normal file
1
src/components/Main/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Main } from './Main'
|
@ -1,43 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { PostContainer } from '../PostContainer'
|
|
||||||
import { PostDataProps, PostProps } from './Post'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
posts: PostDataProps[]
|
|
||||||
featuredPost: PostDataProps
|
|
||||||
}
|
|
||||||
|
|
||||||
const PostsDemo = (props: Props) => {
|
|
||||||
const [posts, setPosts] = useState<PostDataProps[]>(props.posts)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginBlock: '78px' }}>
|
|
||||||
{/* For Demo purposes only. Use inline CSS and styled components temporarily */}
|
|
||||||
{/*@TODO @jinho, wht PostContainer should recive an array of postData instead of only One?*/}
|
|
||||||
<PostContainer
|
|
||||||
title="Featured"
|
|
||||||
postsData={[
|
|
||||||
{
|
|
||||||
data: props.featuredPost,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{posts.length > 0 ? (
|
|
||||||
<PostContainer
|
|
||||||
style={{ marginTop: '108px' }}
|
|
||||||
title="Latest Posts"
|
|
||||||
postsData={posts.map((post) => ({
|
|
||||||
appearance: {},
|
|
||||||
data: post,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div style={{ marginTop: '108px', textAlign: 'center' }}>
|
|
||||||
<h3>No Posts found!</h3>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostsDemo
|
|
@ -1,3 +1,2 @@
|
|||||||
export { default as Post } from './Post'
|
export { default as Post } from './Post'
|
||||||
export { default as PostsDemo } from './PostsDemo'
|
|
||||||
export type { PostProps } from './Post'
|
export type { PostProps } from './Post'
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import { CommonProps } from '@acid-info/lsd-react/dist/utils/useCommonProps'
|
|
||||||
import styled from '@emotion/styled'
|
|
||||||
import { Post, PostProps } from '../Post'
|
|
||||||
import { Typography } from '@acid-info/lsd-react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
export type PostContainerProps = CommonProps &
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & {
|
|
||||||
title?: string
|
|
||||||
postsData: PostProps[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PostContainer({
|
|
||||||
title,
|
|
||||||
postsData,
|
|
||||||
...props
|
|
||||||
}: PostContainerProps) {
|
|
||||||
return (
|
|
||||||
<div {...props}>
|
|
||||||
{title && (
|
|
||||||
<Title variant="body1" genericFontFamily="sans-serif">
|
|
||||||
{title}
|
|
||||||
</Title>
|
|
||||||
)}
|
|
||||||
<Container>
|
|
||||||
{postsData.map((post, index) => (
|
|
||||||
<PostLink key={index} href={`/article/${post.data.remoteId}`}>
|
|
||||||
<PostWrapper>
|
|
||||||
<Post {...post} />
|
|
||||||
</PostWrapper>
|
|
||||||
</PostLink>
|
|
||||||
))}
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 16px;
|
|
||||||
gap: 24px;
|
|
||||||
|
|
||||||
// temporary breakpoint
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const PostWrapper = styled.div`
|
|
||||||
padding: 16px 0;
|
|
||||||
border-top: 1px solid rgb(var(--lsd-theme-primary));
|
|
||||||
width: 100%;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Title = styled(Typography)`
|
|
||||||
padding: 0 16px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const PostLink = styled(Link)`
|
|
||||||
text-decoration: none;
|
|
||||||
`
|
|
@ -1 +0,0 @@
|
|||||||
export { default as PostContainer } from './PostContainer'
|
|
39
src/components/PostList/PostList.tsx
Normal file
39
src/components/PostList/PostList.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Grid, GridItem } from '../Grid/Grid'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import Post, { PostDataProps } from '../Post/Post'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
posts: PostDataProps[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PostsList = (props: Props) => {
|
||||||
|
const [posts, setPosts] = useState<PostDataProps[]>(props.posts)
|
||||||
|
|
||||||
|
//TODO pagination
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid>
|
||||||
|
{posts.map((post, index) => (
|
||||||
|
<GridItem className="w-4" key={index}>
|
||||||
|
<PostLink href={`/article/${post.remoteId}`}>
|
||||||
|
<PostWrapper>
|
||||||
|
<Post data={post} />
|
||||||
|
</PostWrapper>
|
||||||
|
</PostLink>
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostWrapper = styled.div`
|
||||||
|
padding: 16px 0;
|
||||||
|
border-top: 1px solid rgb(var(--lsd-theme-primary));
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PostLink = styled(Link)`
|
||||||
|
text-decoration: none;
|
||||||
|
`
|
36
src/components/Section/Section.tsx
Normal file
36
src/components/Section/Section.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
type Props = PropsWithChildren<{
|
||||||
|
title: string
|
||||||
|
matches?: number
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const Section = ({ title, matches, children, ...props }: Props) => {
|
||||||
|
return (
|
||||||
|
<section {...props}>
|
||||||
|
<Container>
|
||||||
|
<Typography genericFontFamily="sans-serif" variant="body2">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{matches && (
|
||||||
|
<>
|
||||||
|
<Typography variant="body2">•</Typography>
|
||||||
|
<Typography genericFontFamily="sans-serif" variant="body2">
|
||||||
|
{matches} matches
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 16px;
|
||||||
|
`
|
@ -14,7 +14,7 @@ type Props = {
|
|||||||
export default function TableOfContents({ contents, ...props }: Props) {
|
export default function TableOfContents({ contents, ...props }: Props) {
|
||||||
const articleContainer = useArticleContainerContext()
|
const articleContainer = useArticleContainerContext()
|
||||||
const { tocIndex, setTocIndex } = articleContainer
|
const { tocIndex, setTocIndex } = articleContainer
|
||||||
const dy = uiConfigs.navbarRenderedHeight + uiConfigs.postMarginTop
|
const dy = uiConfigs.navbarRenderedHeight + uiConfigs.postSectionMargin
|
||||||
|
|
||||||
const { sticky, stickyRef, height } = useSticky<HTMLDivElement>(dy)
|
const { sticky, stickyRef, height } = useSticky<HTMLDivElement>(dy)
|
||||||
|
|
||||||
@ -86,7 +86,8 @@ const Contents = styled.div<{ height: number }>`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: calc(
|
height: calc(
|
||||||
100vh - ${uiConfigs.navbarRenderedHeight + uiConfigs.postMarginTop + 40}px
|
100vh -
|
||||||
|
${uiConfigs.navbarRenderedHeight + uiConfigs.postSectionMargin + 40}px
|
||||||
);
|
);
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const uiConfigs = {
|
export const uiConfigs = {
|
||||||
navbarRenderedHeight: 45,
|
navbarRenderedHeight: 45,
|
||||||
postMarginTop: 78,
|
postSectionMargin: 78,
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,37 @@
|
|||||||
import { Article } from '@/components/Article'
|
|
||||||
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 { uiConfigs } from '@/configs/ui.configs'
|
||||||
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
import { UnbodyGoogleDoc, UnbodyTocItem } from '@/lib/unbody/unbody.types'
|
||||||
|
import ArticleBody from '@/components/Article/ArticleBody'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: UnbodyGoogleDoc
|
data: UnbodyGoogleDoc & {
|
||||||
error: string | null
|
toc: UnbodyTocItem[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArticleContainer = (props: Props) => {
|
const ArticleContainer = (props: Props) => {
|
||||||
const { data, error } = props
|
const { data } = props
|
||||||
const [tocIndex, setTocIndex] = useState(0)
|
const [tocIndex, setTocIndex] = useState(0)
|
||||||
|
|
||||||
return !error?.length ? (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<ArticleContainerContext.Provider
|
<ArticleContainerContext.Provider
|
||||||
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
||||||
>
|
>
|
||||||
<TableOfContents contents={data.toc ?? []} />
|
<TableOfContents contents={data.toc ?? []} />
|
||||||
<Article data={data} />
|
<ArticleBody data={data} />
|
||||||
<Right />
|
<Right />
|
||||||
</ArticleContainerContext.Provider>
|
</ArticleContainerContext.Provider>
|
||||||
</Container>
|
</Container>
|
||||||
) : (
|
|
||||||
<div>{error}</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: ${uiConfigs.postMarginTop}px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const Right = styled.aside`
|
const Right = styled.aside`
|
||||||
|
@ -6,6 +6,7 @@ import { Searchbar } from '@/components/Searchbar'
|
|||||||
import { ESearchScope } from '@/types/ui.types'
|
import { ESearchScope } from '@/types/ui.types'
|
||||||
import styles from './Article.layout.module.css'
|
import styles from './Article.layout.module.css'
|
||||||
import { Footer } from '@/components/Footer'
|
import { Footer } from '@/components/Footer'
|
||||||
|
import { Main } from '@/components/Main'
|
||||||
|
|
||||||
export default function ArticleLayout(props: PropsWithChildren<any>) {
|
export default function ArticleLayout(props: PropsWithChildren<any>) {
|
||||||
const isDarkState = useIsDarkState()
|
const isDarkState = useIsDarkState()
|
||||||
@ -16,7 +17,7 @@ export default function ArticleLayout(props: PropsWithChildren<any>) {
|
|||||||
<NavbarFiller />
|
<NavbarFiller />
|
||||||
<Searchbar searchScope={ESearchScope.ARTICLE} />
|
<Searchbar searchScope={ESearchScope.ARTICLE} />
|
||||||
</header>
|
</header>
|
||||||
<main>{props.children}</main>
|
<Main>{props.children}</Main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -5,6 +5,7 @@ import { Hero } from '@/components/Hero'
|
|||||||
import { NavbarFiller } from '@/components/Navbar/NavbarFiller'
|
import { NavbarFiller } from '@/components/Navbar/NavbarFiller'
|
||||||
import { Searchbar } from '@/components/Searchbar'
|
import { Searchbar } from '@/components/Searchbar'
|
||||||
import { Footer } from '@/components/Footer'
|
import { Footer } from '@/components/Footer'
|
||||||
|
import { Main } from '@/components/Main'
|
||||||
|
|
||||||
export default function DefaultLayout(props: PropsWithChildren<any>) {
|
export default function DefaultLayout(props: PropsWithChildren<any>) {
|
||||||
const isDarkState = useIsDarkState()
|
const isDarkState = useIsDarkState()
|
||||||
@ -17,7 +18,7 @@ export default function DefaultLayout(props: PropsWithChildren<any>) {
|
|||||||
<Hero />
|
<Hero />
|
||||||
<Searchbar />
|
<Searchbar />
|
||||||
</header>
|
</header>
|
||||||
<main>{props.children}</main>
|
<Main>{props.children}</Main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
3
src/layouts/SearchLayout/Search.layout.module.css
Normal file
3
src/layouts/SearchLayout/Search.layout.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.header > nav {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
24
src/layouts/SearchLayout/Search.layout.tsx
Normal file
24
src/layouts/SearchLayout/Search.layout.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Navbar } from '@/components/Navbar'
|
||||||
|
import useIsDarkState from '@/states/isDarkState/isDarkState'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import { NavbarFiller } from '@/components/Navbar/NavbarFiller'
|
||||||
|
import { Searchbar } from '@/components/Searchbar'
|
||||||
|
import { ESearchScope } from '@/types/ui.types'
|
||||||
|
import styles from './Search.layout.module.css'
|
||||||
|
import { Footer } from '@/components/Footer'
|
||||||
|
import { Main } from '@/components/Main'
|
||||||
|
|
||||||
|
export default function SearchLayout(props: PropsWithChildren<any>) {
|
||||||
|
const isDarkState = useIsDarkState()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Navbar isDark={isDarkState.get()} toggle={isDarkState.toggle} />
|
||||||
|
<NavbarFiller />
|
||||||
|
<Searchbar searchScope={ESearchScope.ARTICLE} />
|
||||||
|
</header>
|
||||||
|
<Main>{props.children}</Main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
1
src/layouts/SearchLayout/index.ts
Normal file
1
src/layouts/SearchLayout/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as SearchLayout } from './Search.layout'
|
@ -93,8 +93,13 @@ export namespace UnbodyGraphQl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace Fragments {
|
export namespace Fragments {
|
||||||
export interface TocItem {}
|
export interface TocItem {
|
||||||
|
tag: string
|
||||||
|
blockIndex: number
|
||||||
|
href: string
|
||||||
|
title: string
|
||||||
|
level: number
|
||||||
|
}
|
||||||
export interface FootnoteItem {}
|
export interface FootnoteItem {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +157,7 @@ export namespace UnbodyGraphQl {
|
|||||||
modifiedAt: string
|
modifiedAt: string
|
||||||
originalName: string
|
originalName: string
|
||||||
path: string[]
|
path: string[]
|
||||||
pathstring: string
|
pathString: string
|
||||||
remoteId: string
|
remoteId: string
|
||||||
size: number
|
size: number
|
||||||
sourceId: string
|
sourceId: string
|
||||||
@ -298,7 +303,7 @@ export namespace UnbodyGraphQl {
|
|||||||
|
|
||||||
export interface WhereOperandsInpObj {
|
export interface WhereOperandsInpObj {
|
||||||
operator?: WhereOperatorEnum
|
operator?: WhereOperatorEnum
|
||||||
path: string[]
|
path: string[] | string
|
||||||
operands?: WhereOperandsInpObj[]
|
operands?: WhereOperandsInpObj[]
|
||||||
valueGeoRange?: WhereGeoRangeInpObj
|
valueGeoRange?: WhereGeoRangeInpObj
|
||||||
valueNumber?: number
|
valueNumber?: number
|
||||||
@ -330,7 +335,7 @@ export namespace UnbodyGraphQl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WhereInpObj {
|
export interface WhereInpObj {
|
||||||
path?: string[]
|
path?: string[] | string
|
||||||
valueInt?: number
|
valueInt?: number
|
||||||
valueNumber?: number
|
valueNumber?: number
|
||||||
valueGeoRange?: WhereGeoRangeInpObj
|
valueGeoRange?: WhereGeoRangeInpObj
|
||||||
|
@ -6,6 +6,7 @@ export type UnbodyTextBlock = UnbodyGraphQl.TextBlock
|
|||||||
export type UnbodyImageBlock = UnbodyGraphQl.ImageBlock
|
export type UnbodyImageBlock = UnbodyGraphQl.ImageBlock
|
||||||
export type UnbodyAudio = UnbodyGraphQl.AudioFile
|
export type UnbodyAudio = UnbodyGraphQl.AudioFile
|
||||||
export type UnbodyGetFilters = UnbodyGraphQl.Filters.GetDocsArgs
|
export type UnbodyGetFilters = UnbodyGraphQl.Filters.GetDocsArgs
|
||||||
|
export type UnbodyTocItem = UnbodyGraphQl.Fragments.TocItem
|
||||||
|
|
||||||
export * as UnbodyGraphQl from './unbody-content.types'
|
export * as UnbodyGraphQl from './unbody-content.types'
|
||||||
|
|
||||||
|
@ -7,35 +7,16 @@ import { ArticlePostData } from '@/types/data.types'
|
|||||||
import { SEO } from '@/components/SEO'
|
import { SEO } from '@/components/SEO'
|
||||||
|
|
||||||
type ArticleProps = {
|
type ArticleProps = {
|
||||||
data: ArticlePostData | null
|
data: ArticlePostData
|
||||||
error: string | null
|
errors: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
const ArticlePage = ({ data, errors }: ArticleProps) => {
|
||||||
const { remoteId } = params!
|
if (errors) return <div>{errors}</div>
|
||||||
|
|
||||||
if (!remoteId) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { data: article, errors } = await api.getArticlePost(remoteId as string)
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
data: article,
|
|
||||||
error: errors,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @jinho lets handle the error directly in thew page component
|
|
||||||
const ArticlePage = (props: ArticleProps) => {
|
|
||||||
if (!props.data) return <div style={{ height: '100vh' }} />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEO title={props.data.title} description={props.data.summary} />
|
<SEO title={data.title} description={data.summary} />
|
||||||
<ArticleContainer data={props.data} error={props.error} />
|
<ArticleContainer data={data} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -50,6 +31,30 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
|
const { remoteId } = params!
|
||||||
|
|
||||||
|
if (!remoteId) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data: article, errors } = await api.getArticlePost(remoteId as string)
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
data: article,
|
||||||
|
error: JSON.stringify(errors),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ArticlePage.getLayout = function getLayout(page: ReactNode) {
|
ArticlePage.getLayout = function getLayout(page: ReactNode) {
|
||||||
return <ArticleLayout>{page}</ArticleLayout>
|
return <ArticleLayout>{page}</ArticleLayout>
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,60 @@
|
|||||||
import { PostDataProps } from '@/components/Post/Post'
|
|
||||||
import PostsDemo from '@/components/Post/PostsDemo'
|
|
||||||
import { UnbodyGoogleDoc, UnbodyImageBlock } from '@/lib/unbody/unbody.types'
|
|
||||||
import api from '@/services/unbody.service'
|
import api from '@/services/unbody.service'
|
||||||
import { ESearchStatus } from '@/types/ui.types'
|
|
||||||
import { GetStaticProps } from 'next'
|
import { PostDataProps } from '@/components/Post/Post'
|
||||||
|
import { PostsList } from '@/components/PostList/PostList'
|
||||||
|
import { Section } from '@/components/Section/Section'
|
||||||
|
|
||||||
|
import { getArticleCover } from '@/utils/data.utils'
|
||||||
|
import { FeaturedPost } from '@/components/FeaturedPost'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
posts: PostDataProps[]
|
posts: PostDataProps[]
|
||||||
|
featured: PostDataProps | null
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ posts }: Props) {
|
export default function Home({ posts, featured }: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostsDemo posts={posts} featuredPost={posts[0]} />
|
{featured && (
|
||||||
|
<Section title={'Featured'}>
|
||||||
|
<FeaturedPost post={featured} />
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
<Section title={'Latest posts'}>
|
||||||
|
<PostsList posts={posts} />
|
||||||
|
</Section>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
const { data: posts, errors } = await api.getHomepagePosts()
|
const {
|
||||||
|
data: { posts, featured },
|
||||||
|
errors,
|
||||||
|
} = await api.getHomepagePosts()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
featured: featured
|
||||||
|
? {
|
||||||
|
remoteId: featured.remoteId,
|
||||||
|
date: featured.modifiedAt,
|
||||||
|
title: featured.title,
|
||||||
|
description: featured.subtitle, // TODO: summary is not available
|
||||||
|
author: 'Jinho',
|
||||||
|
tags: featured.tags,
|
||||||
|
coverImage: getArticleCover(featured.blocks),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
posts: posts.map((post) => ({
|
posts: posts.map((post) => ({
|
||||||
remoteId: post.remoteId,
|
remoteId: post.remoteId,
|
||||||
date: post.modifiedAt,
|
date: post.modifiedAt,
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.summary,
|
description: post.subtitle, // TODO: summary is not available
|
||||||
author: 'Jinho',
|
author: 'Jinho',
|
||||||
tags: post.tags,
|
tags: post.tags,
|
||||||
...(post.blocks && post.blocks!.length > 0
|
coverImage: getArticleCover(post.blocks),
|
||||||
? { coverImage: post.blocks![0] as UnbodyImageBlock }
|
|
||||||
: {}),
|
|
||||||
})),
|
})),
|
||||||
errors,
|
errors,
|
||||||
},
|
},
|
||||||
|
@ -21,8 +21,12 @@ import {
|
|||||||
extractTopicsFromQuery,
|
extractTopicsFromQuery,
|
||||||
} from '@/utils/search.utils'
|
} from '@/utils/search.utils'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { SearchLayout } from '@/layouts/SearchLayout'
|
||||||
|
import { Section } from '@/components/Section/Section'
|
||||||
|
import { PostsList } from '@/components/PostList/PostList'
|
||||||
|
import { getArticleCover } from '@/utils/data.utils'
|
||||||
|
|
||||||
interface SearchPageProps {
|
interface SearchPageProps {
|
||||||
articles: SearchResultItem<UnbodyGoogleDoc>[]
|
articles: SearchResultItem<UnbodyGoogleDoc>[]
|
||||||
@ -73,24 +77,22 @@ export default function SearchPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<section>
|
{articles.data?.length && (
|
||||||
<strong>Related articles</strong>
|
<Section title={'Related Articles'} matches={articles.data?.length}>
|
||||||
<hr />
|
<PostsList
|
||||||
<div>
|
posts={articles.data.map((article) => ({
|
||||||
{articles.loading && <div>...</div>}
|
remoteId: article.doc.remoteId,
|
||||||
{!articles.error && articles.data && articles.data.length > 0 ? (
|
date: article.doc.modifiedAt,
|
||||||
articles.data.map((article: SearchResultItem<UnbodyGoogleDoc>) => (
|
title: article.doc.title,
|
||||||
<div key={article.doc.remoteId}>
|
description: article.doc.subtitle, // TODO: summary is not available
|
||||||
<h2>{article.doc.title}</h2>
|
author: 'Jinho',
|
||||||
<p>{article.doc.summary}</p>
|
tags: article.doc.tags,
|
||||||
</div>
|
coverImage: getArticleCover(article.doc.blocks),
|
||||||
))
|
}))}
|
||||||
) : (
|
/>
|
||||||
<div>Nothing found</div>
|
</Section>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<br />
|
|
||||||
<section>
|
<section>
|
||||||
<strong>Related content blocks</strong>
|
<strong>Related content blocks</strong>
|
||||||
<hr />
|
<hr />
|
||||||
@ -156,6 +158,10 @@ export default function SearchPage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SearchPage.getLayout = function getLayout(page: ReactNode) {
|
||||||
|
return <SearchLayout>{page}</SearchLayout>
|
||||||
|
}
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const { data: articles = [] } = await unbodyApi.searchArticles()
|
const { data: articles = [] } = await unbodyApi.searchArticles()
|
||||||
const { data: blocks = [] } = await unbodyApi.serachBlocks()
|
const { data: blocks = [] } = await unbodyApi.serachBlocks()
|
||||||
|
@ -10,22 +10,26 @@ export const getArticlePostQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
|||||||
sourceId
|
sourceId
|
||||||
remoteId
|
remoteId
|
||||||
title
|
title
|
||||||
|
subtitle
|
||||||
summary
|
summary
|
||||||
tags
|
tags
|
||||||
createdAt
|
createdAt
|
||||||
modifiedAt
|
modifiedAt
|
||||||
toc
|
toc
|
||||||
|
slug
|
||||||
blocks{
|
blocks{
|
||||||
...on ImageBlock{
|
...on ImageBlock{
|
||||||
url
|
url
|
||||||
alt
|
alt
|
||||||
order
|
order
|
||||||
|
__typename
|
||||||
}
|
}
|
||||||
... on TextBlock {
|
... on TextBlock {
|
||||||
footnotes
|
footnotes
|
||||||
html
|
html
|
||||||
order
|
order
|
||||||
tagName
|
tagName
|
||||||
|
__typename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
@ -12,14 +12,18 @@ export const getHomePagePostsQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
|||||||
GetGoogleDocQuery(args)(`
|
GetGoogleDocQuery(args)(`
|
||||||
remoteId
|
remoteId
|
||||||
title
|
title
|
||||||
|
subtitle
|
||||||
summary
|
summary
|
||||||
tags
|
tags
|
||||||
createdAt
|
createdAt
|
||||||
modifiedAt
|
modifiedAt
|
||||||
|
pathString
|
||||||
blocks{
|
blocks{
|
||||||
...on ImageBlock{
|
...on ImageBlock{
|
||||||
url
|
url
|
||||||
alt
|
alt
|
||||||
|
order
|
||||||
|
__typename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
@ -7,9 +7,18 @@ export const getSearchArticlesQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
|||||||
GetGoogleDocQuery(args)(`
|
GetGoogleDocQuery(args)(`
|
||||||
remoteId
|
remoteId
|
||||||
title
|
title
|
||||||
|
subtitle
|
||||||
summary
|
summary
|
||||||
tags
|
tags
|
||||||
modifiedAt
|
modifiedAt
|
||||||
|
blocks{
|
||||||
|
...on ImageBlock{
|
||||||
|
url
|
||||||
|
alt
|
||||||
|
order
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
_additional{
|
_additional{
|
||||||
certainty
|
certainty
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,13 @@ import {
|
|||||||
UnbodyImageBlock,
|
UnbodyImageBlock,
|
||||||
UnbodyTextBlock,
|
UnbodyTextBlock,
|
||||||
UnbodyGraphQlResponseBlocks,
|
UnbodyGraphQlResponseBlocks,
|
||||||
|
UnbodyGraphQlResponse,
|
||||||
} from '@/lib/unbody/unbody.types'
|
} from '@/lib/unbody/unbody.types'
|
||||||
|
|
||||||
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||||
|
|
||||||
|
type WhereOperandsInpObj = UnbodyGraphQl.Filters.WhereOperandsInpObj
|
||||||
|
|
||||||
import { getArticlePostQuery } from '@/queries/getPost'
|
import { getArticlePostQuery } from '@/queries/getPost'
|
||||||
import { getHomePagePostsQuery } from '@/queries/getPosts'
|
import { getHomePagePostsQuery } from '@/queries/getPosts'
|
||||||
import { getAllPostsSlugQuery } from '@/queries/getPostsSlugs'
|
import { getAllPostsSlugQuery } from '@/queries/getPostsSlugs'
|
||||||
@ -35,8 +38,41 @@ type HomepagePost = Pick<
|
|||||||
| 'blocks'
|
| 'blocks'
|
||||||
>
|
>
|
||||||
|
|
||||||
|
type HomePageData = {
|
||||||
|
posts: HomepagePost[]
|
||||||
|
featured: HomepagePost | null
|
||||||
|
}
|
||||||
|
|
||||||
type UnbodyDocTypes = UnbodyGoogleDoc | UnbodyImageBlock | UnbodyTextBlock
|
type UnbodyDocTypes = UnbodyGoogleDoc | UnbodyImageBlock | UnbodyTextBlock
|
||||||
|
|
||||||
|
const OperandFactory = (
|
||||||
|
operator: UnbodyGraphQl.Filters.WhereOperatorEnum,
|
||||||
|
path: string | string[],
|
||||||
|
value: string,
|
||||||
|
valuePath: string,
|
||||||
|
): WhereOperandsInpObj => ({
|
||||||
|
path,
|
||||||
|
operator,
|
||||||
|
[valuePath]: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Operands: Record<string, (...a: any) => WhereOperandsInpObj> = {
|
||||||
|
WHERE_PUBLISHED: () =>
|
||||||
|
OperandFactory(
|
||||||
|
UnbodyGraphQl.Filters.WhereOperatorEnum.Like,
|
||||||
|
'pathString',
|
||||||
|
'*/published/*',
|
||||||
|
'valueString',
|
||||||
|
),
|
||||||
|
WHERE_ID_IS: (id) =>
|
||||||
|
OperandFactory(
|
||||||
|
UnbodyGraphQl.Filters.WhereOperatorEnum.Equal,
|
||||||
|
'remoteId',
|
||||||
|
id,
|
||||||
|
'valueString',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
const mapSearchResultItem = <T extends UnbodyDocTypes>(
|
const mapSearchResultItem = <T extends UnbodyDocTypes>(
|
||||||
q: string,
|
q: string,
|
||||||
tags: string[],
|
tags: string[],
|
||||||
@ -68,17 +104,33 @@ class UnbodyService extends UnbodyClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getHomepagePosts = (): Promise<ApiResponse<HomepagePost[]>> => {
|
getHomepagePosts = (): Promise<ApiResponse<HomePageData>> => {
|
||||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(getHomePagePostsQuery())
|
const query = getHomePagePostsQuery({
|
||||||
|
where: Operands.WHERE_PUBLISHED(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
if (!data) return this.handleResponse([], 'No data')
|
if (!data)
|
||||||
return this.handleResponse(data.Get.GoogleDoc)
|
return this.handleResponse({ featured: null, posts: [] }, 'No data')
|
||||||
|
const featured =
|
||||||
|
data.Get.GoogleDoc.find((post) =>
|
||||||
|
post.pathString.includes('/featured/'),
|
||||||
|
) || null
|
||||||
|
const posts = data.Get.GoogleDoc.filter(
|
||||||
|
(post) => !post.pathString.includes('/featured/'),
|
||||||
|
)
|
||||||
|
return this.handleResponse({ featured, posts })
|
||||||
})
|
})
|
||||||
.catch((e) => this.handleResponse([], e))
|
.catch((e) => this.handleResponse({ featured: null, posts: [] }, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllArticlePostSlugs = (): Promise<ApiResponse<{ remoteId: string }[]>> => {
|
getAllArticlePostSlugs = (): Promise<ApiResponse<{ remoteId: string }[]>> => {
|
||||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(getAllPostsSlugQuery())
|
return this.request<UnbodyGraphQlResponseGoogleDoc>(
|
||||||
|
getAllPostsSlugQuery({
|
||||||
|
where: Operands.WHERE_PUBLISHED(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
if (!data) return this.handleResponse([], 'No data')
|
if (!data) return this.handleResponse([], 'No data')
|
||||||
return this.handleResponse(data.Get.GoogleDoc)
|
return this.handleResponse(data.Get.GoogleDoc)
|
||||||
@ -88,13 +140,15 @@ class UnbodyService extends UnbodyClient {
|
|||||||
|
|
||||||
getArticlePost = (
|
getArticlePost = (
|
||||||
id: string,
|
id: string,
|
||||||
|
published: boolean = true,
|
||||||
): Promise<ApiResponse<UnbodyGoogleDoc | null>> => {
|
): Promise<ApiResponse<UnbodyGoogleDoc | null>> => {
|
||||||
const query = getArticlePostQuery({
|
const query = getArticlePostQuery({
|
||||||
where: {
|
where: published
|
||||||
path: ['remoteId'],
|
? {
|
||||||
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.Equal,
|
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.And,
|
||||||
valueString: id,
|
operands: [Operands.WHERE_PUBLISHED(), Operands.WHERE_ID_IS(id)],
|
||||||
},
|
}
|
||||||
|
: Operands.WHERE_ID_IS(id),
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
||||||
@ -103,6 +157,7 @@ class UnbodyService extends UnbodyClient {
|
|||||||
const article = data.Get.GoogleDoc[0]
|
const article = data.Get.GoogleDoc[0]
|
||||||
return this.handleResponse({
|
return this.handleResponse({
|
||||||
...article,
|
...article,
|
||||||
|
blocks: article.blocks.sort((a, b) => a.order - b.order),
|
||||||
toc: JSON.parse(
|
toc: JSON.parse(
|
||||||
article.toc as string,
|
article.toc as string,
|
||||||
) as Array<UnbodyGraphQl.Fragments.TocItem>,
|
) as Array<UnbodyGraphQl.Fragments.TocItem>,
|
||||||
@ -114,6 +169,7 @@ class UnbodyService extends UnbodyClient {
|
|||||||
serachBlocks = async (
|
serachBlocks = async (
|
||||||
q: string = '',
|
q: string = '',
|
||||||
tags: string[] = [],
|
tags: string[] = [],
|
||||||
|
published: boolean = true,
|
||||||
): Promise<
|
): Promise<
|
||||||
ApiResponse<SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]>
|
ApiResponse<SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]>
|
||||||
> => {
|
> => {
|
||||||
@ -124,12 +180,14 @@ class UnbodyService extends UnbodyClient {
|
|||||||
concepts: [q, ...tags],
|
concepts: [q, ...tags],
|
||||||
certainty: 0.75,
|
certainty: 0.75,
|
||||||
},
|
},
|
||||||
|
...(published
|
||||||
|
? {
|
||||||
|
where: Operands.WHERE_PUBLISHED(),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(query)
|
|
||||||
|
|
||||||
return this.request<UnbodyGraphQlResponseBlocks>(query)
|
return this.request<UnbodyGraphQlResponseBlocks>(query)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
if (!data || !(data.Get.ImageBlock || data.Get.TextBlock))
|
if (!data || !(data.Get.ImageBlock || data.Get.TextBlock))
|
||||||
@ -147,16 +205,20 @@ class UnbodyService extends UnbodyClient {
|
|||||||
searchArticles = (
|
searchArticles = (
|
||||||
q: string = '',
|
q: string = '',
|
||||||
tags: string[] = [],
|
tags: string[] = [],
|
||||||
|
published: boolean = true,
|
||||||
): Promise<ApiResponse<SearchResultItem<UnbodyGoogleDoc>[]>> => {
|
): Promise<ApiResponse<SearchResultItem<UnbodyGoogleDoc>[]>> => {
|
||||||
const query = getSearchArticlesQuery({
|
let queryFilters = {}
|
||||||
...(q.trim().length > 0
|
if (q.trim().length > 0) {
|
||||||
? {
|
queryFilters = {
|
||||||
nearText: {
|
nearText: {
|
||||||
concepts: [q],
|
concepts: [q],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
}
|
||||||
...((tags.length > 0 && {
|
|
||||||
|
if (tags.length > 0) {
|
||||||
|
queryFilters = {
|
||||||
|
...queryFilters,
|
||||||
where: {
|
where: {
|
||||||
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.And,
|
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.And,
|
||||||
operands: tags.map((tag) => ({
|
operands: tags.map((tag) => ({
|
||||||
@ -165,9 +227,20 @@ class UnbodyService extends UnbodyClient {
|
|||||||
valueString: tag,
|
valueString: tag,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}) ||
|
}
|
||||||
{}),
|
}
|
||||||
})
|
|
||||||
|
if (published) {
|
||||||
|
queryFilters = {
|
||||||
|
...queryFilters,
|
||||||
|
where: {
|
||||||
|
...((queryFilters as any).where || {}),
|
||||||
|
...Operands.WHERE_PUBLISHED(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = getSearchArticlesQuery(queryFilters)
|
||||||
|
|
||||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
|
37
src/utils/data.utils.ts
Normal file
37
src/utils/data.utils.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
UnbodyGoogleDoc,
|
||||||
|
UnbodyImageBlock,
|
||||||
|
UnbodyTextBlock,
|
||||||
|
} from '@/lib/unbody/unbody.types'
|
||||||
|
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||||
|
|
||||||
|
function hasClassName(inputString: string, className: string) {
|
||||||
|
const regex = new RegExp(`class\\s*=\\s*"[^"]*\\b${className}\\b[^"]*"`)
|
||||||
|
return regex.test(inputString)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getContentBlocks = (
|
||||||
|
blocks: (UnbodyImageBlock | UnbodyTextBlock)[],
|
||||||
|
) => {
|
||||||
|
return blocks.filter((b) => {
|
||||||
|
return (
|
||||||
|
(b.__typename === UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock &&
|
||||||
|
b.order !== 4) ||
|
||||||
|
(b.__typename === UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock &&
|
||||||
|
!hasClassName(b.html, 'subtitle') &&
|
||||||
|
!hasClassName(b.html, 'title'))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getArticleCover = (
|
||||||
|
blocks: (UnbodyImageBlock | UnbodyTextBlock)[],
|
||||||
|
): UnbodyImageBlock | null => {
|
||||||
|
return (
|
||||||
|
(blocks.find(
|
||||||
|
(b) =>
|
||||||
|
b.order === 4 &&
|
||||||
|
b.__typename === UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock,
|
||||||
|
) as UnbodyImageBlock) || null
|
||||||
|
)
|
||||||
|
}
|
31
src/utils/html.utils.ts
Normal file
31
src/utils/html.utils.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const regexForInnerHtml = /<[^>]*>/
|
||||||
|
|
||||||
|
// const regexForInnerHtml = /<[^>]*>([^<]*)<\/[^>]*>/
|
||||||
|
const regexForId = /id="([^"]*)"/
|
||||||
|
const regexForClass = /class="([^"]*)"/
|
||||||
|
//
|
||||||
|
// export const extractInnerHtml = (html: string) => {
|
||||||
|
// const match = html.match(regexForInnerHtml)
|
||||||
|
// return match ? match[1] : html
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function extractInnerHtml(htmlString: string) {
|
||||||
|
var regex = /^<[^>]+>([\s\S]*)<\/[^>]+>$/
|
||||||
|
var match = regex.exec(htmlString)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return match[1]
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractIdFromFirstTag = (html: string) => {
|
||||||
|
const match = html.match(regexForId)
|
||||||
|
return match ? match[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractClassFromFirstTag = (html: string) => {
|
||||||
|
const match = html.match(regexForClass)
|
||||||
|
return match ? match[1] : null
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user