diff --git a/src/components/ContentBlock/ImageBlock.tsx b/src/components/ContentBlock/ImageBlock.tsx index a6a9c96..6bc80a6 100644 --- a/src/components/ContentBlock/ImageBlock.tsx +++ b/src/components/ContentBlock/ImageBlock.tsx @@ -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 @@ -24,6 +26,9 @@ const ImageBlock = (props: Props) => { type={BlockType.IMAGE} date={document?.modifiedAt ? new Date(document?.modifiedAt) : null} /> + + {data.alt} + ) @@ -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 diff --git a/src/components/ContentBlock/TextBlock.tsx b/src/components/ContentBlock/TextBlock.tsx index 058254b..21a850c 100644 --- a/src/components/ContentBlock/TextBlock.tsx +++ b/src/components/ContentBlock/TextBlock.tsx @@ -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 @@ -28,9 +29,9 @@ const TextBlock = (props: Props) => { : null } /> - - {text} - + + {text as string} + ) diff --git a/src/components/Search/SearchResult.NicerTextFormat.tsx b/src/components/Search/SearchResult.NicerTextFormat.tsx index e69de29..e7cbc47 100644 --- a/src/components/Search/SearchResult.NicerTextFormat.tsx +++ b/src/components/Search/SearchResult.NicerTextFormat.tsx @@ -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 `${readableHost}` + }) +} + +export const NicerTextFormat: React.FC = ({ + children, + ...props +}) => { + const formattedHtml = formatHtmlWithUrls(children as string) + return ( + + ) +} diff --git a/src/components/Search/SearchResult.TopPost.Blocks.tsx b/src/components/Search/SearchResult.TopPost.Blocks.tsx index a92b26c..dd80cf8 100644 --- a/src/components/Search/SearchResult.TopPost.Blocks.tsx +++ b/src/components/Search/SearchResult.TopPost.Blocks.tsx @@ -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[] + 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 (
@@ -26,17 +26,16 @@ export const SearchResultTopPostBlocks = ({ blocks }: Props) => { {textBlocks.map((block, index) => ( - - {(block.data as LPE.Post.TextBlock).text.slice(0, 60)}... - + + {block.text} + ))} )} {imageBlocks.length > 0 && ( - {imageBlocks.map((block, index) => { - const data = block.data as LPE.Post.ImageBlock + {imageBlocks.map((data, index) => { const isPortrait = data.width < data.height return ( *: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%; } diff --git a/src/components/Search/SearchResult.TopPost.tsx b/src/components/Search/SearchResult.TopPost.tsx index 332c99d..a3a4a87 100644 --- a/src/components/Search/SearchResult.TopPost.tsx +++ b/src/components/Search/SearchResult.TopPost.tsx @@ -8,9 +8,16 @@ import { lsdUtils } from '@/utils/lsd.utils' interface Props { post: LPE.Search.ResultItemBase shows: LPE.Podcast.Show[] - blocks: LPE.Search.ResultItemBase[] + 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 ( @@ -23,7 +30,12 @@ export const SearchResultTopPost = ({ post, shows, blocks }: Props) => { size={'large'} contentType={post.type as LPE.PostType} /> - {blocks.length > 0 && } + {relatedTextBlocks.length + relatedImageBlocks.length > 0 && ( + + )} ) } diff --git a/src/configs/ui.configs.ts b/src/configs/ui.configs.ts index d6702a8..b4d3ffa 100644 --- a/src/configs/ui.configs.ts +++ b/src/configs/ui.configs.ts @@ -10,4 +10,9 @@ export const uiConfigs = { largeColumn: 4, smallColumn: 2, }, + searchResult: { + numberOfParagraphsShowInTopResult: 4, + numberOfImagesShowInTopResult: 3, + numberOfTotalBlocksInListView: 20, + }, } diff --git a/src/containers/Search/ListView.tsx b/src/containers/Search/ListView.tsx index de9bc49..9601175 100644 --- a/src/containers/Search/ListView.tsx +++ b/src/containers/Search/ListView.tsx @@ -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[] blocks: LPE.Search.ResultItemBase[] 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) => { )} @@ -97,7 +131,8 @@ export const SearchResultsListView = (props: Props) => { ) : ( - !busy && ( + !busy && + !topPost && ( No results found @@ -107,20 +142,21 @@ export const SearchResultsListView = (props: Props) => { - {renderBlocks.length > 0 ? ( - - - - - ) : ( - !busy && ( - - No related content found - - ) - )} + {!isMobile && + (renderBlocks.length > 0 ? ( + + + + + ) : ( + !busy && ( + + No related content found + + ) + ))} ) @@ -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; } diff --git a/src/pages/search.tsx b/src/pages/search.tsx index d4874a2..5d739bd 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -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([]) 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, + ), + ), + } + }) }) - const blocks = (data?.data.blocks || + const blocks = (data?.blocks || []) as LPE.Search.ResultItemBase[] - const posts = (data?.data.posts || + const posts = (data?.posts || []) as LPE.Search.ResultItemBase[] const handleSearch = async ( query: string, @@ -86,10 +101,14 @@ export default function SearchPage({ topics, shows }: SearchPageProps) { /> {view === 'list' && ( 0} /> )} {view === 'explore' && ( diff --git a/src/utils/search.utils.ts b/src/utils/search.utils.ts index 64b0c7a..2e3da5f 100644 --- a/src/utils/search.utils.ts +++ b/src/utils/search.utils.ts @@ -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, ) => { - let txt = '' - if (query !== undefined && query.length > 0) { - txt += `${query}` + const isTitle = (b: LPE.Post.TextBlock) => { + return b.classNames.includes('title') } - if (filterTags.length > 0) { - if (txt.length > 0) txt += ' . ' - txt += `${filterTags.map((t) => `[${t}]`).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 + } }