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 { moreFromAuthor, references, relatedArticles } from './tempData'
|
||||
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 {
|
||||
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 articleContainer = useArticleContainerContext()
|
||||
const { tocIndex, setTocIndex } = articleContainer
|
||||
@ -26,60 +38,26 @@ export default function Article({ data }: Props) {
|
||||
const date = new Date(createdAt)
|
||||
|
||||
const _thumbnail = useMemo(() => {
|
||||
const imageBlocks: UnbodyImageBlock[] = blocks.filter(
|
||||
(block): block is UnbodyImageBlock => block !== null && 'url' in block,
|
||||
)
|
||||
|
||||
const coverImage = imageBlocks.reduce((prev, curr) =>
|
||||
prev.order < curr.order ? prev : curr,
|
||||
)
|
||||
|
||||
const coverImage = getArticleCover(blocks)
|
||||
if (!coverImage) return null
|
||||
|
||||
return (
|
||||
<ThumbnailContainer aspectRatio={PostImageRatio.LANDSCAPE}>
|
||||
<Thumbnail fill src={coverImage.url} alt={coverImage.alt} />
|
||||
</ThumbnailContainer>
|
||||
<ArticleImageBlockWrapper
|
||||
ratio={PostImageRatio.LANDSCAPE}
|
||||
image={coverImage}
|
||||
/>
|
||||
)
|
||||
}, [blocks])
|
||||
|
||||
const _blocks = useMemo(() => {
|
||||
// Exclude title, subtitle, coverImage
|
||||
const articleBlocks = blocks.sort((a, b) => a.order - b.order).slice(3)
|
||||
|
||||
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 }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return getContentBlocks(blocks).map((block, idx) => (
|
||||
<RenderArticleBlock key={'block-' + idx} block={block} />
|
||||
))
|
||||
}, [blocks])
|
||||
|
||||
const _mobileToc = useMemo(
|
||||
() =>
|
||||
toc?.length > 0 && (
|
||||
<Collapse className={styles.mobileToc} label="Contents">
|
||||
{/* @ts-ignore */}
|
||||
{toc.map((toc, idx) => (
|
||||
<Content
|
||||
onClick={() => setTocIndex(idx)}
|
||||
@ -87,7 +65,7 @@ export default function Article({ data }: Props) {
|
||||
variant="body3"
|
||||
key={idx}
|
||||
>
|
||||
{toc}
|
||||
{toc.title}
|
||||
</Content>
|
||||
))}
|
||||
</Collapse>
|
||||
@ -161,11 +139,9 @@ export default function Article({ data }: Props) {
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* assign id for toc scroll */}
|
||||
|
||||
<Title
|
||||
/*
|
||||
// @ts-ignore */
|
||||
// @ts-ignore */
|
||||
id={toc[0].href.substring(1)}
|
||||
variant={'h1'}
|
||||
genericFontFamily="serif"
|
||||
@ -259,32 +235,6 @@ const TextContainer = styled.div`
|
||||
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`
|
||||
display: flex;
|
||||
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 PostsDemo } from './PostsDemo'
|
||||
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) {
|
||||
const articleContainer = useArticleContainerContext()
|
||||
const { tocIndex, setTocIndex } = articleContainer
|
||||
const dy = uiConfigs.navbarRenderedHeight + uiConfigs.postMarginTop
|
||||
const dy = uiConfigs.navbarRenderedHeight + uiConfigs.postSectionMargin
|
||||
|
||||
const { sticky, stickyRef, height } = useSticky<HTMLDivElement>(dy)
|
||||
|
||||
@ -86,7 +86,8 @@ const Contents = styled.div<{ height: number }>`
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
height: calc(
|
||||
100vh - ${uiConfigs.navbarRenderedHeight + uiConfigs.postMarginTop + 40}px
|
||||
100vh -
|
||||
${uiConfigs.navbarRenderedHeight + uiConfigs.postSectionMargin + 40}px
|
||||
);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const uiConfigs = {
|
||||
navbarRenderedHeight: 45,
|
||||
postMarginTop: 78,
|
||||
postSectionMargin: 78,
|
||||
}
|
||||
|
@ -1,39 +1,37 @@
|
||||
import { Article } from '@/components/Article'
|
||||
import { TableOfContents } from '@/components/TableOfContents'
|
||||
import styled from '@emotion/styled'
|
||||
import { useState } from 'react'
|
||||
import { uiConfigs } from '@/configs/ui.configs'
|
||||
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
||||
import { UnbodyGoogleDoc, UnbodyTocItem } from '@/lib/unbody/unbody.types'
|
||||
import ArticleBody from '@/components/Article/ArticleBody'
|
||||
|
||||
interface Props {
|
||||
data: UnbodyGoogleDoc
|
||||
error: string | null
|
||||
data: UnbodyGoogleDoc & {
|
||||
toc: UnbodyTocItem[]
|
||||
}
|
||||
}
|
||||
|
||||
const ArticleContainer = (props: Props) => {
|
||||
const { data, error } = props
|
||||
const { data } = props
|
||||
const [tocIndex, setTocIndex] = useState(0)
|
||||
|
||||
return !error?.length ? (
|
||||
return (
|
||||
<Container>
|
||||
<ArticleContainerContext.Provider
|
||||
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
||||
>
|
||||
<TableOfContents contents={data.toc ?? []} />
|
||||
<Article data={data} />
|
||||
<ArticleBody data={data} />
|
||||
<Right />
|
||||
</ArticleContainerContext.Provider>
|
||||
</Container>
|
||||
) : (
|
||||
<div>{error}</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: ${uiConfigs.postMarginTop}px;
|
||||
`
|
||||
|
||||
const Right = styled.aside`
|
||||
|
@ -6,6 +6,7 @@ import { Searchbar } from '@/components/Searchbar'
|
||||
import { ESearchScope } from '@/types/ui.types'
|
||||
import styles from './Article.layout.module.css'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { Main } from '@/components/Main'
|
||||
|
||||
export default function ArticleLayout(props: PropsWithChildren<any>) {
|
||||
const isDarkState = useIsDarkState()
|
||||
@ -16,7 +17,7 @@ export default function ArticleLayout(props: PropsWithChildren<any>) {
|
||||
<NavbarFiller />
|
||||
<Searchbar searchScope={ESearchScope.ARTICLE} />
|
||||
</header>
|
||||
<main>{props.children}</main>
|
||||
<Main>{props.children}</Main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
|
@ -5,6 +5,7 @@ import { Hero } from '@/components/Hero'
|
||||
import { NavbarFiller } from '@/components/Navbar/NavbarFiller'
|
||||
import { Searchbar } from '@/components/Searchbar'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { Main } from '@/components/Main'
|
||||
|
||||
export default function DefaultLayout(props: PropsWithChildren<any>) {
|
||||
const isDarkState = useIsDarkState()
|
||||
@ -17,7 +18,7 @@ export default function DefaultLayout(props: PropsWithChildren<any>) {
|
||||
<Hero />
|
||||
<Searchbar />
|
||||
</header>
|
||||
<main>{props.children}</main>
|
||||
<Main>{props.children}</Main>
|
||||
<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 interface TocItem {}
|
||||
|
||||
export interface TocItem {
|
||||
tag: string
|
||||
blockIndex: number
|
||||
href: string
|
||||
title: string
|
||||
level: number
|
||||
}
|
||||
export interface FootnoteItem {}
|
||||
}
|
||||
|
||||
@ -152,7 +157,7 @@ export namespace UnbodyGraphQl {
|
||||
modifiedAt: string
|
||||
originalName: string
|
||||
path: string[]
|
||||
pathstring: string
|
||||
pathString: string
|
||||
remoteId: string
|
||||
size: number
|
||||
sourceId: string
|
||||
@ -298,7 +303,7 @@ export namespace UnbodyGraphQl {
|
||||
|
||||
export interface WhereOperandsInpObj {
|
||||
operator?: WhereOperatorEnum
|
||||
path: string[]
|
||||
path: string[] | string
|
||||
operands?: WhereOperandsInpObj[]
|
||||
valueGeoRange?: WhereGeoRangeInpObj
|
||||
valueNumber?: number
|
||||
@ -330,7 +335,7 @@ export namespace UnbodyGraphQl {
|
||||
}
|
||||
|
||||
export interface WhereInpObj {
|
||||
path?: string[]
|
||||
path?: string[] | string
|
||||
valueInt?: number
|
||||
valueNumber?: number
|
||||
valueGeoRange?: WhereGeoRangeInpObj
|
||||
|
@ -6,6 +6,7 @@ export type UnbodyTextBlock = UnbodyGraphQl.TextBlock
|
||||
export type UnbodyImageBlock = UnbodyGraphQl.ImageBlock
|
||||
export type UnbodyAudio = UnbodyGraphQl.AudioFile
|
||||
export type UnbodyGetFilters = UnbodyGraphQl.Filters.GetDocsArgs
|
||||
export type UnbodyTocItem = UnbodyGraphQl.Fragments.TocItem
|
||||
|
||||
export * as UnbodyGraphQl from './unbody-content.types'
|
||||
|
||||
|
@ -7,35 +7,16 @@ import { ArticlePostData } from '@/types/data.types'
|
||||
import { SEO } from '@/components/SEO'
|
||||
|
||||
type ArticleProps = {
|
||||
data: ArticlePostData | null
|
||||
error: string | null
|
||||
data: ArticlePostData
|
||||
errors: string | null
|
||||
}
|
||||
|
||||
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||
const { remoteId } = params!
|
||||
|
||||
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' }} />
|
||||
|
||||
const ArticlePage = ({ data, errors }: ArticleProps) => {
|
||||
if (errors) return <div>{errors}</div>
|
||||
return (
|
||||
<>
|
||||
<SEO title={props.data.title} description={props.data.summary} />
|
||||
<ArticleContainer data={props.data} error={props.error} />
|
||||
<SEO title={data.title} description={data.summary} />
|
||||
<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) {
|
||||
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 { 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 = {
|
||||
posts: PostDataProps[]
|
||||
featured: PostDataProps | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export default function Home({ posts }: Props) {
|
||||
export default function Home({ posts, featured }: Props) {
|
||||
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 () => {
|
||||
const { data: posts, errors } = await api.getHomepagePosts()
|
||||
const {
|
||||
data: { posts, featured },
|
||||
errors,
|
||||
} = await api.getHomepagePosts()
|
||||
|
||||
return {
|
||||
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) => ({
|
||||
remoteId: post.remoteId,
|
||||
date: post.modifiedAt,
|
||||
title: post.title,
|
||||
description: post.summary,
|
||||
description: post.subtitle, // TODO: summary is not available
|
||||
author: 'Jinho',
|
||||
tags: post.tags,
|
||||
...(post.blocks && post.blocks!.length > 0
|
||||
? { coverImage: post.blocks![0] as UnbodyImageBlock }
|
||||
: {}),
|
||||
coverImage: getArticleCover(post.blocks),
|
||||
})),
|
||||
errors,
|
||||
},
|
||||
|
@ -21,8 +21,12 @@ import {
|
||||
extractTopicsFromQuery,
|
||||
} from '@/utils/search.utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
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 {
|
||||
articles: SearchResultItem<UnbodyGoogleDoc>[]
|
||||
@ -73,24 +77,22 @@ export default function SearchPage({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section>
|
||||
<strong>Related articles</strong>
|
||||
<hr />
|
||||
<div>
|
||||
{articles.loading && <div>...</div>}
|
||||
{!articles.error && articles.data && articles.data.length > 0 ? (
|
||||
articles.data.map((article: SearchResultItem<UnbodyGoogleDoc>) => (
|
||||
<div key={article.doc.remoteId}>
|
||||
<h2>{article.doc.title}</h2>
|
||||
<p>{article.doc.summary}</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>Nothing found</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<br />
|
||||
{articles.data?.length && (
|
||||
<Section title={'Related Articles'} matches={articles.data?.length}>
|
||||
<PostsList
|
||||
posts={articles.data.map((article) => ({
|
||||
remoteId: article.doc.remoteId,
|
||||
date: article.doc.modifiedAt,
|
||||
title: article.doc.title,
|
||||
description: article.doc.subtitle, // TODO: summary is not available
|
||||
author: 'Jinho',
|
||||
tags: article.doc.tags,
|
||||
coverImage: getArticleCover(article.doc.blocks),
|
||||
}))}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<strong>Related content blocks</strong>
|
||||
<hr />
|
||||
@ -156,6 +158,10 @@ export default function SearchPage({
|
||||
)
|
||||
}
|
||||
|
||||
SearchPage.getLayout = function getLayout(page: ReactNode) {
|
||||
return <SearchLayout>{page}</SearchLayout>
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const { data: articles = [] } = await unbodyApi.searchArticles()
|
||||
const { data: blocks = [] } = await unbodyApi.serachBlocks()
|
||||
|
@ -10,22 +10,26 @@ export const getArticlePostQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
||||
sourceId
|
||||
remoteId
|
||||
title
|
||||
subtitle
|
||||
summary
|
||||
tags
|
||||
createdAt
|
||||
modifiedAt
|
||||
toc
|
||||
slug
|
||||
blocks{
|
||||
...on ImageBlock{
|
||||
url
|
||||
alt
|
||||
order
|
||||
__typename
|
||||
}
|
||||
... on TextBlock {
|
||||
footnotes
|
||||
html
|
||||
order
|
||||
tagName
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
@ -12,14 +12,18 @@ export const getHomePagePostsQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
||||
GetGoogleDocQuery(args)(`
|
||||
remoteId
|
||||
title
|
||||
subtitle
|
||||
summary
|
||||
tags
|
||||
createdAt
|
||||
modifiedAt
|
||||
pathString
|
||||
blocks{
|
||||
...on ImageBlock{
|
||||
url
|
||||
alt
|
||||
order
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
@ -7,9 +7,18 @@ export const getSearchArticlesQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
||||
GetGoogleDocQuery(args)(`
|
||||
remoteId
|
||||
title
|
||||
subtitle
|
||||
summary
|
||||
tags
|
||||
modifiedAt
|
||||
blocks{
|
||||
...on ImageBlock{
|
||||
url
|
||||
alt
|
||||
order
|
||||
__typename
|
||||
}
|
||||
}
|
||||
_additional{
|
||||
certainty
|
||||
}
|
||||
|
@ -6,10 +6,13 @@ import {
|
||||
UnbodyImageBlock,
|
||||
UnbodyTextBlock,
|
||||
UnbodyGraphQlResponseBlocks,
|
||||
UnbodyGraphQlResponse,
|
||||
} from '@/lib/unbody/unbody.types'
|
||||
|
||||
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||
|
||||
type WhereOperandsInpObj = UnbodyGraphQl.Filters.WhereOperandsInpObj
|
||||
|
||||
import { getArticlePostQuery } from '@/queries/getPost'
|
||||
import { getHomePagePostsQuery } from '@/queries/getPosts'
|
||||
import { getAllPostsSlugQuery } from '@/queries/getPostsSlugs'
|
||||
@ -35,8 +38,41 @@ type HomepagePost = Pick<
|
||||
| 'blocks'
|
||||
>
|
||||
|
||||
type HomePageData = {
|
||||
posts: HomepagePost[]
|
||||
featured: HomepagePost | null
|
||||
}
|
||||
|
||||
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>(
|
||||
q: string,
|
||||
tags: string[],
|
||||
@ -68,17 +104,33 @@ class UnbodyService extends UnbodyClient {
|
||||
}
|
||||
}
|
||||
|
||||
getHomepagePosts = (): Promise<ApiResponse<HomepagePost[]>> => {
|
||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(getHomePagePostsQuery())
|
||||
getHomepagePosts = (): Promise<ApiResponse<HomePageData>> => {
|
||||
const query = getHomePagePostsQuery({
|
||||
where: Operands.WHERE_PUBLISHED(),
|
||||
})
|
||||
|
||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
||||
.then(({ data }) => {
|
||||
if (!data) return this.handleResponse([], 'No data')
|
||||
return this.handleResponse(data.Get.GoogleDoc)
|
||||
if (!data)
|
||||
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 }[]>> => {
|
||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(getAllPostsSlugQuery())
|
||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(
|
||||
getAllPostsSlugQuery({
|
||||
where: Operands.WHERE_PUBLISHED(),
|
||||
}),
|
||||
)
|
||||
.then(({ data }) => {
|
||||
if (!data) return this.handleResponse([], 'No data')
|
||||
return this.handleResponse(data.Get.GoogleDoc)
|
||||
@ -88,13 +140,15 @@ class UnbodyService extends UnbodyClient {
|
||||
|
||||
getArticlePost = (
|
||||
id: string,
|
||||
published: boolean = true,
|
||||
): Promise<ApiResponse<UnbodyGoogleDoc | null>> => {
|
||||
const query = getArticlePostQuery({
|
||||
where: {
|
||||
path: ['remoteId'],
|
||||
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.Equal,
|
||||
valueString: id,
|
||||
},
|
||||
where: published
|
||||
? {
|
||||
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.And,
|
||||
operands: [Operands.WHERE_PUBLISHED(), Operands.WHERE_ID_IS(id)],
|
||||
}
|
||||
: Operands.WHERE_ID_IS(id),
|
||||
})
|
||||
|
||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
||||
@ -103,6 +157,7 @@ class UnbodyService extends UnbodyClient {
|
||||
const article = data.Get.GoogleDoc[0]
|
||||
return this.handleResponse({
|
||||
...article,
|
||||
blocks: article.blocks.sort((a, b) => a.order - b.order),
|
||||
toc: JSON.parse(
|
||||
article.toc as string,
|
||||
) as Array<UnbodyGraphQl.Fragments.TocItem>,
|
||||
@ -114,6 +169,7 @@ class UnbodyService extends UnbodyClient {
|
||||
serachBlocks = async (
|
||||
q: string = '',
|
||||
tags: string[] = [],
|
||||
published: boolean = true,
|
||||
): Promise<
|
||||
ApiResponse<SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]>
|
||||
> => {
|
||||
@ -124,12 +180,14 @@ class UnbodyService extends UnbodyClient {
|
||||
concepts: [q, ...tags],
|
||||
certainty: 0.75,
|
||||
},
|
||||
...(published
|
||||
? {
|
||||
where: Operands.WHERE_PUBLISHED(),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
console.log(query)
|
||||
|
||||
return this.request<UnbodyGraphQlResponseBlocks>(query)
|
||||
.then(({ data }) => {
|
||||
if (!data || !(data.Get.ImageBlock || data.Get.TextBlock))
|
||||
@ -147,16 +205,20 @@ class UnbodyService extends UnbodyClient {
|
||||
searchArticles = (
|
||||
q: string = '',
|
||||
tags: string[] = [],
|
||||
published: boolean = true,
|
||||
): Promise<ApiResponse<SearchResultItem<UnbodyGoogleDoc>[]>> => {
|
||||
const query = getSearchArticlesQuery({
|
||||
...(q.trim().length > 0
|
||||
? {
|
||||
nearText: {
|
||||
concepts: [q],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...((tags.length > 0 && {
|
||||
let queryFilters = {}
|
||||
if (q.trim().length > 0) {
|
||||
queryFilters = {
|
||||
nearText: {
|
||||
concepts: [q],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (tags.length > 0) {
|
||||
queryFilters = {
|
||||
...queryFilters,
|
||||
where: {
|
||||
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.And,
|
||||
operands: tags.map((tag) => ({
|
||||
@ -165,9 +227,20 @@ class UnbodyService extends UnbodyClient {
|
||||
valueString: tag,
|
||||
})),
|
||||
},
|
||||
}) ||
|
||||
{}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (published) {
|
||||
queryFilters = {
|
||||
...queryFilters,
|
||||
where: {
|
||||
...((queryFilters as any).where || {}),
|
||||
...Operands.WHERE_PUBLISHED(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const query = getSearchArticlesQuery(queryFilters)
|
||||
|
||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
||||
.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