mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-23 06:38:27 +00:00
closing #140
This commit is contained in:
parent
482269fc9e
commit
8844b33adf
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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 }}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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%;
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -10,4 +10,9 @@ export const uiConfigs = {
|
||||
largeColumn: 4,
|
||||
smallColumn: 2,
|
||||
},
|
||||
searchResult: {
|
||||
numberOfParagraphsShowInTopResult: 4,
|
||||
numberOfImagesShowInTopResult: 3,
|
||||
numberOfTotalBlocksInListView: 20,
|
||||
},
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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' && (
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user