This commit is contained in:
amirhouieh 2023-08-29 19:58:15 +02:00
parent 482269fc9e
commit 8844b33adf
9 changed files with 202 additions and 112 deletions

View File

@ -5,6 +5,8 @@ import { GridItem } from '../Grid/Grid'
import { ResponsiveImage } from '../ResponsiveImage/ResponsiveImage'
import ContentBlockFooter from './ContentBlockFooter'
import ContentBlockHeader, { BlockType } from './ContentBlock.Header'
import { lsdUtils } from '@/utils/lsd.utils'
import { Typography } from '@acid-info/lsd-react'
type Props = LPE.Search.ResultItemBase<LPE.Post.ImageBlock>
@ -24,6 +26,9 @@ const ImageBlock = (props: Props) => {
type={BlockType.IMAGE}
date={document?.modifiedAt ? new Date(document?.modifiedAt) : null}
/>
<Typography variant={'body2'} component={'p'}>
{data.alt}
</Typography>
<ContentBlockFooter data={document} order={order} />
</Container>
)
@ -33,7 +38,13 @@ const Container = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
padding: 24px 0;
padding: 0 0;
position: relative;
figure {
margin: 0;
padding: 0;
${lsdUtils.typography('body2')}
}
`
export default ImageBlock

View File

@ -6,6 +6,7 @@ import { LPE } from '../../types/lpe.types'
import { GridItem } from '../Grid/Grid'
import ContentBlockFooter from './ContentBlockFooter'
import ContentBlockHeader, { BlockType } from './ContentBlock.Header'
import { NicerTextFormat } from '@/components/Search/SearchResult.NicerTextFormat'
type Props = LPE.Search.ResultItemBase<LPE.Post.TextBlock>
@ -28,9 +29,9 @@ const TextBlock = (props: Props) => {
: null
}
/>
<Typography variant="body2" genericFontFamily="sans-serif">
{text}
</Typography>
<NicerTextFormat variant="body2" genericFontFamily="sans-serif">
{text as string}
</NicerTextFormat>
<ContentBlockFooter data={document} order={order} />
</Container>
)

View File

@ -0,0 +1,37 @@
import React from 'react'
import { Typography, TypographyProps } from '@acid-info/lsd-react'
type UrlReplacerProps = {
rawHtml: string
} & TypographyProps
function extractHostname(url: string): string {
// Use an anchor element to extract the hostname
const a = document.createElement('a')
a.href = url
return a.hostname.split('.').slice(-2, -1).join('.')
}
function formatHtmlWithUrls(rawHtml: string): string {
// Regular expression to match URLs
const urlRegex = /https?:\/\/([a-zA-Z0-9.-]+)\/[^ ]*/g
return rawHtml.replace(urlRegex, (match) => {
const readableHost = extractHostname(match)
return `<a href="${match}" target="_blank" rel="noopener noreferrer">${readableHost}</a>`
})
}
export const NicerTextFormat: React.FC<TypographyProps> = ({
children,
...props
}) => {
const formattedHtml = formatHtmlWithUrls(children as string)
return (
<Typography
{...props}
dangerouslySetInnerHTML={{ __html: formattedHtml }}
/>
)
}

View File

@ -4,18 +4,18 @@ import { ParagraphIcon } from '@/components/Icons/ParagraphIcon'
import { ResponsiveImage } from '@/components/ResponsiveImage/ResponsiveImage'
import { Grid, GridItem } from '@/components/Grid/Grid'
import { Typography } from '@acid-info/lsd-react'
import { uiConfigs } from '@/configs/ui.configs'
import { NicerTextFormat } from '@/components/Search/SearchResult.NicerTextFormat'
interface Props {
blocks: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
textBlocks: LPE.Post.TextBlock[]
imageBlocks: LPE.Post.ImageBlock[]
}
export const SearchResultTopPostBlocks = ({ blocks }: Props) => {
const imageBlocks = blocks.filter(
(block) => block.type === LPE.ContentTypes.Image,
)
const textBlocks = blocks.filter(
(block) => block.type === LPE.ContentTypes.Text,
)
export const SearchResultTopPostBlocks = ({
textBlocks,
imageBlocks,
}: Props) => {
return (
<Container>
<div>
@ -26,17 +26,16 @@ export const SearchResultTopPostBlocks = ({ blocks }: Props) => {
{textBlocks.map((block, index) => (
<TextBlockItem key={`para-${index}`}>
<ParagraphIcon />
<Typography variant={'subtitle2'}>
{(block.data as LPE.Post.TextBlock).text.slice(0, 60)}...
</Typography>
<NicerTextFormat variant={'subtitle2'}>
{block.text}
</NicerTextFormat>
</TextBlockItem>
))}
</TextBlocks>
)}
{imageBlocks.length > 0 && (
<ImageBlocks>
{imageBlocks.map((block, index) => {
const data = block.data as LPE.Post.ImageBlock
{imageBlocks.map((data, index) => {
const isPortrait = data.width < data.height
return (
<ImageBlockItem
@ -65,16 +64,22 @@ const TextBlockItem = styled.div`
display: flex;
align-items: center;
gap: 8px;
> *:last-of-type {
max-width: 70%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
`
const ImageBlocks = styled.div`
display: flex;
flex-wrap: wrap;
gap: 16px;
gap: 8px;
margin-top: 8px;
`
const ImageBlockItem = styled.div`
width: 30%;
width: 20%;
.refImage__portrait {
width: 15%;
}

View File

@ -8,9 +8,16 @@ import { lsdUtils } from '@/utils/lsd.utils'
interface Props {
post: LPE.Search.ResultItemBase<LPE.Post.Document>
shows: LPE.Podcast.Show[]
blocks: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
relatedTextBlocks: LPE.Article.TextBlock[]
relatedImageBlocks: LPE.Article.ImageBlock[]
}
export const SearchResultTopPost = ({ post, shows, blocks }: Props) => {
export const SearchResultTopPost = ({
post,
shows,
relatedImageBlocks,
relatedTextBlocks,
}: Props) => {
const data = PostCard.toData(post.data, shows)
return (
<Container>
@ -23,7 +30,12 @@ export const SearchResultTopPost = ({ post, shows, blocks }: Props) => {
size={'large'}
contentType={post.type as LPE.PostType}
/>
{blocks.length > 0 && <SearchResultTopPostBlocks blocks={blocks} />}
{relatedTextBlocks.length + relatedImageBlocks.length > 0 && (
<SearchResultTopPostBlocks
imageBlocks={relatedImageBlocks}
textBlocks={relatedTextBlocks}
/>
)}
</Container>
)
}

View File

@ -10,4 +10,9 @@ export const uiConfigs = {
largeColumn: 4,
smallColumn: 2,
},
searchResult: {
numberOfParagraphsShowInTopResult: 4,
numberOfImagesShowInTopResult: 3,
numberOfTotalBlocksInListView: 20,
},
}

View File

@ -10,18 +10,22 @@ import { SearchResultListBlocks } from '@/components/Search/SearchResult.Blocks'
import { Typography } from '@acid-info/lsd-react'
import { lsdUtils } from '@/utils/lsd.utils'
import { uiConfigs } from '@/configs/ui.configs'
import useWindowSize from '@/utils/ui.utils'
interface Props {
posts: LPE.Search.ResultItemBase<LPE.Post.Document>[]
blocks: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
shows: LPE.Podcast.Show[]
busy: boolean
showTopPost: boolean
}
export const SearchResultsListView = (props: Props) => {
const { posts, shows, blocks, busy } = props
const { posts, shows, blocks, busy, showTopPost } = props
const isMobile = useWindowSize().width < 768
const mostReferredPostIndex = useMemo(() => {
if (!showTopPost) return -1
// Extract the IDs of the first 3 posts
const firstThreePostIds = posts.slice(0, 3).map((post) => post.data.id)
@ -57,20 +61,45 @@ export const SearchResultsListView = (props: Props) => {
return mostReferredPostIndex >= 0 ? posts[mostReferredPostIndex] : null
}, [mostReferredPostIndex])
const [renderPosts, renderBlocks, topResultBlocks] = useMemo(() => {
const [
renderPosts,
renderBlocks,
imageBlocksInTopResult,
textBlocksInTopResult,
] = useMemo(() => {
const _renderPosts = topPost
? posts.filter((p) => p.data.id !== topPost.data.id)
: posts
// we want to only show those blocks in top results
// that are among the top 10 results
const _topResultBlocks = blocks
.slice(0, 10)
.filter((b) => b.data.document.id === topPost?.data.id)
const _renderBlocks = blocks.filter(
(b) =>
_topResultBlocks.findIndex((tb) => tb.data.id === b.data.id) === -1,
const blocksRelatedToTopPost = blocks.filter(
(b) => b.data.document.id === topPost?.data.id,
)
return [_renderPosts, _renderBlocks, _topResultBlocks]
const imageBlocksInTopResult = blocksRelatedToTopPost
.filter((block) => block.type === LPE.ContentTypes.Image)
.slice(0, uiConfigs.searchResult.numberOfImagesShowInTopResult)
.map((b) => b.data)
const textBlocksInTopResult = blocksRelatedToTopPost
.filter((block) => block.type === LPE.ContentTypes.Text)
.slice(0, uiConfigs.searchResult.numberOfParagraphsShowInTopResult)
.map((b) => b.data)
const _renderBlocks = blocks.filter((b) => {
if (b.type === LPE.ContentTypes.Image) {
return (
imageBlocksInTopResult.findIndex((ib) => ib.id === b.data.id) === -1
)
}
return true
})
return [
_renderPosts,
_renderBlocks,
imageBlocksInTopResult,
textBlocksInTopResult,
]
}, [posts, blocks, topPost])
return (
@ -84,7 +113,12 @@ export const SearchResultsListView = (props: Props) => {
<SearchResultTopPost
post={topPost}
shows={shows}
blocks={topResultBlocks}
relatedImageBlocks={
imageBlocksInTopResult as LPE.Article.ImageBlock[]
}
relatedTextBlocks={
textBlocksInTopResult as LPE.Article.TextBlock[]
}
/>
</PostsListHeader>
)}
@ -97,7 +131,8 @@ export const SearchResultsListView = (props: Props) => {
<SearchResultListPosts posts={renderPosts} shows={shows} />
</>
) : (
!busy && (
!busy &&
!topPost && (
<Typography variant={'subtitle2'} genericFontFamily={'serif'}>
No results found
</Typography>
@ -107,20 +142,21 @@ export const SearchResultsListView = (props: Props) => {
</PostsList>
<GridItem className={'w-1'} />
<BlocksList className={'w-3'}>
{renderBlocks.length > 0 ? (
<BlockListSticky>
<SearchResultsListHeader
title={copyConfigs.search.labels.relatedContent}
/>
<SearchResultListBlocks blocks={renderBlocks} />
</BlockListSticky>
) : (
!busy && (
<Typography variant={'subtitle2'} genericFontFamily={'serif'}>
No related content found
</Typography>
)
)}
{!isMobile &&
(renderBlocks.length > 0 ? (
<BlockListSticky>
<SearchResultsListHeader
title={copyConfigs.search.labels.relatedContent}
/>
<SearchResultListBlocks blocks={renderBlocks} />
</BlockListSticky>
) : (
!busy && (
<Typography variant={'subtitle2'} genericFontFamily={'serif'}>
No related content found
</Typography>
)
))}
</BlocksList>
</Container>
)
@ -128,6 +164,7 @@ export const SearchResultsListView = (props: Props) => {
const Container = styled(Grid)`
padding-top: 56px;
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
padding-top: 32px;
}
@ -137,6 +174,7 @@ const PostsList = styled(GridItem)`
display: flex;
flex-direction: column;
gap: 56px;
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
gap: 32px;
}

View File

@ -15,6 +15,7 @@ import { ApiResponse } from '@/types/data.types'
import useWindowSize from '@/utils/ui.utils'
import { uiConfigs } from '@/configs/ui.configs'
import themeState, { useThemeState } from '@/states/themeState/theme.state'
import { searchBlocksBasicFilter } from '@/utils/search.utils'
interface SearchPageProps {
topics: string[]
@ -41,16 +42,30 @@ export default function SearchPage({ topics, shows }: SearchPageProps) {
const [types, setTypes] = useState<string[]>([])
const { data, isLoading } = useQuery(['search', query, tags, types], () => {
return api.search({
query: query.length > 0 ? query : ' ',
tags,
type: types as LPE.ContentType[],
})
return api
.search({
query: query.length > 0 ? query : ' ',
tags,
type: types as LPE.ContentType[],
})
.then((res) => {
if (!res) return
if (res.errors) return
if (!res.data) return
return {
...res.data,
blocks: res.data.blocks.filter((b) =>
searchBlocksBasicFilter(
b as LPE.Search.ResultItemBase<LPE.Post.ContentBlock>,
),
),
}
})
})
const blocks = (data?.data.blocks ||
const blocks = (data?.blocks ||
[]) as LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
const posts = (data?.data.posts ||
const posts = (data?.posts ||
[]) as LPE.Search.ResultItemBase<LPE.Post.Document>[]
const handleSearch = async (
query: string,
@ -86,10 +101,14 @@ export default function SearchPage({ topics, shows }: SearchPageProps) {
/>
{view === 'list' && (
<SearchResultsListView
blocks={blocks}
blocks={blocks.slice(
0,
uiConfigs.searchResult.numberOfTotalBlocksInListView,
)}
posts={posts}
shows={shows}
busy={isLoading}
showTopPost={query.length > 0}
/>
)}
{view === 'explore' && (

View File

@ -1,61 +1,23 @@
import { SearchResultItem } from '@/types/data.types'
import { ParsedUrlQuery } from 'querystring'
import { LPE } from '@/types/lpe.types'
export const extractTopicsFromQuery = (query: ParsedUrlQuery): string[] => {
return query.topic ? (query.topic as string).split(',') : []
}
export const extractContentTypesFromQuery = (
query: ParsedUrlQuery,
): string[] => {
return query.type ? (query.type as string).split(',') : []
}
export const addTopicsToQuery = (topics: string[]): string | undefined => {
return topics.length ? `topic=${topics.join(',')}` : undefined
}
export const addContentTypesToQuery = (
contentTypes: string[],
): string | undefined => {
return contentTypes.length ? `type=${contentTypes.join(',')}` : undefined
}
export const extractQueryFromQuery = (queryObj: ParsedUrlQuery): string => {
return (queryObj.query as string) || ''
}
export const addQueryToQuery = (query: string): string | undefined => {
return query.length > 0 ? `query=${query}` : undefined
}
export const createMinimizedSearchText = (
query: string,
filterTags: string[],
export const searchBlocksBasicFilter = (
block: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>,
) => {
let txt = ''
if (query !== undefined && query.length > 0) {
txt += `<span>${query}</span>`
const isTitle = (b: LPE.Post.TextBlock) => {
return b.classNames.includes('title')
}
if (filterTags.length > 0) {
if (txt.length > 0) txt += '<b> . </b>'
txt += `${filterTags.map((t) => `<small>[${t}]</small>`).join(' ')}`
}
return txt
}
export const createSearchLink = (query: string, filterTags: string[]) => {
let link = '/search'
if (query.length > 0 || filterTags.length > 0) {
link += '?'
}
if (query.length > 0 && filterTags.length > 0) {
link += `query=${query}&topics=${filterTags.join(',')}`
} else if (query.length > 0) {
link += `query=${query}`
} else if (filterTags.length > 0) {
link += `topics=${filterTags.join(',')}`
const isLongEnough = (b: LPE.Post.TextBlock) => {
return b.text.length > 60
}
return link
if (block.type === LPE.ContentTypes.Text) {
return (
!isTitle(block.data as LPE.Post.TextBlock) &&
isLongEnough(block.data as LPE.Post.TextBlock)
)
} else {
// is an image
const isPodcastImage = block.data.document.type === LPE.PostTypes.Podcast
return !isPodcastImage
}
}