feat: implement content block component
This commit is contained in:
parent
87ed14e1b2
commit
39fea87d83
|
@ -0,0 +1,32 @@
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
author: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentBlockBody = ({ title, author }: Props) => {
|
||||||
|
return (
|
||||||
|
<BlockBodyContainer>
|
||||||
|
<Typography variant="body1" component="div" genericFontFamily="serif">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body3"
|
||||||
|
component="div"
|
||||||
|
genericFontFamily="sans-serif"
|
||||||
|
>
|
||||||
|
{author}
|
||||||
|
</Typography>
|
||||||
|
</BlockBodyContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlockBodyContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default ContentBlockBody
|
|
@ -0,0 +1,35 @@
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import { PostClassType } from '../Post/Post'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: PostClassType
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentBlockHeader = ({ type, date }: Props) => {
|
||||||
|
return (
|
||||||
|
<ContentBlockInfo>
|
||||||
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
{type.toUpperCase()}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body3">•</Typography>
|
||||||
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
|
{new Date(date).toLocaleString('en-GB', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</ContentBlockInfo>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentBlockInfo = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default ContentBlockHeader
|
|
@ -0,0 +1,64 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
import { SearchResultItem } from '@/types/data.types'
|
||||||
|
import { UnbodyImageBlock } from '@/lib/unbody/unbody.types'
|
||||||
|
|
||||||
|
import { GridItem } from '../Grid/Grid'
|
||||||
|
import { PostClassType } from '../Post/Post'
|
||||||
|
import ContentBlockHeader from './ContentBlock.Header'
|
||||||
|
import ContentBlockBody from './ContentBlock.Body'
|
||||||
|
|
||||||
|
type Props = Omit<SearchResultItem<UnbodyImageBlock>, 'score'>
|
||||||
|
|
||||||
|
const ImageBlock = ({ doc }: Props) => {
|
||||||
|
return (
|
||||||
|
<CustomGridItem className="w-2">
|
||||||
|
<BlockLink href={`/article/${doc.document[0].slug}`}>
|
||||||
|
<Container>
|
||||||
|
<ImageContainer>
|
||||||
|
<Image fill src={doc.url} alt={doc.alt} />
|
||||||
|
</ImageContainer>
|
||||||
|
<ContentBlockHeader
|
||||||
|
type={PostClassType.ARTICLE}
|
||||||
|
// TODO: type error with GoogleCalendarEvent
|
||||||
|
// which one do we use for date between createdAt and modifiedAt?
|
||||||
|
date={doc?.document[0].modifiedAt}
|
||||||
|
/>
|
||||||
|
<ContentBlockBody
|
||||||
|
title={doc.document[0].title}
|
||||||
|
author="Jason Freeman"
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</BlockLink>
|
||||||
|
</CustomGridItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomGridItem = styled(GridItem)`
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-column: span 8 !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const BlockLink = styled(Link)`
|
||||||
|
text-decoration: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-top: 1px solid rgb(var(--lsd-theme-primary));
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ImageContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
aspect-ratio: 1 / 1; // fixed aspect ratio temporarily
|
||||||
|
`
|
||||||
|
|
||||||
|
export default ImageBlock
|
|
@ -0,0 +1,49 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
import { SearchResultItem } from '@/types/data.types'
|
||||||
|
import { UnbodyTextBlock } from '@/lib/unbody/unbody.types'
|
||||||
|
|
||||||
|
import { GridItem } from '../Grid/Grid'
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import { PostClassType } from '../Post/Post'
|
||||||
|
import ContentBlockHeader from './ContentBlock.Header'
|
||||||
|
import ContentBlockBody from './ContentBlock.Body'
|
||||||
|
|
||||||
|
type Props = Omit<SearchResultItem<UnbodyTextBlock>, 'score'>
|
||||||
|
|
||||||
|
const TextBlock = ({ doc }: Props) => {
|
||||||
|
return (
|
||||||
|
<GridItem className="w-4">
|
||||||
|
<BlockLink href={`/article/${doc.document[0].slug}`}>
|
||||||
|
<Container>
|
||||||
|
<ContentBlockHeader
|
||||||
|
type={PostClassType.ARTICLE}
|
||||||
|
date={doc?.document[0].modifiedAt}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" genericFontFamily="sans-serif">
|
||||||
|
{doc.text}
|
||||||
|
</Typography>
|
||||||
|
<ContentBlockBody
|
||||||
|
title={doc.document[0].title}
|
||||||
|
author="Jason Freeman"
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</BlockLink>
|
||||||
|
</GridItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlockLink = styled(Link)`
|
||||||
|
text-decoration: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-top: 1px solid rgb(var(--lsd-theme-primary));
|
||||||
|
`
|
||||||
|
|
||||||
|
export default TextBlock
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as ImageBlock } from './ImageBlock'
|
||||||
|
export { default as TextBlock } from './TextBlock'
|
|
@ -6,9 +6,10 @@ export const Grid = styled.div`
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
// TODO: The mobile design works when commenting this out
|
||||||
|
/* @media (max-width: 768px) {
|
||||||
grid-template-columns: 100%;
|
grid-template-columns: 100%;
|
||||||
}
|
} */
|
||||||
`
|
`
|
||||||
|
|
||||||
export const GridItem = styled.div`
|
export const GridItem = styled.div`
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { getArticleCover } from '@/utils/data.utils'
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { PostsList } from '../PostList/PostList'
|
||||||
|
import { Section } from '../Section/Section'
|
||||||
|
import { SearchResultItem } from '@/types/data.types'
|
||||||
|
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
articles: SearchResultItem<UnbodyGoogleDoc>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RelatedArticles({ articles }: Props) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Section title={'Related Articles'} matches={articles?.length}>
|
||||||
|
<PostsList
|
||||||
|
posts={articles.map((article) => ({
|
||||||
|
slug: article.doc.slug,
|
||||||
|
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>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as RelatedArticles } from './RelatedArticles'
|
|
@ -0,0 +1,51 @@
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { Section } from '../Section/Section'
|
||||||
|
import { SearchResultItem } from '@/types/data.types'
|
||||||
|
import { UnbodyImageBlock, UnbodyTextBlock } from '@/lib/unbody/unbody.types'
|
||||||
|
import { Grid } from '../Grid/Grid'
|
||||||
|
import { ImageBlock, TextBlock } from '../ContentBlock'
|
||||||
|
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
blocks: SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RelatedContent({ blocks }: Props) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Section title={'Related Content'} matches={blocks?.length}>
|
||||||
|
<Grid>
|
||||||
|
{blocks.map(
|
||||||
|
(block: SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>) => {
|
||||||
|
if (!block.doc.document || !block.doc.document[0]) return null
|
||||||
|
|
||||||
|
let refArticle = null
|
||||||
|
if (UnbodyGraphQl.UnbodyDocumentTypeNames.GoogleDoc) {
|
||||||
|
refArticle = block.doc.document[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (block.doc.__typename) {
|
||||||
|
case UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock:
|
||||||
|
return <TextBlock doc={block.doc} />
|
||||||
|
case UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock: {
|
||||||
|
return <ImageBlock doc={block.doc} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 108px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as RelatedContent } from './RelatedContent'
|
|
@ -4,30 +4,18 @@ import {
|
||||||
UnbodyImageBlock,
|
UnbodyImageBlock,
|
||||||
UnbodyTextBlock,
|
UnbodyTextBlock,
|
||||||
} from '@/lib/unbody/unbody.types'
|
} from '@/lib/unbody/unbody.types'
|
||||||
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
|
||||||
|
|
||||||
import Image from 'next/image'
|
|
||||||
|
|
||||||
import unbodyApi from '@/services/unbody.service'
|
import unbodyApi from '@/services/unbody.service'
|
||||||
|
import { PostTypes, SearchResultItem } from '@/types/data.types'
|
||||||
import {
|
import {
|
||||||
PostTypes,
|
|
||||||
SearchHook,
|
|
||||||
SearchHookDataPayload,
|
|
||||||
SearchResultItem,
|
|
||||||
SearchResults,
|
|
||||||
} from '@/types/data.types'
|
|
||||||
import {
|
|
||||||
createMinimizedSearchText,
|
|
||||||
extractQueryFromQuery,
|
extractQueryFromQuery,
|
||||||
extractTopicsFromQuery,
|
extractTopicsFromQuery,
|
||||||
} from '@/utils/search.utils'
|
} from '@/utils/search.utils'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
|
||||||
import { SearchLayout } from '@/layouts/SearchLayout'
|
import { SearchLayout } from '@/layouts/SearchLayout'
|
||||||
import { Section } from '@/components/Section/Section'
|
import { RelatedArticles } from '@/components/RelatedArticles'
|
||||||
import { PostsList } from '@/components/PostList/PostList'
|
import { RelatedContent } from '@/components/RelatedContent'
|
||||||
import { getArticleCover } from '@/utils/data.utils'
|
|
||||||
|
|
||||||
interface SearchPageProps {
|
interface SearchPageProps {
|
||||||
articles: SearchResultItem<UnbodyGoogleDoc>[]
|
articles: SearchResultItem<UnbodyGoogleDoc>[]
|
||||||
|
@ -83,83 +71,10 @@ export default function SearchPage({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{articles.data?.length && (
|
{articles.data?.length && <RelatedArticles articles={articles.data} />}
|
||||||
<Section title={'Related Articles'} matches={articles.data?.length}>
|
|
||||||
<PostsList
|
|
||||||
posts={articles.data.map((article) => ({
|
|
||||||
slug: article.doc.slug,
|
|
||||||
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>
|
{/* TODO: used initialBlocks instead of blocks.data temporarily to render data */}
|
||||||
<strong>Related content blocks</strong>
|
{initialBlocks?.length && <RelatedContent blocks={initialBlocks} />}
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
{blocks.loading && <div>...</div>}
|
|
||||||
{!blocks.error && blocks.data && blocks.data.length > 0 ? (
|
|
||||||
blocks.data.map(
|
|
||||||
(block: SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>) => {
|
|
||||||
if (!block.doc.document || !block.doc.document[0]) return null
|
|
||||||
|
|
||||||
let refArticle = null
|
|
||||||
if (UnbodyGraphQl.UnbodyDocumentTypeNames.GoogleDoc) {
|
|
||||||
refArticle = block.doc.document[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (block.doc.__typename) {
|
|
||||||
case UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock:
|
|
||||||
return (
|
|
||||||
<div key={block.doc.slug}>
|
|
||||||
{refArticle && (
|
|
||||||
<h3>
|
|
||||||
<Link href={`/articles/${refArticle.slug}`}>
|
|
||||||
{refArticle.title}
|
|
||||||
</Link>
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
{
|
|
||||||
<p
|
|
||||||
dangerouslySetInnerHTML={{ __html: block.doc.html }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
case UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock: {
|
|
||||||
return (
|
|
||||||
<div key={block.doc.slug}>
|
|
||||||
{refArticle && (
|
|
||||||
<h3>
|
|
||||||
<Link href={`/articles/${refArticle.slug}`}>
|
|
||||||
{refArticle.title}
|
|
||||||
</Link>
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
<Image
|
|
||||||
title={block.doc.alt}
|
|
||||||
src={block.doc.url}
|
|
||||||
width={block.doc.width}
|
|
||||||
height={block.doc.height}
|
|
||||||
alt={block.doc.alt}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div>Nothing found</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const getSearchBlocksQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
||||||
title
|
title
|
||||||
mentions
|
mentions
|
||||||
slug
|
slug
|
||||||
|
modifiedAt
|
||||||
__typename
|
__typename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +40,7 @@ export const getSearchBlocksQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
||||||
title
|
title
|
||||||
slug
|
slug
|
||||||
mentions
|
mentions
|
||||||
|
modifiedAt
|
||||||
__typename
|
__typename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue